Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

consensus: change soft-slash mechanism #1896

Merged
merged 10 commits into from
Jul 2, 2024
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() {
fed-franz marked this conversation as resolved.
Show resolved Hide resolved
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);
herr-seppia marked this conversation as resolved.
Show resolved Hide resolved
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
Loading