Skip to content

Commit ba1f015

Browse files
committed
New stake policy and tests
1 parent 7b3521e commit ba1f015

9 files changed

Lines changed: 798 additions & 39 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: 134 additions & 22 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::{
@@ -26,8 +28,10 @@ use crate::{
2628
///
2729
/// Actions concerning a staker are:
2830
/// 1. Create: Creates a staker.
29-
/// 2. AddStake: Adds coins from any outside address to the staker's active balance.
30-
/// This action is only possible if:
31+
/// 2. AddStake: Adds coins from any outside address to the staker.
32+
/// The balance to accredit these funds is preferentially the active balance. If there is no
33+
/// positive active balance, it credits the inactive balance instead.
34+
/// This operation has a minimum value of minimum stake, this it always respects the invariant:
3135
/// (a) the resulting non-retired funds respect the invariant 1 - minimum stake for non-retired funds.
3236
/// 3. SetActiveStake: Re-balances between active and inactive stake by setting the amount of active stake.
3337
/// This action restarts the lock-up period of the inactive stake.
@@ -146,6 +150,7 @@ impl Staker {
146150
/// Invariants:
147151
/// (1) active + inactive balances must be == 0 or >= minimum stake
148152
/// (2) active + inactive + retired balances must be == 0 or >= minimum stake
153+
/// IMPORTANT: This code is shared between new and legacy add stake versions.
149154
pub(crate) fn enforce_min_stake(
150155
active_balance: Coin,
151156
inactive_balance: Coin,
@@ -276,75 +281,127 @@ impl StakingContract {
276281
Ok(())
277282
}
278283

279-
/// Adds more Coins to a staker's balance. It will be directly added to the staker's balance.
284+
/// Adds more Coins to a staker's balance. It adds to the staker's active balance
285+
/// if there are some funds on it (active balance >0). Otherwise, it credits the value to
286+
/// the inactive balance.
280287
/// Anyone can add stake for a staker. The staker must already exist.
281288
pub fn add_stake(
282289
&mut self,
283290
store: &mut StakingContractStoreWrite,
284291
staker_address: &Address,
285292
value: Coin,
293+
protocol_version: u16,
286294
tx_logger: &mut TransactionLog,
287-
) -> Result<(), AccountError> {
295+
) -> Result<AddStakeReceipt, AccountError> {
296+
if protocol_version < Policy::ADD_STAKE_PROTOCOL_UPGRADE_VERSION {
297+
log::warn!(%protocol_version, "Adding stake using old protocol version");
298+
return self.legacy_add_stake_0v(store, staker_address, value, tx_logger);
299+
}
288300
// Get the staker.
289301
let mut staker = store.expect_staker(staker_address)?;
290302

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.
303+
// Check that the delegation is still valid, i.e. the validator hasn't been deleted.
301304
if let Some(validator_address) = &staker.delegation {
302-
// Check that the delegation is still valid, i.e. the validator hasn't been deleted.
303305
store.expect_validator(validator_address)?;
304-
self.increase_stake_to_validator(store, validator_address, value);
305306
}
306307

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

311-
// Build the return logs
337+
// Create the receipt.
338+
let receipt = AddStakeReceipt {
339+
credited_balance: credited_balance.clone(),
340+
};
341+
342+
// If we are actively delegating to a validator, we need to update it.
343+
if credited_balance == BalanceType::Active {
344+
if let Some(validator_address) = &staker.delegation {
345+
self.increase_stake_to_validator(store, validator_address, value);
346+
}
347+
}
348+
349+
// Build the return logs.
312350
tx_logger.push_log(Log::Stake {
313351
staker_address: staker_address.clone(),
314352
validator_address: staker.delegation.clone(),
315353
value,
354+
credited_balance,
316355
});
317356

318357
// Update the staker entry.
319358
store.put_staker(staker_address, staker);
320359

321-
Ok(())
360+
Ok(receipt)
322361
}
323362

324363
/// Reverts a stake transaction.
364+
/// IMPORTANT: This code is shared between new and legacy add stake versions.
325365
pub fn revert_add_stake(
326366
&mut self,
327367
store: &mut StakingContractStoreWrite,
328368
staker_address: &Address,
329369
value: Coin,
370+
receipt: AddStakeReceipt,
330371
tx_logger: &mut TransactionLog,
331372
) -> Result<(), AccountError> {
332373
// Get the staker.
333374
let mut staker = store.expect_staker(staker_address)?;
334375

335376
// 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);
377+
if receipt.credited_balance == BalanceType::Active {
378+
if let Some(validator_address) = &staker.delegation {
379+
self.decrease_stake_from_validator(store, validator_address, value);
380+
}
338381
}
339382

340383
// Update the staker's and staking contract's balances.
341-
staker.active_balance -= value;
384+
match receipt.credited_balance {
385+
BalanceType::Active => {
386+
staker.active_balance -= value;
387+
}
388+
BalanceType::Inactive => {
389+
staker.inactive_balance -= value;
390+
if staker.inactive_balance.is_zero() {
391+
staker.inactive_from = None;
392+
}
393+
}
394+
BalanceType::Retired => {
395+
staker.retired_balance -= value;
396+
}
397+
}
342398
self.balance -= value;
343399

344400
tx_logger.push_log(Log::Stake {
345401
staker_address: staker_address.clone(),
346402
validator_address: staker.delegation.clone(),
347403
value,
404+
credited_balance: receipt.credited_balance,
348405
});
349406

350407
// Update the staker entry.
@@ -949,6 +1006,7 @@ impl StakingContract {
9491006
}
9501007

9511008
/// Adds `value` coins to a given validator's total stake.
1009+
/// IMPORTANT: This code is shared between new and legacy add stake versions.
9521010
fn increase_stake_to_validator(
9531011
&mut self,
9541012
store: &mut StakingContractStoreWrite,
@@ -982,6 +1040,7 @@ impl StakingContract {
9821040
}
9831041

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

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)