Skip to content

Commit e56bcdb

Browse files
committed
feat(validations): set a maximum stake amount per validator and stake transaction
1 parent dcf71b0 commit e56bcdb

File tree

6 files changed

+173
-1
lines changed

6 files changed

+173
-1
lines changed

config/src/defaults.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,9 @@ pub const PSEUDO_CONSENSUS_CONSTANTS_WIP0027_COLLATERAL_AGE: u32 = 13440;
498498
/// Maximum weight units that a block can devote to `StakeTransaction`s.
499499
pub const PSEUDO_CONSENSUS_CONSTANTS_POS_MAX_STAKE_BLOCK_WEIGHT: u32 = 10_000_000;
500500

501+
/// Maximum amount of nanoWits that a `StakeTransaction` can add (and can be staked on a single validator).
502+
pub const PSEUDO_CONSENSUS_CONSTANTS_POS_MAX_STAKE_NANOWITS: u64 = 10_000_000_000_000_000;
503+
501504
/// Minimum amount of nanoWits that a `StakeTransaction` can add, and minimum amount that can be
502505
/// left in stake by an `UnstakeTransaction`.
503506
pub const PSEUDO_CONSENSUS_CONSTANTS_POS_MIN_STAKE_NANOWITS: u64 = 10_000_000_000_000;

data_structures/src/chain/mod.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3016,6 +3016,24 @@ impl TransactionsPool {
30163016
})
30173017
}
30183018

3019+
/// Remove stake transactions that would result in overstaking on a validator
3020+
pub fn remove_overstake_transactions(&mut self, transactions: Vec<Hash>) {
3021+
for st_tx_hash in transactions.iter() {
3022+
match self
3023+
.st_transactions
3024+
.get(st_tx_hash)
3025+
.map(|(_, st)| st.clone())
3026+
{
3027+
Some(st_tx) => {
3028+
self.st_remove(&st_tx);
3029+
3030+
()
3031+
}
3032+
None => (),
3033+
}
3034+
}
3035+
}
3036+
30193037
/// Remove an unstake transaction from the pool.
30203038
///
30213039
/// This should be used to remove transactions that got included in a consolidated block.

data_structures/src/error.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,12 @@ pub enum TransactionError {
301301
stake, min_stake
302302
)]
303303
StakeBelowMinimum { min_stake: u64, stake: u64 },
304+
/// Stake amount above maximum
305+
#[fail(
306+
display = "The amount of coins in stake ({}) is more than the maximum allowed ({})",
307+
stake, max_stake
308+
)]
309+
StakeAboveMaximum { max_stake: u64, stake: u64 },
304310
/// Unstaking more than the total staked
305311
#[fail(
306312
display = "Tried to unstake more coins than the current stake ({} > {})",

node/src/actors/chain_manager/mining.rs

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ use futures::future::{try_join_all, FutureExt};
1818

