Skip to content

Commit ba6e3e5

Browse files
committed
test(app): opts out on unstake if insufficient
1 parent 44ca798 commit ba6e3e5

File tree

3 files changed

+265
-16
lines changed

3 files changed

+265
-16
lines changed

core/application/src/state/executor.rs

+31-13
Original file line numberDiff line numberDiff line change
@@ -757,14 +757,18 @@ impl<B: Backend> StateExecutor<B> {
757757
node.stake.locked += amount;
758758
node.stake.locked_until = current_epoch + lock_time;
759759

760-
// If the node doesn't have sufficient stake and is participating, then set it to opted-out
761-
// so that it will be removed from partipating on epoch change.
762-
if !self.has_sufficient_stake(&node_index) && self.is_participating(&node_index) {
760+
// Save the changed node state.
761+
self.node_info.set(node_index, node.clone());
762+
763+
// If the node doesn't have sufficient unlocked stake and is participating, then set it to
764+
// opted-out so that it will not be included as participating for this epoch and will be
765+
// set as Participation::False on epoch change.
766+
if !self.has_sufficient_unlocked_stake(&node_index) && self.is_participating(&node_index) {
763767
node.participation = Participation::OptedOut;
768+
self.node_info.set(node_index, node);
764769
}
765770

766-
// Save the changed node state and return success
767-
self.node_info.set(node_index, node);
771+
// Return success.
768772
TransactionResponse::Success(ExecutionData::None)
769773
}
770774

@@ -1489,18 +1493,22 @@ impl<B: Backend> StateExecutor<B> {
14891493
/// Whether a node has sufficient stake, including both unlocked and locked stake.
14901494
///
14911495
/// Returns `false` if the node does not exist.
1492-
///
1493-
/// Panics if `ProtocolParamKey::MinimumNodeStake` is missing from the parameters or has an
1494-
/// invalid type.
14951496
fn has_sufficient_stake(&self, node_index: &NodeIndex) -> bool {
1496-
let min_amount = match self.parameters.get(&ProtocolParamKey::MinimumNodeStake) {
1497-
Some(ProtocolParamValue::MinimumNodeStake(v)) => v,
1498-
_ => unreachable!(), // set in genesis
1499-
};
1497+
self.node_info
1498+
.get(node_index)
1499+
.map(|node_info| {
1500+
node_info.stake.staked + node_info.stake.locked >= self.get_min_stake()
1501+
})
1502+
.unwrap_or(false)
1503+
}
15001504

1505+
/// Whether the node has sufficient unlocked stake.
1506+
///
1507+
/// Returns `false` if the node does not exist.
1508+
fn has_sufficient_unlocked_stake(&self, node_index: &NodeIndex) -> bool {
15011509
self.node_info
15021510
.get(node_index)
1503-
.map(|node_info| node_info.stake.staked + node_info.stake.locked >= min_amount.into())
1511+
.map(|node_info| node_info.stake.staked >= self.get_min_stake())
15041512
.unwrap_or(false)
15051513
}
15061514

@@ -1514,6 +1522,16 @@ impl<B: Backend> StateExecutor<B> {
15141522
})
15151523
}
15161524

