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

stake-contract: use StakeConfig for minimum_stake and slash warning #3233

Merged
merged 12 commits into from
Dec 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions consensus/src/user/provisioners.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use std::collections::BTreeMap;
use std::mem;

use dusk_core::dusk;
use dusk_core::stake::MINIMUM_STAKE;
use dusk_core::stake::DEFAULT_MINIMUM_STAKE;
use node_data::bls::{PublicKey, PublicKeyBytes};
use node_data::ledger::Seed;
use node_data::StepName;
Expand Down Expand Up @@ -182,7 +182,7 @@ impl Provisioners {
round: u64,
) -> impl Iterator<Item = (&PublicKey, &Stake)> {
self.members.iter().filter(move |(_, m)| {
m.is_eligible(round) && m.value() >= MINIMUM_STAKE
m.is_eligible(round) && m.value() >= DEFAULT_MINIMUM_STAKE
})
}

Expand Down
13 changes: 13 additions & 0 deletions contracts/stake/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,11 @@ unsafe fn get_version(arg_len: u32) -> u32 {
rusk_abi::wrap_call(arg_len, |_: ()| STATE.get_version())
}

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

// "Feeder" queries

#[no_mangle]
Expand All @@ -116,6 +121,14 @@ unsafe fn before_state_transition(arg_len: u32) -> u32 {
})
}

#[no_mangle]
unsafe fn set_config(arg_len: u32) -> u32 {
rusk_abi::wrap_call(arg_len, |config| {
assert_external_caller();
STATE.configure(config)
})
}

