From b1607755d5fb1c4baf11f75bda92cff58aec306d Mon Sep 17 00:00:00 2001 From: ii-cruz Date: Tue, 26 Nov 2024 16:05:54 -0600 Subject: [PATCH 1/3] Add stake adds to active if it's predominant on the active stake or to the bigger between inactive and retired. --- .../src/account/staking_contract/receipts.rs | 16 +++++ .../src/account/staking_contract/staker.rs | 62 +++++++++++++++---- .../src/account/staking_contract/traits.rs | 12 +++- primitives/account/src/logs.rs | 3 + .../account/tests/staking_contract/staker.rs | 34 +++++++--- 5 files changed, 105 insertions(+), 22 deletions(-) diff --git a/primitives/account/src/account/staking_contract/receipts.rs b/primitives/account/src/account/staking_contract/receipts.rs index 0646554312..63e04fec36 100644 --- a/primitives/account/src/account/staking_contract/receipts.rs +++ b/primitives/account/src/account/staking_contract/receipts.rs @@ -111,6 +111,22 @@ pub struct DeleteValidatorReceipt { } convert_receipt!(DeleteValidatorReceipt); +#[derive(Debug, PartialEq, Eq, Clone, Hash, Serialize, Deserialize)] +pub enum BalanceType { + Active, + Inactive, + Retired, +} + +/// Receipt for most staker-related transactions. This is necessary to be able to revert +/// these transactions. +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] +pub struct AddStakeReceipt { + /// the balance which the stake was attributed to. + pub credited_balance: BalanceType, +} +convert_receipt!(AddStakeReceipt); + /// Receipt for most staker-related transactions. This is necessary to be able to revert /// these transactions. #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] diff --git a/primitives/account/src/account/staking_contract/staker.rs b/primitives/account/src/account/staking_contract/staker.rs index 89b97566a6..53942ad466 100644 --- a/primitives/account/src/account/staking_contract/staker.rs +++ b/primitives/account/src/account/staking_contract/staker.rs @@ -9,6 +9,8 @@ use nimiq_primitives::coin::Coin; use nimiq_primitives::policy::Policy; use serde::{Deserialize, Serialize}; +use super::AddStakeReceipt; +use crate::BalanceType; #[cfg(feature = "interaction-traits")] use crate::{ account::staking_contract::{ @@ -283,7 +285,7 @@ impl StakingContract { staker_address: &Address, value: Coin, tx_logger: &mut TransactionLog, - ) -> Result<(), AccountError> { + ) -> Result { // Get the staker. let mut staker = store.expect_staker(staker_address)?; @@ -296,28 +298,48 @@ impl StakingContract { // All checks passed, not allowed to fail from here on! - // If we are delegating to a validator, we need to update it. - if let Some(validator_address) = &staker.delegation { - // Check that the delegation is still valid, i.e. the validator hasn't been deleted. - store.expect_validator(validator_address)?; - self.increase_stake_to_validator(store, validator_address, value); - } - + // Create the receipt. // Update the staker's and staking contract's balances. - staker.active_balance += value; + // We want to credit the active balance only if it's the bigger of the non retired balance. + // Otherwise we chose the balance with the most funds between inactive and retired. + let credited_balance; + if staker.active_balance > staker.inactive_balance { + staker.active_balance += value; + credited_balance = BalanceType::Active; + } else if staker.inactive_balance > staker.retired_balance { + staker.inactive_balance += value; + credited_balance = BalanceType::Inactive; + } else { + staker.retired_balance += value; + credited_balance = BalanceType::Retired; + } self.balance += value; + let receipt = AddStakeReceipt { + credited_balance: credited_balance.clone(), + }; + + if credited_balance == BalanceType::Active { + // If we are delegating to a validator, we need to update it. + if let Some(validator_address) = &staker.delegation { + // Check that the delegation is still valid, i.e. the validator hasn't been deleted. + store.expect_validator(validator_address)?; + self.increase_stake_to_validator(store, validator_address, value); + } + } + // Build the return logs tx_logger.push_log(Log::Stake { staker_address: staker_address.clone(), validator_address: staker.delegation.clone(), value, + credited_balance, }); // Update the staker entry. store.put_staker(staker_address, staker); - Ok(()) + Ok(receipt) } /// Reverts a stake transaction. @@ -326,24 +348,38 @@ impl StakingContract { store: &mut StakingContractStoreWrite, staker_address: &Address, value: Coin, + receipt: AddStakeReceipt, tx_logger: &mut TransactionLog, ) -> Result<(), AccountError> { // Get the staker. let mut staker = store.expect_staker(staker_address)?; // If we are delegating to a validator, we need to update it too. - if let Some(validator_address) = &staker.delegation { - self.decrease_stake_from_validator(store, validator_address, value); + if receipt.credited_balance == BalanceType::Active { + if let Some(validator_address) = &staker.delegation { + self.decrease_stake_from_validator(store, validator_address, value); + } } // Update the staker's and staking contract's balances. - staker.active_balance -= value; + match receipt.credited_balance { + BalanceType::Active => { + staker.active_balance -= value; + } + BalanceType::Inactive => { + staker.inactive_balance -= value; + } + BalanceType::Retired => { + staker.retired_balance -= value; + } + } self.balance -= value; tx_logger.push_log(Log::Stake { staker_address: staker_address.clone(), validator_address: staker.delegation.clone(), value, + credited_balance: receipt.credited_balance, }); // Update the staker entry. diff --git a/primitives/account/src/account/staking_contract/traits.rs b/primitives/account/src/account/staking_contract/traits.rs index 03efc3d6e6..9246f31b6d 100644 --- a/primitives/account/src/account/staking_contract/traits.rs +++ b/primitives/account/src/account/staking_contract/traits.rs @@ -169,7 +169,7 @@ impl AccountTransactionInteraction for StakingContract { } IncomingStakingTransactionData::AddStake { staker_address } => self .add_stake(&mut store, &staker_address, transaction.value, tx_logger) - .map(|_| None), + .map(|receipt| Some(receipt.into())), IncomingStakingTransactionData::UpdateStaker { new_delegation, reactivate_all_stake, @@ -281,7 +281,15 @@ impl AccountTransactionInteraction for StakingContract { self.revert_create_staker(&mut store, &staker_address, transaction.value, tx_logger) } IncomingStakingTransactionData::AddStake { staker_address } => { - self.revert_add_stake(&mut store, &staker_address, transaction.value, tx_logger) + let receipt = receipt.ok_or(AccountError::InvalidReceipt)?.try_into()?; + + self.revert_add_stake( + &mut store, + &staker_address, + transaction.value, + receipt, + tx_logger, + ) } IncomingStakingTransactionData::UpdateStaker { proof, .. } => { // Get the staker address from the proof. diff --git a/primitives/account/src/logs.rs b/primitives/account/src/logs.rs index 76e54d679b..0b3a2ddfce 100644 --- a/primitives/account/src/logs.rs +++ b/primitives/account/src/logs.rs @@ -10,6 +10,8 @@ use nimiq_transaction::{ Transaction, }; +use crate::BalanceType; + #[derive(Debug, PartialEq, Eq, Clone, Hash, Serialize, Deserialize)] // Renaming affects only the struct names and thus their tag, the "type" field. #[serde(rename_all = "kebab-case", tag = "type")] @@ -105,6 +107,7 @@ pub enum Log { staker_address: Address, validator_address: Option
, value: Coin, + credited_balance: BalanceType, }, #[serde(rename_all = "camelCase")] diff --git a/primitives/account/tests/staking_contract/staker.rs b/primitives/account/tests/staking_contract/staker.rs index 07bcd52511..a46c537dec 100644 --- a/primitives/account/tests/staking_contract/staker.rs +++ b/primitives/account/tests/staking_contract/staker.rs @@ -341,13 +341,22 @@ fn add_stake_works() { ) .expect("Failed to commit transaction"); - assert_eq!(receipt, None); + assert_eq!( + receipt, + Some( + AddStakeReceipt { + credited_balance: BalanceType::Active + } + .into() + ) + ); assert_eq!( tx_logger.logs, vec![Log::Stake { staker_address: staker_address.clone(), validator_address: Some(validator_address.clone()), value: tx.value, + credited_balance: BalanceType::Active, }] ); @@ -387,7 +396,7 @@ fn add_stake_works() { .revert_incoming_transaction( &tx, &block_state, - None, + receipt, data_store.write(&mut db_txn), &mut tx_logger, ) @@ -399,6 +408,7 @@ fn add_stake_works() { staker_address: staker_address.clone(), validator_address: Some(validator_address.clone()), value: tx.value, + credited_balance: BalanceType::Active, }] ); @@ -495,14 +505,23 @@ fn add_stake_enforces_minimum_stake() { ) .expect("Failed to commit transaction"); - assert_eq!(receipt, None); + assert_eq!( + receipt, + Some( + AddStakeReceipt { + credited_balance: BalanceType::Retired + } + .into() + ) + ); assert_eq!( tx_logs.logs, vec![Log::Stake { staker_address: staker_setup.staker_address.clone(), validator_address: Some(staker_setup.validator_address.clone()), - value: Coin::from_u64_unchecked(Policy::MINIMUM_STAKE) + value: Coin::from_u64_unchecked(Policy::MINIMUM_STAKE), + credited_balance: BalanceType::Retired, }] ); @@ -512,9 +531,10 @@ fn add_stake_enforces_minimum_stake() { .expect("Staker should exist"); assert_eq!( - staker.active_balance, - Coin::from_u64_unchecked(Policy::MINIMUM_STAKE) + staker.retired_balance, + Coin::from_u64_unchecked(Policy::MINIMUM_STAKE + 50_000_000), ); + assert_eq!(staker.active_balance, Coin::ZERO,); assert_eq!(staker.inactive_balance, Coin::ZERO); assert_eq!(staker.inactive_from, None); @@ -526,7 +546,7 @@ fn add_stake_enforces_minimum_stake() { assert_eq!(validator.num_stakers, 1); assert_eq!( validator.total_stake, - Coin::from_u64_unchecked(Policy::VALIDATOR_DEPOSIT + Policy::MINIMUM_STAKE) + Coin::from_u64_unchecked(Policy::VALIDATOR_DEPOSIT) ); } From 7217e53da43c04facf6de886f89009940050f2f3 Mon Sep 17 00:00:00 2001 From: ii-cruz Date: Wed, 11 Dec 2024 18:10:26 +0000 Subject: [PATCH 2/3] Adding minimum stake restriction to the add stake. --- .../src/account/staking_contract/staker.rs | 17 +++++++++++------ .../account/tests/staking_contract/staker.rs | 15 +++++---------- .../src/account/staking_contract/structs.rs | 8 ++++---- .../tests/staking_contract_verify.rs | 4 ++-- test-utils/src/transactions.rs | 2 +- 5 files changed, 23 insertions(+), 23 deletions(-) diff --git a/primitives/account/src/account/staking_contract/staker.rs b/primitives/account/src/account/staking_contract/staker.rs index 53942ad466..81139e3eb8 100644 --- a/primitives/account/src/account/staking_contract/staker.rs +++ b/primitives/account/src/account/staking_contract/staker.rs @@ -289,12 +289,17 @@ impl StakingContract { // Get the staker. let mut staker = store.expect_staker(staker_address)?; - // Fail if the minimum stake would be violated for the non-retired funds (invariant 1). - Staker::enforce_min_stake( - staker.active_balance + value, - staker.inactive_balance, - staker.retired_balance, - )?; + // Add stake txs never violate minimum stake for the non-retired funds (invariant 1), + // because the intrinsic tx checks that value is >= min stake. + assert!( + Staker::enforce_min_stake( + staker.active_balance + value, + staker.inactive_balance, + staker.retired_balance, + ) + .is_ok(), + "Add stake should never violate the min stake invariants" + ); // All checks passed, not allowed to fail from here on! diff --git a/primitives/account/tests/staking_contract/staker.rs b/primitives/account/tests/staking_contract/staker.rs index a46c537dec..223a842486 100644 --- a/primitives/account/tests/staking_contract/staker.rs +++ b/primitives/account/tests/staking_contract/staker.rs @@ -1,7 +1,9 @@ use nimiq_account::*; use nimiq_database::{mdbx::MdbxDatabase, traits::Database}; use nimiq_keys::Address; -use nimiq_primitives::{account::AccountError, coin::Coin, policy::Policy}; +use nimiq_primitives::{ + account::AccountError, coin::Coin, policy::Policy, transaction::TransactionError, +}; use nimiq_test_log::test; use nimiq_transaction::{ account::staking_contract::{IncomingStakingTransactionData, OutgoingStakingTransactionData}, @@ -473,16 +475,9 @@ fn add_stake_enforces_minimum_stake() { Policy::MINIMUM_STAKE - 1, &staker_keypair, ); - - let mut tx_logs = TransactionLog::empty(); assert_eq!( - staker_setup.staking_contract.commit_incoming_transaction( - &tx, - &staker_setup.before_release_block_state, - data_store.write(&mut db_txn), - &mut tx_logs, - ), - Err(AccountError::InvalidCoinValue) + tx.verify(NetworkId::UnitAlbatross), + Err(TransactionError::InvalidValue) ); // Can add in the valid case. diff --git a/primitives/transaction/src/account/staking_contract/structs.rs b/primitives/transaction/src/account/staking_contract/structs.rs index e81e1ecca2..e2191f88b5 100644 --- a/primitives/transaction/src/account/staking_contract/structs.rs +++ b/primitives/transaction/src/account/staking_contract/structs.rs @@ -176,10 +176,10 @@ impl IncomingStakingTransactionData { verify_transaction_signature(transaction, proof)? } IncomingStakingTransactionData::AddStake { .. } => { - // Adding stake should be at least greater than 0. - if transaction.value.is_zero() { - warn!("Add stake transactions must have positive value. The offending transaction is the following:\n{:?}", transaction); - return Err(TransactionError::ZeroValue); + // Adding stake should be greater than 0. + if transaction.value < Coin::from_u64_unchecked(Policy::MINIMUM_STAKE) { + warn!("Add stake must increment stake by at least minimum stake. The offending transaction is the following:\n{:?}", transaction); + return Err(TransactionError::InvalidValue); } // No more checks needed. diff --git a/primitives/transaction/tests/staking_contract_verify.rs b/primitives/transaction/tests/staking_contract_verify.rs index 92f611ca4a..e58872d946 100644 --- a/primitives/transaction/tests/staking_contract_verify.rs +++ b/primitives/transaction/tests/staking_contract_verify.rs @@ -531,12 +531,12 @@ fn stake() { IncomingStakingTransactionData::AddStake { staker_address: STAKER_ADDRESS.parse().unwrap(), }, - 100, + Policy::MINIMUM_STAKE, &keypair, None, ); - let tx_hex = "018c551fabc6e6e00c609c3f0313257ad7e835643c000000000000000000000000000000000000000000010315068c551fabc6e6e00c609c3f0313257ad7e835643c000000000000006400000000000000640000000107006200b3adb13fe6887f6cdcb8c82c429f718fcdbbb27b2a19df7c1ea9814f19cd910500f5d801f531117483118108b30cd606301a424ba63147f3f0a2e085bd655fd15c8eafb971a39883fc9da3711d7ac474cd6047eed7791ec6e00c6ed1a464fddb01"; + let tx_hex = "018c551fabc6e6e00c609c3f0313257ad7e835643c000000000000000000000000000000000000000000010315068c551fabc6e6e00c609c3f0313257ad7e835643c000000000098968000000000000000640000000107006200b3adb13fe6887f6cdcb8c82c429f718fcdbbb27b2a19df7c1ea9814f19cd910500f6318c0aa54b87dc8da554a2f5cdab566d69d09d5f4b148fe98a547cfb4d2032623a5aa11d3150cb716160f5d1ba87adda0ae203f5659d60490366e8a020f10a"; let tx_size = 187; let mut ser_tx: Vec = Vec::with_capacity(tx_size); diff --git a/test-utils/src/transactions.rs b/test-utils/src/transactions.rs index d8cb01d585..7fda38f677 100644 --- a/test-utils/src/transactions.rs +++ b/test-utils/src/transactions.rs @@ -244,7 +244,7 @@ impl TransactionsGenerator { IncomingType::SetActiveStake => { value = Coin::ZERO; } - IncomingType::CreateStaker | IncomingType::RetireStake => { + IncomingType::CreateStaker | IncomingType::AddStake | IncomingType::RetireStake => { value = Coin::from_u64_unchecked(Policy::MINIMUM_STAKE); } _ => {} From 94256d9ac6c30a7378b8c2885e06d50508eec33a Mon Sep 17 00:00:00 2001 From: ii-cruz Date: Fri, 13 Dec 2024 12:07:34 +0000 Subject: [PATCH 3/3] Change crediting policy when adding stake. Adds respective unit tests. --- .../src/account/staking_contract/staker.rs | 19 +- .../account/tests/staking_contract/staker.rs | 410 ++++++++++++++++++ 2 files changed, 419 insertions(+), 10 deletions(-) diff --git a/primitives/account/src/account/staking_contract/staker.rs b/primitives/account/src/account/staking_contract/staker.rs index 81139e3eb8..dcb74fd866 100644 --- a/primitives/account/src/account/staking_contract/staker.rs +++ b/primitives/account/src/account/staking_contract/staker.rs @@ -303,23 +303,22 @@ impl StakingContract { // All checks passed, not allowed to fail from here on! - // Create the receipt. // Update the staker's and staking contract's balances. - // We want to credit the active balance only if it's the bigger of the non retired balance. - // Otherwise we chose the balance with the most funds between inactive and retired. - let credited_balance; - if staker.active_balance > staker.inactive_balance { + // We want to preferentially credit the active balance. Only if there is no active balance, + // then we will choose the balance with the most funds between inactive and retired. + let credited_balance = if !staker.active_balance.is_zero() { staker.active_balance += value; - credited_balance = BalanceType::Active; - } else if staker.inactive_balance > staker.retired_balance { + BalanceType::Active + } else if staker.inactive_balance >= staker.retired_balance { staker.inactive_balance += value; - credited_balance = BalanceType::Inactive; + BalanceType::Inactive } else { staker.retired_balance += value; - credited_balance = BalanceType::Retired; - } + BalanceType::Retired + }; self.balance += value; + // Create the receipt. let receipt = AddStakeReceipt { credited_balance: credited_balance.clone(), }; diff --git a/primitives/account/tests/staking_contract/staker.rs b/primitives/account/tests/staking_contract/staker.rs index 223a842486..ea5a142ea9 100644 --- a/primitives/account/tests/staking_contract/staker.rs +++ b/primitives/account/tests/staking_contract/staker.rs @@ -445,6 +445,416 @@ fn add_stake_works() { ); } +/// Adding stake should give priority to active stake if any. +/// , otherwise it +/// should credit the biggest balance between inactive and retired. +#[test] +fn add_stake_policy_priority_to_active_balance() { + // ----------------------------------- + // Test setup: + // ----------------------------------- + let mut staker_setup = StakerSetup::setup_staker_with_inactive_retired_balance( + ValidatorState::Active, + Policy::MINIMUM_STAKE, + Policy::MINIMUM_STAKE + 1, + 50_000_000, + ); + assert!(staker_setup.active_stake < staker_setup.retired_stake); + assert!(staker_setup.active_stake < staker_setup.inactive_stake); + let data_store = staker_setup + .accounts + .data_store(&Policy::STAKING_CONTRACT_ADDRESS); + let mut db_txn = staker_setup.env.write_transaction(); + let mut db_txn = (&mut db_txn).into(); + let staker_keypair = ed25519_key_pair(STAKER_PRIVATE_KEY); + + // ----------------------------------- + // Test execution: + // ----------------------------------- + // Add stake operation credits to active stake. + let tx = make_signed_incoming_transaction( + IncomingStakingTransactionData::AddStake { + staker_address: staker_setup.staker_address.clone(), + }, + Policy::MINIMUM_STAKE, + &staker_keypair, + ); + + let mut tx_logs = TransactionLog::empty(); + let receipt = staker_setup + .staking_contract + .commit_incoming_transaction( + &tx, + &staker_setup.before_release_block_state, + data_store.write(&mut db_txn), + &mut tx_logs, + ) + .expect("Failed to commit transaction"); + + assert_eq!( + receipt, + Some( + AddStakeReceipt { + credited_balance: BalanceType::Active + } + .into() + ) + ); + + assert_eq!( + tx_logs.logs, + vec![Log::Stake { + staker_address: staker_setup.staker_address.clone(), + validator_address: Some(staker_setup.validator_address.clone()), + value: Coin::from_u64_unchecked(Policy::MINIMUM_STAKE), + credited_balance: BalanceType::Active, + }] + ); + + let staker = staker_setup + .staking_contract + .get_staker(&data_store.read(&db_txn), &staker_setup.staker_address) + .expect("Staker should exist"); + + assert_eq!( + staker.delegation, + Some(staker_setup.validator_address.clone()) + ); + assert_eq!( + staker.active_balance, + Coin::from_u64_unchecked(Policy::MINIMUM_STAKE * 2) + ); + assert_eq!( + staker.inactive_balance, + Coin::from_u64_unchecked(Policy::MINIMUM_STAKE + 1) + ); + assert_eq!(staker.inactive_from, Some(328)); + assert_eq!( + staker.retired_balance, + Coin::from_u64_unchecked(Policy::MINIMUM_STAKE * 5) + ); + + let validator = staker_setup + .staking_contract + .get_validator(&data_store.read(&db_txn), &staker_setup.validator_address) + .unwrap(); + + assert_eq!(validator.num_stakers, 1); + assert_eq!( + validator.total_stake, + Coin::from_u64_unchecked(Policy::VALIDATOR_DEPOSIT + Policy::MINIMUM_STAKE * 2) + ); + + // Reverts correctly. + staker_setup + .staking_contract + .revert_incoming_transaction( + &tx, + &staker_setup.before_release_block_state, + receipt, + data_store.write(&mut db_txn), + &mut TransactionLog::empty(), + ) + .expect("Failed to commit transaction"); + + let staker = staker_setup + .staking_contract + .get_staker(&data_store.read(&db_txn), &staker_setup.staker_address) + .expect("Staker should exist"); + assert_eq!( + staker.delegation, + Some(staker_setup.validator_address.clone()) + ); + + assert_eq!(staker.active_balance, staker_setup.active_stake); + assert_eq!(staker.inactive_balance, staker_setup.inactive_stake); + assert_eq!( + staker.inactive_from, + Some(staker_setup.effective_block_state.number) + ); + assert_eq!(staker.retired_balance, staker_setup.retired_stake); + + let validator = staker_setup + .staking_contract + .get_validator(&data_store.read(&db_txn), &staker_setup.validator_address) + .unwrap(); + assert_eq!(validator.num_stakers, 1); + assert_eq!( + validator.total_stake, + Coin::from_u64_unchecked(Policy::VALIDATOR_DEPOSIT + Policy::MINIMUM_STAKE) + ); +} + +/// Adding stake in absence of active balance should prioritize the maximum between the inactive and retired balance. +#[test] +fn add_stake_policy_maximum_between_inactive_retired() { + // ----------------------------------- + // Test setup: + // ----------------------------------- + let mut staker_setup = StakerSetup::setup_staker_with_inactive_retired_balance( + ValidatorState::Active, + 0, + Policy::MINIMUM_STAKE + 1, + 50_000_000, + ); + assert!(staker_setup.active_stake < staker_setup.retired_stake); + assert!(staker_setup.active_stake < staker_setup.inactive_stake); + let data_store = staker_setup + .accounts + .data_store(&Policy::STAKING_CONTRACT_ADDRESS); + let mut db_txn = staker_setup.env.write_transaction(); + let mut db_txn = (&mut db_txn).into(); + let staker_keypair = ed25519_key_pair(STAKER_PRIVATE_KEY); + + // ----------------------------------- + // Test execution: + // ----------------------------------- + // Add stake operation credits to active stake. + let tx = make_signed_incoming_transaction( + IncomingStakingTransactionData::AddStake { + staker_address: staker_setup.staker_address.clone(), + }, + Policy::MINIMUM_STAKE, + &staker_keypair, + ); + + let mut tx_logs = TransactionLog::empty(); + let receipt = staker_setup + .staking_contract + .commit_incoming_transaction( + &tx, + &staker_setup.before_release_block_state, + data_store.write(&mut db_txn), + &mut tx_logs, + ) + .expect("Failed to commit transaction"); + + assert_eq!( + receipt, + Some( + AddStakeReceipt { + credited_balance: BalanceType::Retired + } + .into() + ) + ); + + assert_eq!( + tx_logs.logs, + vec![Log::Stake { + staker_address: staker_setup.staker_address.clone(), + validator_address: Some(staker_setup.validator_address.clone()), + value: Coin::from_u64_unchecked(Policy::MINIMUM_STAKE), + credited_balance: BalanceType::Retired, + }] + ); + + let staker = staker_setup + .staking_contract + .get_staker(&data_store.read(&db_txn), &staker_setup.staker_address) + .expect("Staker should exist"); + + assert_eq!( + staker.delegation, + Some(staker_setup.validator_address.clone()) + ); + assert_eq!(staker.active_balance, Coin::ZERO); + assert_eq!( + staker.inactive_balance, + Coin::from_u64_unchecked(Policy::MINIMUM_STAKE + 1) + ); + assert_eq!(staker.inactive_from, Some(328)); + assert_eq!( + staker.retired_balance, + Coin::from_u64_unchecked(Policy::MINIMUM_STAKE * 6) + ); + + let validator = staker_setup + .staking_contract + .get_validator(&data_store.read(&db_txn), &staker_setup.validator_address) + .unwrap(); + + assert_eq!(validator.num_stakers, 1); + assert_eq!( + validator.total_stake, + Coin::from_u64_unchecked(Policy::VALIDATOR_DEPOSIT) + ); + + // Reverts correctly. + staker_setup + .staking_contract + .revert_incoming_transaction( + &tx, + &staker_setup.before_release_block_state, + receipt, + data_store.write(&mut db_txn), + &mut TransactionLog::empty(), + ) + .expect("Failed to commit transaction"); + + let staker = staker_setup + .staking_contract + .get_staker(&data_store.read(&db_txn), &staker_setup.staker_address) + .expect("Staker should exist"); + assert_eq!( + staker.delegation, + Some(staker_setup.validator_address.clone()) + ); + + assert_eq!(staker.active_balance, staker_setup.active_stake); + assert_eq!(staker.inactive_balance, staker_setup.inactive_stake); + assert_eq!( + staker.inactive_from, + Some(staker_setup.effective_block_state.number) + ); + assert_eq!(staker.retired_balance, staker_setup.retired_stake); + + let validator = staker_setup + .staking_contract + .get_validator(&data_store.read(&db_txn), &staker_setup.validator_address) + .unwrap(); + assert_eq!(validator.num_stakers, 1); + assert_eq!( + validator.total_stake, + Coin::from_u64_unchecked(Policy::VALIDATOR_DEPOSIT) + ); +} + +/// Adding stake to the inactive stake works. +#[test] +fn add_stake_policy_to_inactive_works() { + // ----------------------------------- + // Test setup: + // ----------------------------------- + let mut staker_setup = StakerSetup::setup_staker_with_inactive_retired_balance( + ValidatorState::Active, + 0, + 50_000_000, + Policy::MINIMUM_STAKE + 1, + ); + assert!(staker_setup.active_stake < staker_setup.retired_stake); + assert!(staker_setup.active_stake < staker_setup.inactive_stake); + let data_store = staker_setup + .accounts + .data_store(&Policy::STAKING_CONTRACT_ADDRESS); + let mut db_txn = staker_setup.env.write_transaction(); + let mut db_txn = (&mut db_txn).into(); + let staker_keypair = ed25519_key_pair(STAKER_PRIVATE_KEY); + + // ----------------------------------- + // Test execution: + // ----------------------------------- + // Add stake operation credits to active stake. + let tx = make_signed_incoming_transaction( + IncomingStakingTransactionData::AddStake { + staker_address: staker_setup.staker_address.clone(), + }, + Policy::MINIMUM_STAKE, + &staker_keypair, + ); + + let mut tx_logs = TransactionLog::empty(); + let receipt = staker_setup + .staking_contract + .commit_incoming_transaction( + &tx, + &staker_setup.before_release_block_state, + data_store.write(&mut db_txn), + &mut tx_logs, + ) + .expect("Failed to commit transaction"); + + assert_eq!( + receipt, + Some( + AddStakeReceipt { + credited_balance: BalanceType::Inactive + } + .into() + ) + ); + + assert_eq!( + tx_logs.logs, + vec![Log::Stake { + staker_address: staker_setup.staker_address.clone(), + validator_address: Some(staker_setup.validator_address.clone()), + value: Coin::from_u64_unchecked(Policy::MINIMUM_STAKE), + credited_balance: BalanceType::Inactive, + }] + ); + + let staker = staker_setup + .staking_contract + .get_staker(&data_store.read(&db_txn), &staker_setup.staker_address) + .expect("Staker should exist"); + assert_eq!( + staker.delegation, + Some(staker_setup.validator_address.clone()) + ); + + assert_eq!(staker.active_balance, Coin::ZERO); + assert_eq!( + staker.inactive_balance, + Coin::from_u64_unchecked(Policy::MINIMUM_STAKE * 6) + ); + assert_eq!(staker.inactive_from, Some(328)); + assert_eq!( + staker.retired_balance, + Coin::from_u64_unchecked(Policy::MINIMUM_STAKE + 1) + ); + + let validator = staker_setup + .staking_contract + .get_validator(&data_store.read(&db_txn), &staker_setup.validator_address) + .unwrap(); + + assert_eq!(validator.num_stakers, 1); + assert_eq!( + validator.total_stake, + Coin::from_u64_unchecked(Policy::VALIDATOR_DEPOSIT) + ); + + // Reverts correctly. + staker_setup + .staking_contract + .revert_incoming_transaction( + &tx, + &staker_setup.before_release_block_state, + receipt, + data_store.write(&mut db_txn), + &mut TransactionLog::empty(), + ) + .expect("Failed to commit transaction"); + + let staker = staker_setup + .staking_contract + .get_staker(&data_store.read(&db_txn), &staker_setup.staker_address) + .expect("Staker should exist"); + assert_eq!( + staker.delegation, + Some(staker_setup.validator_address.clone()) + ); + + assert_eq!(staker.active_balance, staker_setup.active_stake); + assert_eq!(staker.inactive_balance, staker_setup.inactive_stake); + assert_eq!( + staker.inactive_from, + Some(staker_setup.effective_block_state.number) + ); + assert_eq!(staker.retired_balance, staker_setup.retired_stake); + + let validator = staker_setup + .staking_contract + .get_validator(&data_store.read(&db_txn), &staker_setup.validator_address) + .unwrap(); + assert_eq!(validator.num_stakers, 1); + assert_eq!( + validator.total_stake, + Coin::from_u64_unchecked(Policy::VALIDATOR_DEPOSIT) + ); +} + /// Adding stake cannot violate minimum stake for non-retired balances. #[test] fn add_stake_enforces_minimum_stake() {