Skip to content

Commit

Permalink
feat(validations): set a maximum stake amount per validator and stake…
Browse files Browse the repository at this point in the history
… transaction
  • Loading branch information
drcpu-github committed Oct 30, 2024
1 parent f16c2b8 commit b2efe31
Show file tree
Hide file tree
Showing 6 changed files with 168 additions and 1 deletion.
3 changes: 3 additions & 0 deletions config/src/defaults.rs
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,9 @@ pub const PSEUDO_CONSENSUS_CONSTANTS_WIP0027_COLLATERAL_AGE: u32 = 13440;
/// Maximum weight units that a block can devote to `StakeTransaction`s.
pub const PSEUDO_CONSENSUS_CONSTANTS_POS_MAX_STAKE_BLOCK_WEIGHT: u32 = 10_000_000;

/// Maximum amount of nanoWits that a `StakeTransaction` can add (and can be staked on a single validator).
pub const PSEUDO_CONSENSUS_CONSTANTS_POS_MAX_STAKE_NANOWITS: u64 = 10_000_000_000_000_000;

/// Minimum amount of nanoWits that a `StakeTransaction` can add, and minimum amount that can be
/// left in stake by an `UnstakeTransaction`.
pub const PSEUDO_CONSENSUS_CONSTANTS_POS_MIN_STAKE_NANOWITS: u64 = 10_000_000_000_000;
Expand Down
13 changes: 13 additions & 0 deletions data_structures/src/chain/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3013,6 +3013,19 @@ impl TransactionsPool {
})
}

/// Remove stake transactions that would result in overstaking on a validator
pub fn remove_overstake_transactions(&mut self, transactions: Vec<Hash>) {
for st_tx_hash in transactions.iter() {
if let Some(st_tx) = self
.st_transactions
.get(st_tx_hash)
.map(|(_, st)| st.clone())
{
self.st_remove(&st_tx);
}
}
}

/// Remove an unstake transaction from the pool.
///
/// This should be used to remove transactions that got included in a consolidated block.
Expand Down
6 changes: 6 additions & 0 deletions data_structures/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,12 @@ pub enum TransactionError {
stake, min_stake
)]
StakeBelowMinimum { min_stake: u64, stake: u64 },
/// Stake amount above maximum
#[fail(
display = "The amount of coins in stake ({}) is more than the maximum allowed ({})",
stake, max_stake
)]
StakeAboveMaximum { max_stake: u64, stake: u64 },
/// Unstaking more than the total staked
#[fail(
display = "Tried to unstake more coins than the current stake ({} > {})",
Expand Down
45 changes: 44 additions & 1 deletion node/src/actors/chain_manager/mining.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ use futures::future::{try_join_all, FutureExt};