1525+
/// Returns the minimum amount of stake required for a node to be participating.
1526+
///
1527+
/// Panics if `ProtocolParamKey::MinimumNodeStake` is missing from the parameters or has an
1528+
fn get_min_stake(&self) -> HpUfixed<18> {
1529+
match self.parameters.get(&ProtocolParamKey::MinimumNodeStake) {
1530+
Some(ProtocolParamValue::MinimumNodeStake(v)) => v.into(),
1531+
_ => unreachable!(), // set in genesis
1532+
}
1533+
}
1534+
15171535
fn get_node_info(&self, sender: TransactionSender) -> Option<(NodeIndex, NodeInfo)> {
15181536
match sender {
15191537
TransactionSender::NodeMain(public_key) => match self.pub_key_to_index.get(&public_key)

core/application/src/tests/staking.rs

+202-2
Original file line numberDiff line numberDiff line change
@@ -12,23 +12,52 @@ use fleek_crypto::{
1212
use hp_fixed::unsigned::HpUfixed;
1313
use lightning_committee_beacon::{CommitteeBeaconConfig, CommitteeBeaconTimerConfig};
1414
use lightning_interfaces::types::{
15+
CommitteeSelectionBeaconCommit,
1516
ExecutionData,
1617
ExecutionError,
1718
HandshakePorts,
1819
NodePorts,
20+
Participation,
1921
UpdateMethod,
2022
};
21-
use lightning_interfaces::SyncQueryRunnerInterface;
23+
use lightning_interfaces::{KeystoreInterface, SyncQueryRunnerInterface};
2224
use lightning_test_utils::consensus::MockConsensusConfig;
2325
use lightning_test_utils::e2e::{
2426
DowncastToTestFullNode,
2527
TestFullNodeComponentsWithMockConsensus,
2628
TestNetwork,
2729
TestNetworkNode,
2830
};
31+
use lightning_utils::application::QueryRunnerExt;
2932
use tempfile::tempdir;
33+
use utils::{
34+
create_genesis_committee,
35+
deposit,
36+
deposit_and_stake,
37+
expect_tx_revert,
38+
expect_tx_success,
39+
get_flk_balance,
40+
get_locked,
41+
get_locked_time,
42+
get_node_info,
43+
get_stake_locked_until,
44+
get_staked,
45+
init_app,
46+
prepare_deposit_update,
47+
prepare_initial_stake_update,
48+
prepare_regular_stake_update,
49+
prepare_stake_lock_update,
50+
prepare_unstake_update,
51+
prepare_update_request_consensus,
52+
prepare_update_request_node,
53+
prepare_withdraw_unstaked_update,
54+
run_update,
55+
run_updates,
56+
test_genesis,
57+
test_init_app,
58+
};
3059

31-
use super::utils::*;
60+
use super::*;
3261

3362
#[tokio::test]
3463
async fn test_stake() {
@@ -793,3 +822,174 @@ async fn test_withdraw_unstaked_works_properly() {
793822
// Shutdown the network.
794823
network.shutdown().await;
795824
}
825+
826+
#[tokio::test]
827+
async fn test_unstake_as_non_committee_node_opts_out_node_and_removes_after_epoch_change() {
828+
let network = utils::TestNetwork::builder()
829+
.with_committee_nodes(4)
830+
.with_non_committee_nodes(1)
831+
.build()
832+
.await
833+
.unwrap();
834+
let query = network.query();
835+
let epoch = query.get_current_epoch();
836+
837+
// Check the initial stake.
838+
let stake = query.get_node_info(&4, |n| n.stake).unwrap();
839+
assert_eq!(stake.staked, 1000u64.into());
840+
assert_eq!(stake.locked, 0u64.into());
841+
842+
// Execute unstake transaction from the first node.
843+
let resp = network
844+
.execute(vec![network.node(4).build_transasction_as_owner(
845+
UpdateMethod::Unstake {
846+
amount: 1000u64.into(),
847+
node: network.node(4).keystore.get_ed25519_pk(),
848+
},
849+
1,
850+
)])
851+
.await
852+
.unwrap();
853+
assert_eq!(resp.block_number, 1);
854+
855+
// Check that the stake is now locked.
856+
let stake = query.get_node_info(&4, |n| n.stake).unwrap();
857+
assert_eq!(stake.staked, 0u64.into());
858+
assert_eq!(stake.locked, 1000u64.into());
859+
860+
// Execute epoch change transactions.
861+
let resp = network.execute_change_epoch(epoch).await.unwrap();
862+
assert_eq!(resp.block_number, 2);
863+
864+
// Execute commit-reveal transactions to complete the epoch change process.
865+
let resp = network
866+
.execute(
867+
network
868+
.nodes
869+
.iter()
870+
.enumerate()
871+
.map(|(i, n)| {
872+
n.build_transaction(UpdateMethod::CommitteeSelectionBeaconCommit {
873+
commit: CommitteeSelectionBeaconCommit::build(epoch, 0, [i as u8; 32]),
874+
})
875+
})
876+
.collect(),
877+
)
878+
.await
879+
.unwrap();
880+
assert_eq!(resp.block_number, 3);
881+
let resp = network
882+
.execute(
883+
network
884+
.nodes
885+
.iter()
886+
.enumerate()
887+
.map(|(i, n)| {
888+
n.build_transaction(UpdateMethod::CommitteeSelectionBeaconReveal {
889+
reveal: [i as u8; 32],
890+
})
891+
})
892+
.collect(),
893+
)
894+
.await
895+
.unwrap();
896+
assert_eq!(resp.block_number, 4);
897+
898+
// Check that the epoch has changed.
899+
assert_eq!(query.get_current_epoch(), epoch + 1);
900+
901+
// Check that the node is no longer participating.
902+
assert_eq!(
903+
query.get_node_info(&4, |n| n.participation).unwrap(),
904+
Participation::False
905+
);
906+
}
907+
908+
#[tokio::test]
909+
async fn test_unstake_as_committee_node_opts_out_node_and_removes_after_epoch_change() {
910+
let network = utils::TestNetwork::builder()
911+
.with_committee_nodes(5)
912+
.build()
913+
.await
914+
.unwrap();
915+
let query = network.query();
916+
let epoch = query.get_current_epoch();
917+
918+
// Check the initial stake.
919+
let stake = query.get_node_info(&4, |n| n.stake).unwrap();
920+
assert_eq!(stake.staked, 1000u64.into());
921+
assert_eq!(stake.locked, 0u64.into());
922+
923+
// Execute unstake transaction from the first node.
924+
let resp = network
925+
.execute(vec![network.node(4).build_transasction_as_owner(
926+
UpdateMethod::Unstake {
927+
amount: 1000u64.into(),
928+
node: network.node(4).keystore.get_ed25519_pk(),
929+
},
930+
1,
931+
)])
932+
.await
933+
.unwrap();
934+
assert_eq!(resp.block_number, 1);
935+
936+
// Check that the stake is now locked.
937+
let stake = query.get_node_info(&4, |n| n.stake).unwrap();
938+
assert_eq!(stake.staked, 0u64.into());
939+
assert_eq!(stake.locked, 1000u64.into());
940+
941+
// Execute epoch change transactions from participating nodes.
942+
let resp = network
943+
.execute(
944+
network.nodes[0..4]
945+
.iter()
946+
.map(|node| node.build_transaction(UpdateMethod::ChangeEpoch { epoch }))
947+
.collect(),
948+
)
949+
.await
950+
.unwrap();
951+
assert_eq!(resp.block_number, 2);
952+
953+
// Execute commit-reveal transactions to complete the epoch change process.
954+
let resp = network
955+
.execute(
956+
network
957+
.nodes
958+
.iter()
959+
.enumerate()
960+
.map(|(i, n)| {
961+
n.build_transaction(UpdateMethod::CommitteeSelectionBeaconCommit {
962+
commit: CommitteeSelectionBeaconCommit::build(epoch, 0, [i as u8; 32]),
963+
})
964+
})
965+
.collect(),
966+
)
967+
.await
968+
.unwrap();
969+
assert_eq!(resp.block_number, 3);
970+
let resp = network
971+
.execute(
972+
network
973+
.nodes
974+
.iter()
975+
.enumerate()
976+
.map(|(i, n)| {
977+
n.build_transaction(UpdateMethod::CommitteeSelectionBeaconReveal {
978+
reveal: [i as u8; 32],
979+
})
980+
})
981+
.collect(),
982+
)
983+
.await
984+
.unwrap();
985+
assert_eq!(resp.block_number, 4);
986+
987+
// Check that the epoch has changed.
988+
assert_eq!(query.get_current_epoch(), epoch + 1);
989+
990+
// Check that the node is no longer participating.
991+
assert_eq!(
992+
query.get_node_info(&4, |n| n.participation).unwrap(),
993+
Participation::False
994+
);
995+
}

core/application/src/tests/utils.rs

+32-1
Original file line numberDiff line numberDiff line change
@@ -956,8 +956,11 @@ pub struct TestNetworkBuilder {
956956
commit_phase_duration: u64,
957957
reveal_phase_duration: u64,
958958
stake_lock_time: u64,
959+
genesis_mutator: Option<GenesisMutator>,
959960
}
960961

962+
pub type GenesisMutator = Arc<dyn Fn(&mut Genesis)>;
963+
961964
impl Default for TestNetworkBuilder {
962965
fn default() -> Self {
963966
Self::new()
@@ -972,6 +975,7 @@ impl TestNetworkBuilder {
972975
commit_phase_duration: 2,
973976
reveal_phase_duration: 2,
974977
stake_lock_time: 5,
978+
genesis_mutator: None,
975979
}
976980
}
977981

@@ -1000,6 +1004,14 @@ impl TestNetworkBuilder {
10001004
self
10011005
}
10021006

1007+
pub fn with_genesis_mutator<F>(mut self, mutator: F) -> Self
1008+
where
1009+
F: Fn(&mut Genesis) + 'static,
1010+
{
1011+
self.genesis_mutator = Some(Arc::new(mutator));
1012+
self
1013+
}
1014+
10031015
pub async fn build(&self) -> Result<TestNetwork> {
10041016
let _ = try_init_tracing(None);
10051017

@@ -1079,7 +1091,12 @@ impl TestNetworkBuilder {
10791091
});
10801092
}
10811093

1082-
let genesis = builder.build();
1094+
let mut genesis = builder.build();
1095+
1096+
if let Some(mutator) = self.genesis_mutator.clone() {
1097+
mutator(&mut genesis);
1098+
}
1099+
10831100
app.apply_genesis(genesis).await?;
10841101

10851102
let tx_socket = app.transaction_executor();
@@ -1186,4 +1203,18 @@ impl TestNode {
11861203
)
11871204
.into()
11881205
}
1206+
1207+
pub fn build_transasction_as_owner(
1208+
&self,
1209+
method: UpdateMethod,
1210+
nonce: u64,
1211+
) -> TransactionRequest {
1212+
TransactionBuilder::from_update(
1213+
method,
1214+
self.chain_id,
1215+
nonce,
1216+
&TransactionSigner::AccountOwner(self.owner_secret_key.clone()),
1217+
)
1218+
.into()
1219+
}
11891220
}

0 commit comments

Comments
 (0)