Skip to content

Commit 056e6be

Browse files
committed
New stake policy and tests
1 parent 133ee14 commit 056e6be

10 files changed

Lines changed: 793 additions & 38 deletions

File tree

primitives/account/src/account/staking_contract/receipts.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,22 @@ pub struct DeleteValidatorReceipt {
111111
}
112112
convert_receipt!(DeleteValidatorReceipt);
113113

114+
#[derive(Debug, PartialEq, Eq, Clone, Hash, Serialize, Deserialize)]
115+
pub enum BalanceType {
116+
Active,
117+
Inactive,
118+
Retired,
119+
}
120+
121+
/// Receipt for most staker-related transactions. This is necessary to be able to revert
122+
/// these transactions.
123+
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
124+
pub struct AddStakeReceipt {
125+
/// the balance which the stake was attributed to.
126+
pub credited_balance: BalanceType,
127+
}
128+
convert_receipt!(AddStakeReceipt);
129+
114130
/// Receipt for most staker-related transactions. This is necessary to be able to revert
115131
/// these transactions.
116132
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]

primitives/account/src/account/staking_contract/staker.rs

Lines changed: 126 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ use nimiq_primitives::coin::Coin;
99
use nimiq_primitives::policy::Policy;
1010
use serde::{Deserialize, Serialize};
1111

