From ba6e3e5a952a345e975933da9349733207729299 Mon Sep 17 00:00:00 2001 From: Steven Normore Date: Mon, 11 Nov 2024 12:00:28 -0500 Subject: [PATCH] test(app): opts out on unstake if insufficient --- core/application/src/state/executor.rs | 44 ++++-- core/application/src/tests/staking.rs | 204 ++++++++++++++++++++++++- core/application/src/tests/utils.rs | 33 +++- 3 files changed, 265 insertions(+), 16 deletions(-) diff --git a/core/application/src/state/executor.rs b/core/application/src/state/executor.rs index 837009e06..0f452aafc 100644 --- a/core/application/src/state/executor.rs +++ b/core/application/src/state/executor.rs @@ -757,14 +757,18 @@ impl StateExecutor { node.stake.locked += amount; node.stake.locked_until = current_epoch + lock_time; - // If the node doesn't have sufficient stake and is participating, then set it to opted-out - // so that it will be removed from partipating on epoch change. - if !self.has_sufficient_stake(&node_index) && self.is_participating(&node_index) { + // Save the changed node state. + self.node_info.set(node_index, node.clone()); + + // If the node doesn't have sufficient unlocked stake and is participating, then set it to + // opted-out so that it will not be included as participating for this epoch and will be + // set as Participation::False on epoch change. + if !self.has_sufficient_unlocked_stake(&node_index) && self.is_participating(&node_index) { node.participation = Participation::OptedOut; + self.node_info.set(node_index, node); } - // Save the changed node state and return success - self.node_info.set(node_index, node); + // Return success. TransactionResponse::Success(ExecutionData::None) } @@ -1489,18 +1493,22 @@ impl StateExecutor { /// Whether a node has sufficient stake, including both unlocked and locked stake. /// /// Returns `false` if the node does not exist. - /// - /// Panics if `ProtocolParamKey::MinimumNodeStake` is missing from the parameters or has an - /// invalid type. fn has_sufficient_stake(&self, node_index: &NodeIndex) -> bool { - let min_amount = match self.parameters.get(&ProtocolParamKey::MinimumNodeStake) { - Some(ProtocolParamValue::MinimumNodeStake(v)) => v, - _ => unreachable!(), // set in genesis - }; + self.node_info + .get(node_index) + .map(|node_info| { + node_info.stake.staked + node_info.stake.locked >= self.get_min_stake() + }) + .unwrap_or(false) + } + /// Whether the node has sufficient unlocked stake. + /// + /// Returns `false` if the node does not exist. + fn has_sufficient_unlocked_stake(&self, node_index: &NodeIndex) -> bool { self.node_info .get(node_index) - .map(|node_info| node_info.stake.staked + node_info.stake.locked >= min_amount.into()) + .map(|node_info| node_info.stake.staked >= self.get_min_stake()) .unwrap_or(false) } @@ -1514,6 +1522,16 @@ impl StateExecutor { }) } + /// Returns the minimum amount of stake required for a node to be participating. + /// + /// Panics if `ProtocolParamKey::MinimumNodeStake` is missing from the parameters or has an + fn get_min_stake(&self) -> HpUfixed<18> { + match self.parameters.get(&ProtocolParamKey::MinimumNodeStake) { + Some(ProtocolParamValue::MinimumNodeStake(v)) => v.into(), + _ => unreachable!(), // set in genesis + } + } + fn get_node_info(&self, sender: TransactionSender) -> Option<(NodeIndex, NodeInfo)> { match sender { TransactionSender::NodeMain(public_key) => match self.pub_key_to_index.get(&public_key) diff --git a/core/application/src/tests/staking.rs b/core/application/src/tests/staking.rs index 19fc5f73d..1830b5626 100644 --- a/core/application/src/tests/staking.rs +++ b/core/application/src/tests/staking.rs @@ -12,13 +12,15 @@ use fleek_crypto::{ use hp_fixed::unsigned::HpUfixed; use lightning_committee_beacon::{CommitteeBeaconConfig, CommitteeBeaconTimerConfig}; use lightning_interfaces::types::{ + CommitteeSelectionBeaconCommit, ExecutionData, ExecutionError, HandshakePorts, NodePorts, + Participation, UpdateMethod, }; -use lightning_interfaces::SyncQueryRunnerInterface; +use lightning_interfaces::{KeystoreInterface, SyncQueryRunnerInterface}; use lightning_test_utils::consensus::MockConsensusConfig; use lightning_test_utils::e2e::{ DowncastToTestFullNode, @@ -26,9 +28,36 @@ use lightning_test_utils::e2e::{ TestNetwork, TestNetworkNode, }; +use lightning_utils::application::QueryRunnerExt; use tempfile::tempdir; +use utils::{ + create_genesis_committee, + deposit, + deposit_and_stake, + expect_tx_revert, + expect_tx_success, + get_flk_balance, + get_locked, + get_locked_time, + get_node_info, + get_stake_locked_until, + get_staked, + init_app, + prepare_deposit_update, + prepare_initial_stake_update, + prepare_regular_stake_update, + prepare_stake_lock_update, + prepare_unstake_update, + prepare_update_request_consensus, + prepare_update_request_node, + prepare_withdraw_unstaked_update, + run_update, + run_updates, + test_genesis, + test_init_app, +}; -use super::utils::*; +use super::*; #[tokio::test] async fn test_stake() { @@ -793,3 +822,174 @@ async fn test_withdraw_unstaked_works_properly() { // Shutdown the network. network.shutdown().await; } + +#[tokio::test] +async fn test_unstake_as_non_committee_node_opts_out_node_and_removes_after_epoch_change() { + let network = utils::TestNetwork::builder() + .with_committee_nodes(4) + .with_non_committee_nodes(1) + .build() + .await + .unwrap(); + let query = network.query(); + let epoch = query.get_current_epoch(); + + // Check the initial stake. + let stake = query.get_node_info(&4, |n| n.stake).unwrap(); + assert_eq!(stake.staked, 1000u64.into()); + assert_eq!(stake.locked, 0u64.into()); + + // Execute unstake transaction from the first node. + let resp = network + .execute(vec![network.node(4).build_transasction_as_owner( + UpdateMethod::Unstake { + amount: 1000u64.into(), + node: network.node(4).keystore.get_ed25519_pk(), + }, + 1, + )]) + .await + .unwrap(); + assert_eq!(resp.block_number, 1); + + // Check that the stake is now locked. + let stake = query.get_node_info(&4, |n| n.stake).unwrap(); + assert_eq!(stake.staked, 0u64.into()); + assert_eq!(stake.locked, 1000u64.into()); + + // Execute epoch change transactions. + let resp = network.execute_change_epoch(epoch).await.unwrap(); + assert_eq!(resp.block_number, 2); + + // Execute commit-reveal transactions to complete the epoch change process. + let resp = network + .execute( + network + .nodes + .iter() + .enumerate() + .map(|(i, n)| { + n.build_transaction(UpdateMethod::CommitteeSelectionBeaconCommit { + commit: CommitteeSelectionBeaconCommit::build(epoch, 0, [i as u8; 32]), + }) + }) + .collect(), + ) + .await + .unwrap(); + assert_eq!(resp.block_number, 3); + let resp = network + .execute( + network + .nodes + .iter() + .enumerate() + .map(|(i, n)| { + n.build_transaction(UpdateMethod::CommitteeSelectionBeaconReveal { + reveal: [i as u8; 32], + }) + }) + .collect(), + ) + .await + .unwrap(); + assert_eq!(resp.block_number, 4); + + // Check that the epoch has changed. + assert_eq!(query.get_current_epoch(), epoch + 1); + + // Check that the node is no longer participating. + assert_eq!( + query.get_node_info(&4, |n| n.participation).unwrap(), + Participation::False + ); +} + +#[tokio::test] +async fn test_unstake_as_committee_node_opts_out_node_and_removes_after_epoch_change() { + let network = utils::TestNetwork::builder() + .with_committee_nodes(5) + .build() + .await + .unwrap(); + let query = network.query(); + let epoch = query.get_current_epoch(); + + // Check the initial stake. + let stake = query.get_node_info(&4, |n| n.stake).unwrap(); + assert_eq!(stake.staked, 1000u64.into()); + assert_eq!(stake.locked, 0u64.into()); + + // Execute unstake transaction from the first node. + let resp = network + .execute(vec![network.node(4).build_transasction_as_owner( + UpdateMethod::Unstake { + amount: 1000u64.into(), + node: network.node(4).keystore.get_ed25519_pk(), + }, + 1, + )]) + .await + .unwrap(); + assert_eq!(resp.block_number, 1); + + // Check that the stake is now locked. + let stake = query.get_node_info(&4, |n| n.stake).unwrap(); + assert_eq!(stake.staked, 0u64.into()); + assert_eq!(stake.locked, 1000u64.into()); + + // Execute epoch change transactions from participating nodes. + let resp = network + .execute( + network.nodes[0..4] + .iter() + .map(|node| node.build_transaction(UpdateMethod::ChangeEpoch { epoch })) + .collect(), + ) + .await + .unwrap(); + assert_eq!(resp.block_number, 2); + + // Execute commit-reveal transactions to complete the epoch change process. + let resp = network + .execute( + network + .nodes + .iter() + .enumerate() + .map(|(i, n)| { + n.build_transaction(UpdateMethod::CommitteeSelectionBeaconCommit { + commit: CommitteeSelectionBeaconCommit::build(epoch, 0, [i as u8; 32]), + }) + }) + .collect(), + ) + .await + .unwrap(); + assert_eq!(resp.block_number, 3); + let resp = network + .execute( + network + .nodes + .iter() + .enumerate() + .map(|(i, n)| { + n.build_transaction(UpdateMethod::CommitteeSelectionBeaconReveal { + reveal: [i as u8; 32], + }) + }) + .collect(), + ) + .await + .unwrap(); + assert_eq!(resp.block_number, 4); + + // Check that the epoch has changed. + assert_eq!(query.get_current_epoch(), epoch + 1); + + // Check that the node is no longer participating. + assert_eq!( + query.get_node_info(&4, |n| n.participation).unwrap(), + Participation::False + ); +} diff --git a/core/application/src/tests/utils.rs b/core/application/src/tests/utils.rs index ffdf5b2c6..4a794af4b 100644 --- a/core/application/src/tests/utils.rs +++ b/core/application/src/tests/utils.rs @@ -956,8 +956,11 @@ pub struct TestNetworkBuilder { commit_phase_duration: u64, reveal_phase_duration: u64, stake_lock_time: u64, + genesis_mutator: Option, } +pub type GenesisMutator = Arc; + impl Default for TestNetworkBuilder { fn default() -> Self { Self::new() @@ -972,6 +975,7 @@ impl TestNetworkBuilder { commit_phase_duration: 2, reveal_phase_duration: 2, stake_lock_time: 5, + genesis_mutator: None, } } @@ -1000,6 +1004,14 @@ impl TestNetworkBuilder { self } + pub fn with_genesis_mutator(mut self, mutator: F) -> Self + where + F: Fn(&mut Genesis) + 'static, + { + self.genesis_mutator = Some(Arc::new(mutator)); + self + } + pub async fn build(&self) -> Result { let _ = try_init_tracing(None); @@ -1079,7 +1091,12 @@ impl TestNetworkBuilder { }); } - let genesis = builder.build(); + let mut genesis = builder.build(); + + if let Some(mutator) = self.genesis_mutator.clone() { + mutator(&mut genesis); + } + app.apply_genesis(genesis).await?; let tx_socket = app.transaction_executor(); @@ -1186,4 +1203,18 @@ impl TestNode { ) .into() } + + pub fn build_transasction_as_owner( + &self, + method: UpdateMethod, + nonce: u64, + ) -> TransactionRequest { + TransactionBuilder::from_update( + method, + self.chain_id, + nonce, + &TransactionSigner::AccountOwner(self.owner_secret_key.clone()), + ) + .into() + } }