use witnet_config::defaults::{
PSEUDO_CONSENSUS_CONSTANTS_POS_MAX_STAKE_BLOCK_WEIGHT,
PSEUDO_CONSENSUS_CONSTANTS_POS_MAX_STAKE_NANOWITS,
PSEUDO_CONSENSUS_CONSTANTS_POS_MIN_STAKE_NANOWITS,
PSEUDO_CONSENSUS_CONSTANTS_WIP0027_COLLATERAL_AGE,
};
Expand All @@ -37,7 +38,11 @@ use witnet_data_structures::{
proto::versioning::{ProtocolVersion, ProtocolVersion::*, VersionedHashable},
radon_error::RadonError,
radon_report::{RadonReport, ReportContext, TypeLike},
staking::{stake::totalize_stakes, stakes::QueryStakesKey},
staking::{
helpers::StakeKey,
stake::totalize_stakes,
stakes::{QueryStakesKey, StakesTracker},
},
transaction::{
CommitTransaction, CommitTransactionBody, DRTransactionBody, MintTransaction,
RevealTransaction, RevealTransactionBody, StakeTransactionBody, TallyTransaction,
Expand Down Expand Up @@ -77,6 +82,8 @@ use crate::{
signature_mngr,
};

const MAX_STAKE_NANOWITS: u64 = PSEUDO_CONSENSUS_CONSTANTS_POS_MAX_STAKE_NANOWITS;

impl ChainManager {
/// Try to mine a block
pub fn try_mine_block(&mut self, ctx: &mut Context<Self>) -> Result<(), ChainManagerError> {
Expand Down Expand Up @@ -236,6 +243,7 @@ impl ChainManager {
tapi_version,
&active_wips,
Some(validator_count),
&act.chain_state.stakes,
);

// Sign the block hash
Expand Down Expand Up @@ -920,6 +928,7 @@ pub fn build_block(
tapi_signals: u32,
active_wips: &ActiveWips,
validator_count: Option<usize>,
stakes: &StakesTracker,
) -> (BlockHeader, BlockTransactions) {
let validator_count = validator_count.unwrap_or(DEFAULT_VALIDATOR_COUNT_FOR_TESTS);
let (transactions_pool, unspent_outputs_pool, dr_pool) = pools_ref;
Expand Down Expand Up @@ -1141,6 +1150,7 @@ pub fn build_block(
let protocol_version = ProtocolVersion::from_epoch(epoch);

if protocol_version > V1_7 {
let mut overstake_transactions = Vec::<Hash>::new();
let mut included_validators = HashSet::<PublicKeyHash>::new();
for st_tx in transactions_pool.st_iter() {
let validator_pkh = st_tx.body.output.authorization.public_key.pkh();
Expand All @@ -1152,6 +1162,37 @@ pub fn build_block(
continue;
}

// If a set of staking transactions is sent simultaneously to the transactions pool using a staking amount smaller
// than MAX_STAKE_NANOWITS they can all be accepted since they do not introduce overstaking yet. However, accepting
// all of them in subsequent blocks could violate the MAX_STAKE_NANOWITS rule. Thus we still need to check that we
// do not include all these staking transactions in a block so we do not produce an invalid block.
let stakes_key = QueryStakesKey::Key(StakeKey {
validator: st_tx.body.output.key.validator,
withdrawer: st_tx.body.output.key.withdrawer,
});
match stakes.query_stakes(stakes_key) {
Ok(stake_entry) => {
// TODO: modify this to enable delegated staking with multiple withdrawer addresses on a single validator
let staked_amount: u64 = stake_entry
.first()
.map(|stake| stake.value.coins)
.unwrap()
.into();
if st_tx.body.output.value + staked_amount > MAX_STAKE_NANOWITS {
overstake_transactions.push(st_tx.hash());
continue;
}
}
Err(_) => {
// This should never happen since a staking transaction to a non-existing (validator, withdrawer) pair
// with a value higher than MAX_STAKE_NANOWITS should not have been accepted in the transactions pool.
if st_tx.body.output.value > MAX_STAKE_NANOWITS {
overstake_transactions.push(st_tx.hash());
continue;
}
}
};

let transaction_weight = st_tx.weight();
let transaction_fee =
match st_transaction_fee(st_tx, &utxo_diff, epoch, epoch_constants) {
Expand Down Expand Up @@ -1188,6 +1229,8 @@ pub fn build_block(

included_validators.insert(validator_pkh);
}

transactions_pool.remove_overstake_transactions(overstake_transactions);
} else {
transactions_pool.clear_stake_transactions();
}
Expand Down
69 changes: 69 additions & 0 deletions validations/src/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use std::{
use itertools::Itertools;

use witnet_config::defaults::{
PSEUDO_CONSENSUS_CONSTANTS_POS_MAX_STAKE_NANOWITS,
PSEUDO_CONSENSUS_CONSTANTS_POS_MIN_STAKE_NANOWITS,
PSEUDO_CONSENSUS_CONSTANTS_POS_UNSTAKING_DELAY_SECONDS,
PSEUDO_CONSENSUS_CONSTANTS_WIP0022_REWARD_COLLATERAL_RATIO,
Expand Down Expand Up @@ -57,6 +58,7 @@ mod witnessing;
static ONE_WIT: u64 = 1_000_000_000;
const MAX_VT_WEIGHT: u32 = 20_000;
const MAX_DR_WEIGHT: u32 = 80_000;
const MAX_STAKE_NANOWITS: u64 = PSEUDO_CONSENSUS_CONSTANTS_POS_MAX_STAKE_NANOWITS;
const MIN_STAKE_NANOWITS: u64 = PSEUDO_CONSENSUS_CONSTANTS_POS_MIN_STAKE_NANOWITS;
const UNSTAKING_DELAY_SECONDS: u64 = PSEUDO_CONSENSUS_CONSTANTS_POS_UNSTAKING_DELAY_SECONDS;

Expand Down Expand Up @@ -9001,6 +9003,73 @@ fn st_below_min_stake() {
);
}

#[test]
fn st_above_max_stake() {
register_protocol_version(ProtocolVersion::V1_8, 10000, 10);

// Setup stakes tracker with a (validator, validator) pair
let (validator_pkh, withdrawer_pkh, stakes) =
setup_stakes_tracker(MAX_STAKE_NANOWITS, PRIV_KEY_1, PRIV_KEY_2);

let utxo_set = UnspentOutputsPool::default();
let block_number = 0;
let utxo_diff = UtxoDiff::new(&utxo_set, block_number);
let mut signatures_to_verify = vec![];
let vti = Input::new(
"2222222222222222222222222222222222222222222222222222222222222222:1"
.parse()
.unwrap(),
);

// The stake transaction will fail because its value is above MAX_STAKE_NANOWITS
let stake_output = StakeOutput {
value: MAX_STAKE_NANOWITS + 1,
..Default::default()
};
let stake_tx_body = StakeTransactionBody::new(vec![vti], stake_output, None);
let stake_tx = StakeTransaction::new(stake_tx_body, vec![]);
let x = validate_stake_transaction(
&stake_tx,
&utxo_diff,
Epoch::from(10000 as u32),
EpochConstants::default(),
&mut signatures_to_verify,
&stakes,
);
assert_eq!(
x.unwrap_err().downcast::<TransactionError>().unwrap(),
TransactionError::StakeAboveMaximum {
max_stake: MAX_STAKE_NANOWITS,
stake: MAX_STAKE_NANOWITS + 1,
}
);

// The stake transaction will fail because the sum of its value and the amount which is
// already staked is above MAX_STAKE_NANOWITS
let stake_output = StakeOutput {
value: MIN_STAKE_NANOWITS,
key: StakeKey::from((validator_pkh, withdrawer_pkh)),
..Default::default()
};
let stake_tx_body = StakeTransactionBody::new(vec![vti], stake_output, None);
let stake_tx = StakeTransaction::new(stake_tx_body, vec![]);
let x = validate_stake_transaction(
&stake_tx,
&utxo_diff,
Epoch::from(10000 as u32),
EpochConstants::default(),
&mut signatures_to_verify,
&stakes,
);
assert_eq!(
x.unwrap_err().downcast::<TransactionError>().unwrap(),
TransactionError::StakeAboveMaximum {
max_stake: MAX_STAKE_NANOWITS,
stake: MAX_STAKE_NANOWITS + MIN_STAKE_NANOWITS,
}
);
}

#[test]
fn unstake_success() {
// Setup stakes tracker with a (validator, validator) pair
Expand Down
33 changes: 33 additions & 0 deletions validations/src/validations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use std::{
use itertools::Itertools;

use witnet_config::defaults::{
PSEUDO_CONSENSUS_CONSTANTS_POS_MAX_STAKE_NANOWITS,
PSEUDO_CONSENSUS_CONSTANTS_POS_MIN_STAKE_NANOWITS,
PSEUDO_CONSENSUS_CONSTANTS_POS_UNSTAKING_DELAY_SECONDS,
PSEUDO_CONSENSUS_CONSTANTS_WIP0022_REWARD_COLLATERAL_RATIO,
Expand Down Expand Up @@ -68,6 +69,7 @@ use crate::eligibility::{

// TODO: move to a configuration
const MAX_STAKE_BLOCK_WEIGHT: u32 = 10_000_000;
const MAX_STAKE_NANOWITS: u64 = PSEUDO_CONSENSUS_CONSTANTS_POS_MAX_STAKE_NANOWITS;
const MIN_STAKE_NANOWITS: u64 = PSEUDO_CONSENSUS_CONSTANTS_POS_MIN_STAKE_NANOWITS;
const MAX_UNSTAKE_BLOCK_WEIGHT: u32 = 5_000;
const UNSTAKING_DELAY_SECONDS: u64 = PSEUDO_CONSENSUS_CONSTANTS_POS_UNSTAKING_DELAY_SECONDS;
Expand Down Expand Up @@ -1346,6 +1348,37 @@ pub fn validate_stake_transaction<'a>(
st_tx.body.output.key.withdrawer,
)?;

// Check that the amount of coins to stake plus the alread staked amount is equal or smaller than the maximum allowed
let stakes_key = QueryStakesKey::Key(StakeKey {
validator: st_tx.body.output.key.validator,
withdrawer: st_tx.body.output.key.withdrawer,
});
match stakes.query_stakes(stakes_key) {
Ok(stake_entry) => {
// TODO: modify this to enable delegated staking with multiple withdrawer addresses on a single validator
let staked_amount: u64 = stake_entry
.first()
.map(|stake| stake.value.coins)
.unwrap()
.into();
if staked_amount + st_tx.body.output.value > MAX_STAKE_NANOWITS {
Err(TransactionError::StakeAboveMaximum {
max_stake: MAX_STAKE_NANOWITS,
stake: staked_amount + st_tx.body.output.value,
})?;
}
}
Err(_) => {
// Check that the amount of coins to stake is equal or smaller than the maximum allowed
if st_tx.body.output.value > MAX_STAKE_NANOWITS {
Err(TransactionError::StakeAboveMaximum {
max_stake: MAX_STAKE_NANOWITS,
stake: st_tx.body.output.value,
})?;
}
}
};

validate_transaction_signature(
&st_tx.signatures,
&st_tx.body.inputs,
Expand Down

0 comments on commit b2efe31

Please sign in to comment.