12+
use super::AddStakeReceipt;
13+
use crate::BalanceType;
1214
#[cfg(feature = "interaction-traits")]
1315
use crate::{
1416
account::staking_contract::{
@@ -146,6 +148,7 @@ impl Staker {
146148
/// Invariants:
147149
/// (1) active + inactive balances must be == 0 or >= minimum stake
148150
/// (2) active + inactive + retired balances must be == 0 or >= minimum stake
151+
/// IMPORTANT: This code is shared between new and legacy add stake versions.
149152
pub(crate) fn enforce_min_stake(
150153
active_balance: Coin,
151154
inactive_balance: Coin,
@@ -283,68 +286,118 @@ impl StakingContract {
283286
store: &mut StakingContractStoreWrite,
284287
staker_address: &Address,
285288
value: Coin,
289+
protocol_version: u16,
286290
tx_logger: &mut TransactionLog,
287-
) -> Result<(), AccountError> {
291+
) -> Result<AddStakeReceipt, AccountError> {
292+
if protocol_version < Policy::ADD_STAKE_PROTOCOL_UPGRADE_VERSION {
293+
log::warn!(%protocol_version, "Adding stake using old protocol version");
294+
return self.legacy_add_stake_0v(store, staker_address, value, tx_logger);
295+
}
288296
// Get the staker.
289297
let mut staker = store.expect_staker(staker_address)?;
290298

291-
// Fail if the minimum stake would be violated for the non-retired funds (invariant 1).
292-
Staker::enforce_min_stake(
293-
staker.active_balance + value,
294-
staker.inactive_balance,
295-
staker.retired_balance,
296-
)?;
297-
298-
// All checks passed, not allowed to fail from here on!
299-
300-
// If we are delegating to a validator, we need to update it.
299+
// Check that the delegation is still valid, i.e. the validator hasn't been deleted.
301300
if let Some(validator_address) = &staker.delegation {
302-
// Check that the delegation is still valid, i.e. the validator hasn't been deleted.
303301
store.expect_validator(validator_address)?;
304-
self.increase_stake_to_validator(store, validator_address, value);
305302
}
306303

304+
// Add stake txs never violate minimum stake for the non-retired funds (invariant 1),
305+
// because the intrinsic tx checks that value is >= min stake.
306+
assert!(
307+
Staker::enforce_min_stake(
308+
staker.active_balance + value,
309+
staker.inactive_balance,
310+
staker.retired_balance,
311+
)
312+
.is_ok(),
313+
"Add stake should never violate the min stake invariants"
314+
);
315+
316+
// All checks passed, not allowed to fail from here on!
317+
307318
// Update the staker's and staking contract's balances.
308-
staker.active_balance += value;
319+
// We want to preferentially credit the active balance. Only if there is no active balance,
320+
// then we will choose the balance with the most funds between inactive and retired.
321+
let credited_balance = if !staker.active_balance.is_zero() {
322+
staker.active_balance += value;
323+
BalanceType::Active
324+
} else {
325+
staker.inactive_balance += value;
326+
if staker.inactive_from.is_none() {
327+
staker.inactive_from = Some(0);
328+
}
329+
BalanceType::Inactive
330+
};
309331
self.balance += value;
310332

333+
// Create the receipt.
334+
let receipt = AddStakeReceipt {
335+
credited_balance: credited_balance.clone(),
336+
};
337+
338+
// If we are actively delegating to a validator, we need to update it.
339+
if credited_balance == BalanceType::Active {
340+
if let Some(validator_address) = &staker.delegation {
341+
self.increase_stake_to_validator(store, validator_address, value);
342+
}
343+
}
344+
311345
// Build the return logs
312346
tx_logger.push_log(Log::Stake {
313347
staker_address: staker_address.clone(),
314348
validator_address: staker.delegation.clone(),
315349
value,
350+
credited_balance,
316351
});
317352

318353
// Update the staker entry.
319354
store.put_staker(staker_address, staker);
320355

321-
Ok(())
356+
Ok(receipt)
322357
}
323358

324359
/// Reverts a stake transaction.
360+
/// IMPORTANT: This code is shared between new and legacy add stake versions.
325361
pub fn revert_add_stake(
326362
&mut self,
327363
store: &mut StakingContractStoreWrite,
328364
staker_address: &Address,
329365
value: Coin,
366+
receipt: AddStakeReceipt,
330367
tx_logger: &mut TransactionLog,
331368
) -> Result<(), AccountError> {
332369
// Get the staker.
333370
let mut staker = store.expect_staker(staker_address)?;
334371

335372
// If we are delegating to a validator, we need to update it too.
336-
if let Some(validator_address) = &staker.delegation {
337-
self.decrease_stake_from_validator(store, validator_address, value);
373+
if receipt.credited_balance == BalanceType::Active {
374+
if let Some(validator_address) = &staker.delegation {
375+
self.decrease_stake_from_validator(store, validator_address, value);
376+
}
338377
}
339378

340379
// Update the staker's and staking contract's balances.
341-
staker.active_balance -= value;
380+
match receipt.credited_balance {
381+
BalanceType::Active => {
382+
staker.active_balance -= value;
383+
}
384+
BalanceType::Inactive => {
385+
staker.inactive_balance -= value;
386+
if staker.inactive_balance.is_zero() {
387+
staker.inactive_from = None;
388+
}
389+
}
390+
BalanceType::Retired => {
391+
staker.retired_balance -= value;
392+
}
393+
}
342394
self.balance -= value;
343395

344396
tx_logger.push_log(Log::Stake {
345397
staker_address: staker_address.clone(),
346398
validator_address: staker.delegation.clone(),
347399
value,
400+
credited_balance: receipt.credited_balance,
348401
});
349402

350403
// Update the staker entry.
@@ -949,6 +1002,7 @@ impl StakingContract {
9491002
}
9501003

9511004
/// Adds `value` coins to a given validator's total stake.
1005+
/// IMPORTANT: This code is shared between new and legacy add stake versions.
9521006
fn increase_stake_to_validator(
9531007
&mut self,
9541008
store: &mut StakingContractStoreWrite,
@@ -982,6 +1036,7 @@ impl StakingContract {
9821036
}
9831037

9841038
/// Removes `value` coins from a given validator's inactive total stake.
1039+
/// IMPORTANT: This code is shared between new and legacy add stake versions.
9851040
fn decrease_stake_from_validator(
9861041
&mut self,
9871042
store: &mut StakingContractStoreWrite,
@@ -1015,4 +1070,57 @@ impl StakingContract {
10151070
// Neither validator nor tombstone exist, this is an error.
10161071
panic!("inconsistent contract state");
10171072
}
1073+
1074+
// Pre protocol upgrade.
1075+
// IMPORTANT: DO NOT REMOVE THIS CODE!
1076+
// It is needed for history nodes to sync.
1077+
1078+
/// Legacy add stake logic, it adds more Coins to a staker's active balance.
1079+
/// It will be directly added to the staker's balance.
1080+
/// Anyone can add stake for a staker. The staker must already exist.
1081+
pub fn legacy_add_stake_0v(
1082+
&mut self,
1083+
store: &mut StakingContractStoreWrite,
1084+
staker_address: &Address,
1085+
value: Coin,
1086+
tx_logger: &mut TransactionLog,
1087+
) -> Result<AddStakeReceipt, AccountError> {
1088+
// Get the staker.
1089+
let mut staker = store.expect_staker(staker_address)?;
1090+
1091+
// Fail if the minimum stake would be violated for the non-retired funds (invariant 1).
1092+
Staker::enforce_min_stake(
1093+
staker.active_balance + value,
1094+
staker.inactive_balance,
1095+
staker.retired_balance,
1096+
)?;
1097+
1098+
// All checks passed, not allowed to fail from here on!
1099+
1100+
// If we are delegating to a validator, we need to update it.
1101+
if let Some(validator_address) = &staker.delegation {
1102+
// Check that the delegation is still valid, i.e. the validator hasn't been deleted.
1103+
store.expect_validator(validator_address)?;
1104+
self.increase_stake_to_validator(store, validator_address, value);
1105+
}
1106+
1107+
// Update the staker's and staking contract's balances.
1108+
staker.active_balance += value;
1109+
self.balance += value;
1110+
1111+
// Build the return logs
1112+
tx_logger.push_log(Log::Stake {
1113+
staker_address: staker_address.clone(),
1114+
validator_address: staker.delegation.clone(),
1115+
value,
1116+
credited_balance: BalanceType::Active,
1117+
});
1118+
1119+
// Update the staker entry.
1120+
store.put_staker(staker_address, staker);
1121+
1122+
Ok(AddStakeReceipt {
1123+
credited_balance: BalanceType::Active,
1124+
})
1125+
}
10181126
}

primitives/account/src/account/staking_contract/traits.rs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -169,8 +169,14 @@ impl AccountTransactionInteraction for StakingContract {
169169
.map(|_| None)
170170
}
171171
IncomingStakingTransactionData::AddStake { staker_address } => self
172-
.add_stake(&mut store, &staker_address, transaction.value, tx_logger)
173-
.map(|_| None),
172+
.add_stake(
173+
&mut store,
174+
&staker_address,
175+
transaction.value,
176+
block_state.protocol_version,
177+
tx_logger,
178+
)
179+
.map(|receipt| Some(receipt.into())),
174180
IncomingStakingTransactionData::UpdateStaker {
175181
new_delegation,
176182
reactivate_all_stake,
@@ -282,7 +288,15 @@ impl AccountTransactionInteraction for StakingContract {
282288
self.revert_create_staker(&mut store, &staker_address, transaction.value, tx_logger)
283289
}
284290
IncomingStakingTransactionData::AddStake { staker_address } => {
285-
self.revert_add_stake(&mut store, &staker_address, transaction.value, tx_logger)
291+
let receipt = receipt.ok_or(AccountError::InvalidReceipt)?.try_into()?;
292+
293+
self.revert_add_stake(
294+
&mut store,
295+
&staker_address,
296+
transaction.value,
297+
receipt,
298+
tx_logger,
299+
)
286300
}
287301
IncomingStakingTransactionData::UpdateStaker { proof, .. } => {
288302
// Get the staker address from the proof.

primitives/account/src/logs.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ use nimiq_transaction::{
1010
Transaction,
1111
};
1212

13+
use crate::BalanceType;
14+
1315
#[derive(Debug, PartialEq, Eq, Clone, Hash, Serialize, Deserialize)]
1416
// Renaming affects only the struct names and thus their tag, the "type" field.
1517
#[serde(rename_all = "kebab-case", tag = "type")]
@@ -105,6 +107,7 @@ pub enum Log {
105107
staker_address: Address,
106108
validator_address: Option<Address>,
107109
value: Coin,
110+
credited_balance: BalanceType,
108111
},
109112

110113
#[serde(rename_all = "camelCase")]

0 commit comments

Comments
 (0)