Skip to content

Commit

Permalink
Merge pull request #1896 from dusk-network/soft_slash
Browse files Browse the repository at this point in the history
  • Loading branch information
herr-seppia authored Jul 2, 2024
2 parents c6a2749 + f5d43a6 commit 4d1f725
Show file tree
Hide file tree
Showing 8 changed files with 184 additions and 76 deletions.
12 changes: 6 additions & 6 deletions contracts/stake/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ unsafe fn get_stake(arg_len: u32) -> u32 {
}

#[no_mangle]
unsafe fn slashed_amount(arg_len: u32) -> u32 {
rusk_abi::wrap_call(arg_len, |_: ()| STATE.slashed_amount())
unsafe fn burnt_amount(arg_len: u32) -> u32 {
rusk_abi::wrap_call(arg_len, |_: ()| STATE.burnt_amount())
}

#[no_mangle]
Expand All @@ -89,7 +89,7 @@ unsafe fn prev_state_changes(arg_len: u32) -> u32 {
unsafe fn before_state_transition(arg_len: u32) -> u32 {
rusk_abi::wrap_call(arg_len, |_: ()| {
assert_external_caller();
STATE.before_state_transition()
STATE.on_new_block()
})
}

Expand Down Expand Up @@ -126,10 +126,10 @@ unsafe fn hard_slash(arg_len: u32) -> u32 {
}

#[no_mangle]
unsafe fn set_slashed_amount(arg_len: u32) -> u32 {
rusk_abi::wrap_call(arg_len, |slashed_amount| {
unsafe fn set_burnt_amount(arg_len: u32) -> u32 {
rusk_abi::wrap_call(arg_len, |burnt_amount| {
assert_external_caller();
STATE.set_slashed_amount(slashed_amount)
STATE.set_burnt_amount(burnt_amount)
})
}

Expand Down
119 changes: 72 additions & 47 deletions contracts/stake/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use dusk_bytes::Serializable;
use execution_core::{
stake::{
next_epoch, Stake, StakeData, StakingEvent, Unstake, Withdraw, EPOCH,
STAKE_WARNINGS,
},
transfer::Mint,
StakePublicKey,
Expand All @@ -31,7 +32,7 @@ use crate::*;
#[derive(Debug, Default, Clone)]
pub struct StakeState {
stakes: BTreeMap<[u8; StakePublicKey::SIZE], (StakeData, StakePublicKey)>,
slashed_amount: u64,
burnt_amount: u64,
previous_block_state: BTreeMap<
[u8; StakePublicKey::SIZE],
(Option<StakeData>, StakePublicKey),
Expand All @@ -48,26 +49,26 @@ impl StakeState {
pub const fn new() -> Self {
Self {
stakes: BTreeMap::new(),
slashed_amount: 0u64,
burnt_amount: 0u64,
previous_block_state: BTreeMap::new(),
previous_block_height: 0,
}
}

pub fn before_state_transition(&mut self) {
pub fn on_new_block(&mut self) {
self.previous_block_state.clear()
}

fn clear_prev_if_needed(&mut self) {
fn check_new_block(&mut self) {
let current_height = rusk_abi::block_height();
if current_height != self.previous_block_height {
self.previous_block_height = current_height;
self.before_state_transition();
self.on_new_block();
}
}

pub fn stake(&mut self, stake: Stake) {
self.clear_prev_if_needed();
self.check_new_block();

if stake.value < MINIMUM_STAKE {
panic!("The staked value is lower than the minimum amount!");
Expand Down Expand Up @@ -108,7 +109,7 @@ impl StakeState {
}

pub fn unstake(&mut self, unstake: Unstake) {
self.clear_prev_if_needed();
self.check_new_block();

// remove the stake from a key and increment the signature counter
let loaded_stake = self
Expand Down Expand Up @@ -255,9 +256,11 @@ impl StakeState {
/// Rewards a `stake_pk` with the given `value`. If a stake does not exist
/// in the map for the key one will be created.
pub fn reward(&mut self, stake_pk: &StakePublicKey, value: u64) {
self.clear_prev_if_needed();
self.check_new_block();

let stake = self.load_or_create_stake_mut(stake_pk);
// Reset faults counter
stake.faults = 0;
stake.increase_reward(value);
rusk_abi::emit(
"reward",
Expand All @@ -268,9 +271,9 @@ impl StakeState {
);
}

/// Total amount slashed from the genesis
pub fn slashed_amount(&self) -> u64 {
self.slashed_amount
/// Total amount burned since the genesis
pub fn burnt_amount(&self) -> u64 {
self.burnt_amount
}

/// Version of the stake contract
Expand All @@ -283,19 +286,55 @@ impl StakeState {
/// If the reward is less than the `to_slash` amount, then the reward is
/// depleted and the provisioner eligibility is shifted to the
/// next epoch as well
pub fn slash(&mut self, stake_pk: &StakePublicKey, to_slash: u64) {
self.clear_prev_if_needed();
pub fn slash(&mut self, stake_pk: &StakePublicKey, to_slash: Option<u64>) {
self.check_new_block();

let stake = self
.get_stake_mut(stake_pk)
.expect("The stake to slash should exist");

// Stake can have no amount if provisioner unstake in the same block
if stake.amount().is_none() {
return;
}

let prev_value = Some(stake.clone());

let to_slash = min(to_slash, stake.reward);
stake.faults = stake.faults.saturating_add(1);
let effective_faults =
stake.faults.saturating_sub(STAKE_WARNINGS) as u64;

let (stake_amount, eligibility) =
stake.amount.as_mut().expect("stake_to_exists");

// Shift eligibility (aka stake suspension) only if warnings are
// saturated
if effective_faults > 0 {
// The stake is suspended for the rest of the current epoch plus
// effective_faults epochs
let to_shift = effective_faults * EPOCH;
*eligibility = next_epoch(rusk_abi::block_height()) + to_shift;
rusk_abi::emit(
"suspended",
StakingEvent {
public_key: *stake_pk,
value: *eligibility,
},
);
}

// Slash the provided amount or calculate the percentage according to
// effective faults
let to_slash =
to_slash.unwrap_or(*stake_amount / 100 * effective_faults * 10);
let to_slash = min(to_slash, *stake_amount);

if to_slash > 0 {
stake.reward -= to_slash;
// Move the slash amount from stake to reward and deduct contract
// balance
*stake_amount -= to_slash;
stake.increase_reward(to_slash);
Self::deduct_contract_balance(to_slash);

rusk_abi::emit(
"slash",
Expand All @@ -306,24 +345,6 @@ impl StakeState {
);
}

if stake.reward == 0 {
// stake.amount can be None if the provisioner unstake in the same
// block
if let Some((_, eligibility)) = stake.amount.as_mut() {
*eligibility = next_epoch(rusk_abi::block_height()) + EPOCH;
rusk_abi::emit(
"shifted",
StakingEvent {
public_key: *stake_pk,
value: *eligibility,
},
);
}
}

// Update the total slashed amount
self.slashed_amount += to_slash;

let key = stake_pk.to_bytes();
self.previous_block_state
.entry(key)
Expand All @@ -335,7 +356,7 @@ impl StakeState {
/// If the stake is less than the `to_slash` amount, then the stake is
/// depleted
pub fn hard_slash(&mut self, stake_pk: &StakePublicKey, to_slash: u64) {
self.clear_prev_if_needed();
self.check_new_block();

let stake_info = self
.get_stake_mut(stake_pk)
Expand All @@ -359,17 +380,10 @@ impl StakeState {
// Update the staked amount
stake.0 -= to_slash;

// Update the contract balance to reflect the change in the amount
// withdrawable from the contract
let _: bool = rusk_abi::call(
TRANSFER_CONTRACT,
"sub_contract_balance",
&(STAKE_CONTRACT, to_slash),
)
.expect("Subtracting balance should succeed");
Self::deduct_contract_balance(to_slash);

// Update the total slashed amount
self.slashed_amount += to_slash;
// Update the total burnt amount
self.burnt_amount += to_slash;

rusk_abi::emit(
"hard_slash",
Expand All @@ -384,9 +398,9 @@ impl StakeState {
.or_insert((prev_value, *stake_pk));
}

/// Sets the slashed amount
pub fn set_slashed_amount(&mut self, slashed_amount: u64) {
self.slashed_amount = slashed_amount;
/// Sets the burnt amount
pub fn set_burnt_amount(&mut self, burnt_amount: u64) {
self.burnt_amount = burnt_amount;
}

/// Feeds the host with the stakes.
Expand All @@ -396,6 +410,17 @@ impl StakeState {
}
}

fn deduct_contract_balance(amount: u64) {
// Update the module balance to reflect the change in the amount
// withdrawable from the contract
let _: () = rusk_abi::call(
TRANSFER_CONTRACT,
"sub_contract_balance",
&(STAKE_CONTRACT, amount),
)
.expect("Subtracting balance should succeed");
}

/// Feeds the host with previous state of the changed provisioners.
pub fn prev_state_changes(&self) {
for (stake_data, stake_pk) in self.previous_block_state.values() {
Expand Down
4 changes: 1 addition & 3 deletions contracts/stake/tests/common/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,7 @@ use execution_core::{
value_commitment, JubJubScalar, Note, PublicKey, SchnorrSecretKey,
SecretKey, Sender, TxSkeleton, ViewKey,
};
use rusk_abi::{
CallReceipt, ContractError, ContractId, Error, Session, TRANSFER_CONTRACT,
};
use rusk_abi::{CallReceipt, ContractError, Error, Session, TRANSFER_CONTRACT};

const POINT_LIMIT: u64 = 0x100000000;

Expand Down
52 changes: 51 additions & 1 deletion contracts/stake/tests/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,30 @@ fn reward_slash() -> Result<(), Error> {
let mut session = instantiate(rng, vm, &pk, GENESIS_VALUE);

let reward_amount = dusk(10.0);
let stake_amount = dusk(100.0);
let slash_amount = dusk(5.0);

let stake_data = StakeData {
reward: 0,
amount: Some((stake_amount, 0)),
counter: 0,
faults: 0,
};

session.call::<_, ()>(
TRANSFER_CONTRACT,
"add_contract_balance",
&(STAKE_CONTRACT, stake_amount),
u64::MAX,
)?;

session.call::<_, ()>(
STAKE_CONTRACT,
"insert_stake",
&(stake_pk, stake_data),
u64::MAX,
)?;

let receipt = session.call::<_, ()>(
STAKE_CONTRACT,
"reward",
Expand All @@ -49,10 +71,37 @@ fn reward_slash() -> Result<(), Error> {
let receipt = session.call::<_, ()>(
STAKE_CONTRACT,
"slash",
&(stake_pk, slash_amount),
&(stake_pk, Some(slash_amount)),
u64::MAX,
)?;
assert!(receipt.events.len() == 1, "No shift at first warn");
assert_event(&receipt.events, "slash", &stake_pk, slash_amount);
let stake_amount = stake_amount - slash_amount;

let receipt = session.call::<_, ()>(
STAKE_CONTRACT,
"slash",
&(stake_pk, None::<u64>),
u64::MAX,
)?;
// 10% of current amount
let slash_amount = stake_amount / 10;
assert_event(&receipt.events, "slash", &stake_pk, slash_amount);
assert_event(&receipt.events, "suspended", &stake_pk, 4320);

let receipt = session.call::<_, ()>(
STAKE_CONTRACT,
"slash",
&(stake_pk, None::<u64>),
u64::MAX,
)?;
let stake_amount = stake_amount - slash_amount;

// 20% of current amount
let slash_amount = stake_amount / 100 * 20;
assert_event(&receipt.events, "slash", &stake_pk, slash_amount);
assert_event(&receipt.events, "suspended", &stake_pk, 6480);

Ok(())
}

Expand All @@ -79,6 +128,7 @@ fn stake_hard_slash() -> Result<(), Error> {
reward: 0,
amount: Some((balance, block_height)),
counter: 0,
faults: 0,
};

session.call::<_, ()>(
Expand Down
6 changes: 6 additions & 0 deletions execution-core/src/stake.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ use crate::{
/// Epoch used for stake operations
pub const EPOCH: u64 = 2160;

/// Number of warnings before being penalized
pub const STAKE_WARNINGS: u8 = 1;

/// Calculate the block height at which the next epoch takes effect.
#[must_use]
pub const fn next_epoch(block_height: BlockHeight) -> u64 {
Expand Down Expand Up @@ -161,6 +164,8 @@ pub struct StakeData {
pub reward: u64,
/// The signature counter to prevent replay.
pub counter: u64,
/// Faults
pub faults: u8,
}

impl StakeData {
Expand Down Expand Up @@ -193,6 +198,7 @@ impl StakeData {
amount,
reward,
counter: 0,
faults: 0,
}
}

Expand Down
1 change: 1 addition & 0 deletions rusk-recovery/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ fn generate_stake_state(
amount,
reward: staker.reward.unwrap_or_default(),
counter: 0,
faults: 0,
};
session
.call::<_, ()>(
Expand Down
Loading

0 comments on commit 4d1f725

Please sign in to comment.