#[no_mangle]
unsafe fn insert_stake(arg_len: u32) -> u32 {
rusk_abi::wrap_call(arg_len, |(pk, stake_data)| {
Expand Down
47 changes: 28 additions & 19 deletions contracts/stake/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,19 @@

use alloc::collections::BTreeMap;
use alloc::vec::Vec;

use core::cmp::min;

use dusk_bytes::Serializable;

use dusk_core::{
signatures::bls::PublicKey as BlsPublicKey,
stake::{
next_epoch, Reward, SlashEvent, Stake, StakeAmount, StakeData,
StakeEvent, StakeFundOwner, StakeKeys, Withdraw, WithdrawToContract,
EPOCH, MINIMUM_STAKE, STAKE_CONTRACT, STAKE_WARNINGS,
},
transfer::{ContractToContract, ReceiveFromContract, TRANSFER_CONTRACT},
ContractId,
use dusk_core::signatures::bls::PublicKey as BlsPublicKey;
use dusk_core::stake::{
next_epoch, Reward, SlashEvent, Stake, StakeAmount, StakeConfig, StakeData,
StakeEvent, StakeFundOwner, StakeKeys, Withdraw, WithdrawToContract, EPOCH,
STAKE_CONTRACT,
};

use crate::*;
use dusk_core::transfer::{
ContractToContract, ReceiveFromContract, TRANSFER_CONTRACT,
};
use dusk_core::ContractId;

/// Contract keeping track of each public key's stake.
///
Expand All @@ -34,23 +30,33 @@ use crate::*;
/// valid stake.
#[derive(Debug, Default, Clone)]
pub struct StakeState {
stakes: BTreeMap<[u8; BlsPublicKey::SIZE], (StakeData, StakeKeys)>,
burnt_amount: u64,
config: StakeConfig,
previous_block_state:
BTreeMap<[u8; BlsPublicKey::SIZE], (Option<StakeData>, BlsPublicKey)>,
stakes: BTreeMap<[u8; BlsPublicKey::SIZE], (StakeData, StakeKeys)>,
}

const STAKE_CONTRACT_VERSION: u64 = 8;

impl StakeState {
pub const fn new() -> Self {
Self {
stakes: BTreeMap::new(),
burnt_amount: 0u64,
config: StakeConfig::new(),
previous_block_state: BTreeMap::new(),
stakes: BTreeMap::new(),
}
}

pub fn config(&self) -> &StakeConfig {
&self.config
}

pub fn configure(&mut self, config: StakeConfig) {
self.config = config;
}

pub fn on_new_block(&mut self) {
self.previous_block_state.clear()
}
Expand Down Expand Up @@ -80,6 +86,7 @@ impl StakeState {
}

pub fn stake(&mut self, stake: Stake) {
let minimum_stake = self.config.minimum_stake;
let value = stake.value();
let signature = *stake.signature();

Expand All @@ -91,7 +98,7 @@ impl StakeState {
let prev_stake = self.get_stake(&stake.keys().account).copied();
let (loaded_stake, keys) = self.load_or_create_stake_mut(stake.keys());

if loaded_stake.amount.is_none() && value < MINIMUM_STAKE {
if loaded_stake.amount.is_none() && value < minimum_stake {
herr-seppia marked this conversation as resolved.
Show resolved Hide resolved
panic!("The staked value is lower than the minimum amount!");
}

Expand Down Expand Up @@ -143,6 +150,7 @@ impl StakeState {
let stake: Stake =
rkyv::from_bytes(&recv.data).expect("Invalid stake received");
let value = stake.value();
let minimum_stake = self.config.minimum_stake;

if stake.chain_id() != self.chain_id() {
panic!("The stake must target the correct chain");
Expand All @@ -157,7 +165,7 @@ impl StakeState {
assert!(value == recv.value, "Stake amount mismatch");

if loaded_stake.amount.is_none() {
if value < MINIMUM_STAKE {
if value < minimum_stake {
panic!("The staked value is lower than the minimum amount!");
}
herr-seppia marked this conversation as resolved.
Show resolved Hide resolved

Expand Down Expand Up @@ -254,7 +262,7 @@ impl StakeState {
if loaded_stake.reward == 0 {
self.stakes.remove(&unstake.account().to_bytes());
}
} else if stake.total_funds() < MINIMUM_STAKE {
} else if stake.total_funds() < self.config.minimum_stake {
panic!("Stake left is lower than minimum stake");
}

Expand Down Expand Up @@ -513,6 +521,7 @@ impl StakeState {
/// depleted and the provisioner eligibility is shifted to the
/// next epoch as well
pub fn slash(&mut self, account: &BlsPublicKey, to_slash: Option<u64>) {
let stake_warnings = self.config.warnings;
let (stake, _) = self
.get_stake_mut(account)
.expect("The stake to slash should exist");
Expand All @@ -525,7 +534,7 @@ impl StakeState {

stake.faults = stake.faults.saturating_add(1);
let effective_faults =
stake.faults.saturating_sub(STAKE_WARNINGS) as u64;
stake.faults.saturating_sub(stake_warnings) as u64;

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

Expand Down
44 changes: 39 additions & 5 deletions core/src/stake.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,45 @@ pub const STAKE_CONTRACT: ContractId = crate::reserved(0x2);
/// Epoch used for stake operations
pub const EPOCH: u64 = 2160;

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

/// The minimum amount of Dusk one can stake.
#[deprecated(
since = "0.1.0",
note = "please use `DEFAULT_MINIMUM_STAKE` instead"
)]
pub const MINIMUM_STAKE: Dusk = DEFAULT_MINIMUM_STAKE;

/// The default minimum amount of Dusk one can stake.
pub const DEFAULT_MINIMUM_STAKE: Dusk = dusk(1_000.0);

/// Configuration for the stake contract
#[derive(Debug, Clone, Archive, Serialize, Deserialize)]
#[archive_attr(derive(CheckBytes))]
pub struct StakeConfig {
/// Number of warnings before being penalized
pub warnings: u8,
/// Minimum amount of Dusk that can be staked
pub minimum_stake: Dusk,
}

impl StakeConfig {
/// Create a new default stake configuration.
#[must_use]
pub const fn new() -> Self {
Self {
warnings: DEFAULT_STAKE_WARNINGS,
minimum_stake: DEFAULT_MINIMUM_STAKE,
}
}
}

impl Default for StakeConfig {
fn default() -> Self {
Self::new()
}
}

/// Calculate the block height at which the next epoch takes effect.
#[must_use]
Expand Down Expand Up @@ -374,9 +411,6 @@ pub struct SlashEvent {
pub next_eligibility: u64,
}

/// The minimum amount of Dusk one can stake.
pub const MINIMUM_STAKE: Dusk = dusk(1_000.0);

/// The representation of a public key's stake.
///
/// A user can stake for a particular `amount` larger in value than the
Expand Down
5 changes: 3 additions & 2 deletions rusk-recovery/src/state/snapshot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ mod tests {

use std::error::Error;

use dusk_core::stake::MINIMUM_STAKE;
use dusk_core::stake::DEFAULT_MINIMUM_STAKE;

use super::*;
use crate::state;
Expand Down Expand Up @@ -133,7 +133,8 @@ mod tests {
}

if !testnet.stakes().any(|s| {
s.amount >= MINIMUM_STAKE && s.eligibility.unwrap_or_default() == 0
s.amount >= DEFAULT_MINIMUM_STAKE
&& s.eligibility.unwrap_or_default() == 0
}) {
panic!("Testnet must have at least a provisioner configured");
}
Expand Down
4 changes: 2 additions & 2 deletions rusk-wallet/src/bin/io/prompt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use crossterm::{

use anyhow::Result;
use bip39::{ErrorKind, Language, Mnemonic};
use dusk_core::stake::MINIMUM_STAKE;
use dusk_core::stake::DEFAULT_MINIMUM_STAKE;

use inquire::ui::RenderConfig;
use inquire::validator::Validation;
Expand Down Expand Up @@ -267,7 +267,7 @@ pub(crate) fn request_optional_token_amt(

/// Request amount of tokens that can't be lower than MINIMUM_STAKE
pub(crate) fn request_stake_token_amt(balance: Dusk) -> Result<Dusk, Error> {
let min: Dusk = MINIMUM_STAKE.into();
let min: Dusk = DEFAULT_MINIMUM_STAKE.into();

request_token("stake", min, balance, None).map_err(Error::from)
}
Expand Down
27 changes: 15 additions & 12 deletions rusk/tests/services/contract_stake.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
use std::path::Path;
use std::sync::{Arc, RwLock};

use dusk_core::stake::{self, Stake, EPOCH, MINIMUM_STAKE};
use dusk_core::stake::{self, Stake, DEFAULT_MINIMUM_STAKE, EPOCH};

use dusk_bytes::Serializable;
use dusk_core::transfer::data::ContractCall;
Expand Down Expand Up @@ -72,7 +72,7 @@ pub async fn stake_from_contract() -> Result<()> {
let stake = Stake::new_from_contract(
&sk,
contract_id,
MINIMUM_STAKE,
DEFAULT_MINIMUM_STAKE,
rusk.chain_id().unwrap(),
);
let call = ContractCall::new(contract_id, "stake", &stake)
Expand All @@ -81,7 +81,7 @@ pub async fn stake_from_contract() -> Result<()> {
.moonlight_execute(
0,
0,
MINIMUM_STAKE,
DEFAULT_MINIMUM_STAKE,
GAS_LIMIT,
GAS_PRICE,
Some(call.clone()),
Expand All @@ -92,15 +92,15 @@ pub async fn stake_from_contract() -> Result<()> {

let stake = wallet.get_stake(0).expect("stake to be found");
assert_eq!(
MINIMUM_STAKE,
DEFAULT_MINIMUM_STAKE,
stake.amount.expect("stake amount to be found").value
);

let stake_from_contract = wallet
.moonlight_execute(
0,
0,
MINIMUM_STAKE,
DEFAULT_MINIMUM_STAKE,
GAS_LIMIT,
GAS_PRICE,
Some(call.clone()),
Expand All @@ -111,15 +111,15 @@ pub async fn stake_from_contract() -> Result<()> {

let stake = wallet.get_stake(0).expect("stake to be found");
assert_eq!(
MINIMUM_STAKE * 2,
DEFAULT_MINIMUM_STAKE * 2,
stake.amount.expect("stake amount to be found").value
);

let stake_from_contract = wallet
.moonlight_execute(
0,
0,
MINIMUM_STAKE,
DEFAULT_MINIMUM_STAKE,
GAS_LIMIT,
GAS_PRICE,
Some(call),
Expand All @@ -136,12 +136,12 @@ pub async fn stake_from_contract() -> Result<()> {

let stake = wallet.get_stake(0).expect("stake to be found");
assert_eq!(
MINIMUM_STAKE * 2 + (MINIMUM_STAKE / 10 * 9) as u64,
DEFAULT_MINIMUM_STAKE * 2 + (DEFAULT_MINIMUM_STAKE / 10 * 9) as u64,
stake.amount.expect("stake amount to be found").value
);

assert_eq!(
(MINIMUM_STAKE / 10) as u64,
(DEFAULT_MINIMUM_STAKE / 10) as u64,
stake.amount.expect("stake amount to be found").locked
);

Expand Down Expand Up @@ -175,7 +175,7 @@ pub async fn stake_from_contract() -> Result<()> {
&mut rng,
&sk,
contract_id,
3 * MINIMUM_STAKE,
3 * DEFAULT_MINIMUM_STAKE,
transfer::withdraw::WithdrawReceiver::Moonlight(pk),
transfer::withdraw::WithdrawReplayToken::Moonlight(7),
),
Expand All @@ -189,7 +189,7 @@ pub async fn stake_from_contract() -> Result<()> {
.amount
.unwrap()
.total_funds(),
3 * MINIMUM_STAKE
3 * DEFAULT_MINIMUM_STAKE
);
let call = ContractCall::new(contract_id, "unstake", &unstake)
.expect("call to be successful");
Expand All @@ -202,7 +202,10 @@ pub async fn stake_from_contract() -> Result<()> {
assert_eq!(wallet.get_stake(0).expect("stake to exists").amount, None);
let new_balance = wallet.get_account(0).unwrap().balance;
let fee_paid = tx.gas_spent * GAS_PRICE;
assert_eq!(new_balance, prev_balance + 3 * MINIMUM_STAKE - fee_paid);
assert_eq!(
new_balance,
prev_balance + 3 * DEFAULT_MINIMUM_STAKE - fee_paid
);

let current_reward = wallet.get_stake(0).expect("Stake to exists").reward;

Expand Down
4 changes: 2 additions & 2 deletions rusk/tests/services/moonlight_stake.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
use std::path::Path;
use std::sync::{Arc, RwLock};

use dusk_core::stake::MINIMUM_STAKE;
use dusk_core::stake::DEFAULT_MINIMUM_STAKE;

use rand::prelude::*;
use rand::rngs::StdRng;
Expand Down Expand Up @@ -149,7 +149,7 @@ pub async fn stake() -> Result<()> {
info!("Original Root: {:?}", hex::encode(original_root));

// Perform some staking actions.
wallet_stake(&rusk, &wallet, MINIMUM_STAKE);
wallet_stake(&rusk, &wallet, DEFAULT_MINIMUM_STAKE);

// Check the state's root is changed from the original one
let new_root = rusk.state_root();
Expand Down
Loading
Loading