1919
use witnet_config::defaults::{
2020
PSEUDO_CONSENSUS_CONSTANTS_POS_MAX_STAKE_BLOCK_WEIGHT,
21+
PSEUDO_CONSENSUS_CONSTANTS_POS_MAX_STAKE_NANOWITS,
2122
PSEUDO_CONSENSUS_CONSTANTS_POS_MIN_STAKE_NANOWITS,
2223
PSEUDO_CONSENSUS_CONSTANTS_WIP0027_COLLATERAL_AGE,
2324
};
@@ -37,7 +38,11 @@ use witnet_data_structures::{
3738
proto::versioning::{ProtocolVersion, ProtocolVersion::*, VersionedHashable},
3839
radon_error::RadonError,
3940
radon_report::{RadonReport, ReportContext, TypeLike},
40-
staking::{stake::totalize_stakes, stakes::QueryStakesKey},
41+
staking::{
42+
helpers::StakeKey,
43+
stake::totalize_stakes,
44+
stakes::{QueryStakesKey, StakesTracker},
45+
},
4146
transaction::{
4247
CommitTransaction, CommitTransactionBody, DRTransactionBody, MintTransaction,
4348
RevealTransaction, RevealTransactionBody, StakeTransactionBody, TallyTransaction,
@@ -77,6 +82,8 @@ use crate::{
7782
signature_mngr,
7883
};
7984

85+
const MAX_STAKE_NANOWITS: u64 = PSEUDO_CONSENSUS_CONSTANTS_POS_MAX_STAKE_NANOWITS;
86+
8087
impl ChainManager {
8188
/// Try to mine a block
8289
pub fn try_mine_block(&mut self, ctx: &mut Context<Self>) -> Result<(), ChainManagerError> {
@@ -236,6 +243,7 @@ impl ChainManager {
236243
tapi_version,
237244
&active_wips,
238245
Some(validator_count),
246+
&act.chain_state.stakes,
239247
);
240248

241249
// Sign the block hash
@@ -920,6 +928,7 @@ pub fn build_block(
920928
tapi_signals: u32,
921929
active_wips: &ActiveWips,
922930
validator_count: Option<usize>,
931+
stakes: &StakesTracker,
923932
) -> (BlockHeader, BlockTransactions) {
924933
let validator_count = validator_count.unwrap_or(DEFAULT_VALIDATOR_COUNT_FOR_TESTS);
925934
let (transactions_pool, unspent_outputs_pool, dr_pool) = pools_ref;
@@ -1141,6 +1150,7 @@ pub fn build_block(
11411150
let protocol_version = ProtocolVersion::from_epoch(epoch);
11421151

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

1165+
// If a set of staking transactions is sent simultaneously to the transactions pool using a staking amount smaller
1166+
// than MAX_STAKE_NANOWITS they can all be accepted since they do not introduce overstaking yet. However, accepting
1167+
// all of them in subsequent blocks could violate the MAX_STAKE_NANOWITS rule. Thus we still need to check that we
1168+
// do not include all these staking transactions in a block so we do not produce an invalid block.
1169+
let stakes_key = QueryStakesKey::Key(StakeKey {
1170+
validator: st_tx.body.output.key.validator,
1171+
withdrawer: st_tx.body.output.key.withdrawer,
1172+
});
1173+
match stakes.query_stakes(stakes_key) {
1174+
Ok(stake_entry) => {
1175+
// TODO: modify this to enable delegated staking with multiple withdrawer addresses on a single validator
1176+
let staked_amount: u64 = stake_entry
1177+
.first()
1178+
.map(|stake| stake.value.coins)
1179+
.unwrap()
1180+
.into();
1181+
if st_tx.body.output.value + staked_amount > MAX_STAKE_NANOWITS {
1182+
overstake_transactions.push(st_tx.hash());
1183+
continue;
1184+
}
1185+
}
1186+
Err(_) => {
1187+
// This should never happen since a staking transaction to a non-existing (validator, withdrawer) pair
1188+
// with a value higher than MAX_STAKE_NANOWITS should not have been accepted in the transactions pool.
1189+
if st_tx.body.output.value > MAX_STAKE_NANOWITS {
1190+
overstake_transactions.push(st_tx.hash());
1191+
continue;
1192+
}
1193+
}
1194+
};
1195+
11551196
let transaction_weight = st_tx.weight();
11561197
let transaction_fee =
11571198
match st_transaction_fee(st_tx, &utxo_diff, epoch, epoch_constants) {
@@ -1188,6 +1229,8 @@ pub fn build_block(
11881229

11891230
included_validators.insert(validator_pkh);
11901231
}
1232+
1233+
transactions_pool.remove_overstake_transactions(overstake_transactions);
11911234
} else {
11921235
transactions_pool.clear_stake_transactions();
11931236
}

validations/src/tests/mod.rs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use std::{
77
use itertools::Itertools;
88

99
use witnet_config::defaults::{
10+
PSEUDO_CONSENSUS_CONSTANTS_POS_MAX_STAKE_NANOWITS,
1011
PSEUDO_CONSENSUS_CONSTANTS_POS_MIN_STAKE_NANOWITS,
1112
PSEUDO_CONSENSUS_CONSTANTS_POS_UNSTAKING_DELAY_SECONDS,
1213
PSEUDO_CONSENSUS_CONSTANTS_WIP0022_REWARD_COLLATERAL_RATIO,
@@ -57,6 +58,7 @@ mod witnessing;
5758
static ONE_WIT: u64 = 1_000_000_000;
5859
const MAX_VT_WEIGHT: u32 = 20_000;
5960
const MAX_DR_WEIGHT: u32 = 80_000;
61+
const MAX_STAKE_NANOWITS: u64 = PSEUDO_CONSENSUS_CONSTANTS_POS_MAX_STAKE_NANOWITS;
6062
const MIN_STAKE_NANOWITS: u64 = PSEUDO_CONSENSUS_CONSTANTS_POS_MIN_STAKE_NANOWITS;
6163
const UNSTAKING_DELAY_SECONDS: u64 = PSEUDO_CONSENSUS_CONSTANTS_POS_UNSTAKING_DELAY_SECONDS;
6264

@@ -9000,6 +9002,73 @@ fn st_below_min_stake() {
90009002
);
90019003
}
90029004

9005+
#[test]
9006+
fn st_above_max_stake() {
9007+
register_protocol_version(ProtocolVersion::V1_8, 10000, 10);
9008+
9009+
// Setup stakes tracker with a (validator, validator) pair
9010+
let (validator_pkh, withdrawer_pkh, stakes) =
9011+
setup_stakes_tracker(MAX_STAKE_NANOWITS, PRIV_KEY_1, PRIV_KEY_2);
9012+
9013+
let utxo_set = UnspentOutputsPool::default();
9014+
let block_number = 0;
9015+
let utxo_diff = UtxoDiff::new(&utxo_set, block_number);
9016+
let mut signatures_to_verify = vec![];
9017+
let vti = Input::new(
9018+
"2222222222222222222222222222222222222222222222222222222222222222:1"
9019+
.parse()
9020+
.unwrap(),
9021+
);
9022+
9023+
// The stake transaction will fail because its value is above MAX_STAKE_NANOWITS
9024+
let stake_output = StakeOutput {
9025+
value: MAX_STAKE_NANOWITS + 1,
9026+
..Default::default()
9027+
};
9028+
let stake_tx_body = StakeTransactionBody::new(vec![vti], stake_output, None);
9029+
let stake_tx = StakeTransaction::new(stake_tx_body, vec![]);
9030+
let x = validate_stake_transaction(
9031+
&stake_tx,
9032+
&utxo_diff,
9033+
Epoch::from(10000 as u32),
9034+
EpochConstants::default(),
9035+
&mut signatures_to_verify,
9036+
&stakes,
9037+
);
9038+
assert_eq!(
9039+
x.unwrap_err().downcast::<TransactionError>().unwrap(),
9040+
TransactionError::StakeAboveMaximum {
9041+
max_stake: MAX_STAKE_NANOWITS,
9042+
stake: MAX_STAKE_NANOWITS + 1,
9043+
}
9044+
);
9045+
9046+
// The stake transaction will fail because the sum of its value and the amount which is
9047+
// already staked is above MAX_STAKE_NANOWITS
9048+
let stake_output = StakeOutput {
9049+
value: MIN_STAKE_NANOWITS,
9050+
key: StakeKey::from((validator_pkh, withdrawer_pkh)),
9051+
..Default::default()
9052+
};
9053+
let stake_tx_body = StakeTransactionBody::new(vec![vti], stake_output, None);
9054+
let stake_tx = StakeTransaction::new(stake_tx_body, vec![]);
9055+
let x = validate_stake_transaction(
9056+
&stake_tx,
9057+
&utxo_diff,
9058+
Epoch::from(10000 as u32),
9059+
EpochConstants::default(),
9060+
&mut signatures_to_verify,
9061+
&stakes,
9062+
);
9063+
assert_eq!(
9064+
x.unwrap_err().downcast::<TransactionError>().unwrap(),
9065+
TransactionError::StakeAboveMaximum {
9066+
max_stake: MAX_STAKE_NANOWITS,
9067+
stake: MAX_STAKE_NANOWITS + MIN_STAKE_NANOWITS,
9068+
}
9069+
);
9070+
}
9071+
90039072
#[test]
90049073
fn unstake_success() {
90059074
// Setup stakes tracker with a (validator, validator) pair

validations/src/validations.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use std::{
88
use itertools::Itertools;
99

1010
use witnet_config::defaults::{
11+
PSEUDO_CONSENSUS_CONSTANTS_POS_MAX_STAKE_NANOWITS,
1112
PSEUDO_CONSENSUS_CONSTANTS_POS_MIN_STAKE_NANOWITS,
1213
PSEUDO_CONSENSUS_CONSTANTS_POS_UNSTAKING_DELAY_SECONDS,
1314
PSEUDO_CONSENSUS_CONSTANTS_WIP0022_REWARD_COLLATERAL_RATIO,
@@ -68,6 +69,7 @@ use crate::eligibility::{
6869

6970
// TODO: move to a configuration
7071
const MAX_STAKE_BLOCK_WEIGHT: u32 = 10_000_000;
72+
const MAX_STAKE_NANOWITS: u64 = PSEUDO_CONSENSUS_CONSTANTS_POS_MAX_STAKE_NANOWITS;
7173
const MIN_STAKE_NANOWITS: u64 = PSEUDO_CONSENSUS_CONSTANTS_POS_MIN_STAKE_NANOWITS;
7274
const MAX_UNSTAKE_BLOCK_WEIGHT: u32 = 5_000;
7375
const UNSTAKING_DELAY_SECONDS: u64 = PSEUDO_CONSENSUS_CONSTANTS_POS_UNSTAKING_DELAY_SECONDS;
@@ -1346,6 +1348,37 @@ pub fn validate_stake_transaction<'a>(
13461348
st_tx.body.output.key.withdrawer,
13471349
)?;
13481350

1351+
// Check that the amount of coins to stake plus the alread staked amount is equal or smaller than the maximum allowed
1352+
let stakes_key = QueryStakesKey::Key(StakeKey {
1353+
validator: st_tx.body.output.key.validator,
1354+
withdrawer: st_tx.body.output.key.withdrawer,
1355+
});
1356+
match stakes.query_stakes(stakes_key) {
1357+
Ok(stake_entry) => {
1358+
// TODO: modify this to enable delegated staking with multiple withdrawer addresses on a single validator
1359+
let staked_amount: u64 = stake_entry
1360+
.first()
1361+
.map(|stake| stake.value.coins)
1362+
.unwrap()
1363+
.into();
1364+
if staked_amount + st_tx.body.output.value > MAX_STAKE_NANOWITS {
1365+
Err(TransactionError::StakeAboveMaximum {
1366+
max_stake: MAX_STAKE_NANOWITS,
1367+
stake: staked_amount + st_tx.body.output.value,
1368+
})?;
1369+
}
1370+
}
1371+
Err(_) => {
1372+
// Check that the amount of coins to stake is equal or smaller than the maximum allowed
1373+
if st_tx.body.output.value > MAX_STAKE_NANOWITS {
1374+
Err(TransactionError::StakeAboveMaximum {
1375+
max_stake: MAX_STAKE_NANOWITS,
1376+
stake: st_tx.body.output.value,
1377+
})?;
1378+
}
1379+
}
1380+
};
1381+
13491382
validate_transaction_signature(
13501383
&st_tx.signatures,
13511384
&st_tx.body.inputs,

0 commit comments

Comments
 (0)