Skip to content

Commit

Permalink
test(app): opts out on unstake if insufficient
Browse files Browse the repository at this point in the history
  • Loading branch information
snormore committed Nov 11, 2024
1 parent 44ca798 commit ba6e3e5
Show file tree
Hide file tree
Showing 3 changed files with 265 additions and 16 deletions.
44 changes: 31 additions & 13 deletions core/application/src/state/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -757,14 +757,18 @@ impl<B: Backend> StateExecutor<B> {
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)
}

Expand Down Expand Up @@ -1489,18 +1493,22 @@ impl<B: Backend> StateExecutor<B> {
/// 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)
}

Expand All @@ -1514,6 +1522,16 @@ impl<B: Backend> StateExecutor<B> {
})
}

/// 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)
Expand Down
204 changes: 202 additions & 2 deletions core/application/src/tests/staking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,52 @@ 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,
TestFullNodeComponentsWithMockConsensus,
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() {
Expand Down Expand Up @@ -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
);
}
33 changes: 32 additions & 1 deletion core/application/src/tests/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -956,8 +956,11 @@ pub struct TestNetworkBuilder {
commit_phase_duration: u64,
reveal_phase_duration: u64,
stake_lock_time: u64,
genesis_mutator: Option<GenesisMutator>,
}

pub type GenesisMutator = Arc<dyn Fn(&mut Genesis)>;

impl Default for TestNetworkBuilder {
fn default() -> Self {
Self::new()
Expand All @@ -972,6 +975,7 @@ impl TestNetworkBuilder {
commit_phase_duration: 2,
reveal_phase_duration: 2,
stake_lock_time: 5,
genesis_mutator: None,
}
}

Expand Down Expand Up @@ -1000,6 +1004,14 @@ impl TestNetworkBuilder {
self
}

pub fn with_genesis_mutator<F>(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<TestNetwork> {
let _ = try_init_tracing(None);

Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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()
}
}

0 comments on commit ba6e3e5

Please sign in to comment.