diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/helpers.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/helpers.rs index 887c23fb8d..092f35e80c 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/helpers.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/helpers.rs @@ -14,9 +14,9 @@ use cosmwasm_std::{Coin, Decimal, StdError, StdResult, Uint128}; pub fn compare_decimals(a: Decimal, b: Decimal, epsilon: Option) { let epsilon = epsilon.unwrap_or_else(|| Decimal::from_ratio(1u128, 100_000_000u128)); if a > b { - assert!(a - b < epsilon, "{a} != {b}") + assert!(a - b < epsilon, "{a} != {b}, delta: {}", a - b) } else { - assert!(b - a < epsilon, "{a} != {b}") + assert!(b - a < epsilon, "{a} != {b}, delta: {}", b - a) } } diff --git a/contracts/mixnet/src/mixnodes/helpers.rs b/contracts/mixnet/src/mixnodes/helpers.rs index fc2d56254f..ef78ffb741 100644 --- a/contracts/mixnet/src/mixnodes/helpers.rs +++ b/contracts/mixnet/src/mixnodes/helpers.rs @@ -156,28 +156,29 @@ pub(crate) mod tests { test: &mut TestSetup, stake: Option, ) -> Vec { - let (mix_id, keypair) = test.add_dummy_mixnode_with_keypair(OWNER_EXISTS, stake); + let (mix_id, keypair) = test.add_legacy_mixnode_with_keypair(OWNER_EXISTS, stake); let mix_exists = DummyMixnode { mix_id, owner: Addr::unchecked(OWNER_EXISTS), identity: keypair.public_key().to_base58_string(), }; - let (mix_id, keypair) = test.add_dummy_mixnode_with_keypair(OWNER_UNBONDING, stake); + let (mix_id, keypair) = test.add_legacy_mixnode_with_keypair(OWNER_UNBONDING, stake); let mix_unbonding = DummyMixnode { mix_id, owner: Addr::unchecked(OWNER_UNBONDING), identity: keypair.public_key().to_base58_string(), }; - let (mix_id, keypair) = test.add_dummy_mixnode_with_keypair(OWNER_UNBONDED, stake); + let (mix_id, keypair) = test.add_legacy_mixnode_with_keypair(OWNER_UNBONDED, stake); let mix_unbonded = DummyMixnode { mix_id, owner: Addr::unchecked(OWNER_UNBONDED), identity: keypair.public_key().to_base58_string(), }; - let (mix_id, keypair) = test.add_dummy_mixnode_with_keypair(OWNER_UNBONDED_LEFTOVER, stake); + let (mix_id, keypair) = + test.add_legacy_mixnode_with_keypair(OWNER_UNBONDED_LEFTOVER, stake); let mix_unbonded_leftover = DummyMixnode { mix_id, owner: Addr::unchecked(OWNER_UNBONDED_LEFTOVER), diff --git a/contracts/mixnet/src/mixnodes/queries.rs b/contracts/mixnet/src/mixnodes/queries.rs index 8300edf5f1..8f127ed9f5 100644 --- a/contracts/mixnet/src/mixnodes/queries.rs +++ b/contracts/mixnet/src/mixnodes/queries.rs @@ -266,7 +266,7 @@ pub(crate) mod tests { #[test] fn obeys_limits() { let mut test = TestSetup::new(); - test.add_dummy_mixnodes(1000); + test.add_legacy_mixnodes(1000); let limit = 2; let page1 = query_mixnode_bonds_paged(test.deps(), None, Some(limit)).unwrap(); @@ -276,7 +276,7 @@ pub(crate) mod tests { #[test] fn has_default_limit() { let mut test = TestSetup::new(); - test.add_dummy_mixnodes(1000); + test.add_legacy_mixnodes(1000); // query without explicitly setting a limit let page1 = query_mixnode_bonds_paged(test.deps(), None, None).unwrap(); @@ -290,7 +290,7 @@ pub(crate) mod tests { #[test] fn has_max_limit() { let mut test = TestSetup::new(); - test.add_dummy_mixnodes(1000); + test.add_legacy_mixnodes(1000); // query with a crazily high limit in an attempt to use too many resources let crazy_limit = 1000; @@ -353,7 +353,7 @@ pub(crate) mod tests { #[test] fn obeys_limits() { let mut test = TestSetup::new(); - test.add_dummy_mixnodes(1000); + test.add_legacy_mixnodes(1000); let limit = 2; let page1 = query_mixnodes_details_paged(test.deps(), None, Some(limit)).unwrap(); @@ -363,7 +363,7 @@ pub(crate) mod tests { #[test] fn has_default_limit() { let mut test = TestSetup::new(); - test.add_dummy_mixnodes(1000); + test.add_legacy_mixnodes(1000); // query without explicitly setting a limit let page1 = query_mixnodes_details_paged(test.deps(), None, None).unwrap(); @@ -377,7 +377,7 @@ pub(crate) mod tests { #[test] fn has_max_limit() { let mut test = TestSetup::new(); - test.add_dummy_mixnodes(1000); + test.add_legacy_mixnodes(1000); // query with a crazily high limit in an attempt to use too many resources let crazy_limit = 1000; diff --git a/contracts/mixnet/src/support/tests/mod.rs b/contracts/mixnet/src/support/tests/mod.rs index 26547ff6b0..ed59e8a19c 100644 --- a/contracts/mixnet/src/support/tests/mod.rs +++ b/contracts/mixnet/src/support/tests/mod.rs @@ -11,6 +11,7 @@ pub mod test_helpers { use crate::contract::{execute, instantiate}; use crate::delegations::queries::query_node_delegations_paged; use crate::delegations::storage as delegations_storage; + use crate::delegations::storage::delegations; use crate::delegations::transactions::try_delegate_to_node; use crate::interval::transactions::{ perform_pending_epoch_actions, perform_pending_interval_actions, try_begin_epoch_transition, @@ -64,6 +65,7 @@ pub mod test_helpers { use mixnet_contract_common::events::{ may_find_attribute, MixnetEventType, DELEGATES_REWARD_KEY, OPERATOR_REWARD_KEY, }; + use mixnet_contract_common::helpers::compare_decimals; use mixnet_contract_common::mixnode::{NodeRewarding, UnbondedMixnode}; use mixnet_contract_common::nym_node::{RewardedSetMetadata, Role}; use mixnet_contract_common::pending_events::{PendingEpochEventData, PendingIntervalEventData}; @@ -86,6 +88,8 @@ pub mod test_helpers { }; use nym_crypto::asymmetric::identity; use nym_crypto::asymmetric::identity::KeyPair; + use rand::distributions::WeightedIndex; + use rand::prelude::*; use rand_chacha::rand_core::{CryptoRng, RngCore, SeedableRng}; use rand_chacha::ChaCha20Rng; use serde::Serialize; @@ -135,14 +139,16 @@ pub mod test_helpers { } } + #[track_caller] pub fn assert_eq_with_leeway(a: Uint128, b: Uint128, leeway: Uint128) { if a > b { - assert!(a - b <= leeway) + assert!(a - b <= leeway, "{} != {}", a, b) } else { - assert!(b - a <= leeway) + assert!(b - a <= leeway, "{} != {}", a, b) } } + #[track_caller] pub fn assert_decimals(a: Decimal, b: Decimal) { let epsilon = Decimal::from_ratio(1u128, 100_000_000u128); if a > b { @@ -182,6 +188,137 @@ pub mod test_helpers { } } + pub fn new_complex() -> Self { + let mut test = TestSetup::new(); + + let mut nodes = Vec::new(); + + let problematic_delegator = "n1foomp"; + let problematic_delegator_twin = "n1bar"; + let problematic_delegator_alt_twin = "n1whatever"; + + let choices = [true, false]; + + // every epoch there's a 2% chance of somebody bonding a node + let bonding_weights = [2, 98]; + + // and 15% of making a delegation + let delegation_weights = [15, 85]; + + // and 1% of making a VESTED delegation + let vested_delegation_weights = [1, 99]; + + let bonding_dist = WeightedIndex::new(bonding_weights).unwrap(); + let delegation_dist = WeightedIndex::new(delegation_weights).unwrap(); + let vested_delegation_dist = WeightedIndex::new(vested_delegation_weights).unwrap(); + + // make sure we have at least a single node at the beginning + let owner = test.random_address(); + let mix_id = test.add_legacy_mixnode(&owner, None); + nodes.push(mix_id); + + // create a bunch of nodes and delegations and progress through epochs + for epoch_id in 0..1000 { + // go through 1000 epochs + + let owner = test.random_address(); + let min_stake = 100_000_000; + // u32 has max value of 4B, which is ~4k nym tokens, which is a realistic amount somebody could bond/delegate + let variance = test.rng.next_u32(); + let stake = Uint128::new(min_stake as u128 + variance as u128); + + if choices[bonding_dist.sample(&mut test.rng)] { + // bond + let mix_id = test.add_legacy_mixnode(&owner, Some(stake)); + nodes.push(mix_id); + } + + if choices[delegation_dist.sample(&mut test.rng)] { + // uniformly choose a random node to delegate to + let node = nodes.choose(&mut test.rng).unwrap(); + test.add_immediate_delegation(&owner, stake, *node) + } + + if choices[vested_delegation_dist.sample(&mut test.rng)] { + // uniformly choose a random node to make vested delegation to + let node = nodes.choose(&mut test.rng).unwrap(); + test.add_immediate_delegation_with_legal_proxy(&owner, stake, *node) + } + + // make sure we cover our edge case of somebody having both liquid and vested delegation towards the same node + if epoch_id == 123 { + test.add_immediate_delegation(problematic_delegator, stake, 4); + test.add_immediate_delegation(problematic_delegator_twin, stake, 4); + } + + if epoch_id == 666 { + test.add_immediate_delegation_with_legal_proxy(problematic_delegator, stake, 4); + test.add_immediate_delegation_with_legal_proxy( + problematic_delegator_twin, + stake, + 4, + ); + } + + if epoch_id == 234 { + test.add_immediate_delegation(problematic_delegator_alt_twin, stake, 4); + } + + if epoch_id == 420 { + test.add_immediate_delegation_with_legal_proxy( + problematic_delegator_alt_twin, + stake, + 4, + ); + } + + test.skip_to_next_epoch_end(); + // it doesn't matter that they're on the same layer here, we just need to make sure they're rewarded + test.force_assign_rewarded_set(vec![RoleAssignment { + role: Role::Layer1, + nodes: nodes.clone(), + }]); + test.start_epoch_transition(); + + // reward each node + for node in &nodes { + let performance = test.rng.next_u64() % 100; + let work_factor = test.active_node_work(); + test.reward_with_distribution( + *node, + NodeRewardingParameters { + performance: Performance::from_percentage_value(performance).unwrap(), + work_factor, + }, + ); + } + + test.set_epoch_in_progress_state(); + } + + test + } + + #[track_caller] + pub fn ensure_delegation_sync(&self, mix_id: NodeId) { + let mix_info = self.mix_rewarding(mix_id); + let epsilon = "0.001".parse().unwrap(); + + let subtotal: Decimal = delegations() + .prefix(mix_id) + .range(self.deps().storage, None, None, Order::Ascending) + .filter_map(|d| { + d.map(|(_, del)| { + let pending_rewards = mix_info.determine_delegation_reward(&del).unwrap(); + pending_rewards + del.dec_amount().unwrap() + }) + .ok() + }) + .sum(); + + compare_decimals(mix_info.delegates, subtotal, Some(epsilon)) + } + pub fn random_address(&mut self) -> String { format!("n1foomp{}", self.rng.next_u64()) } @@ -339,6 +476,13 @@ pub mod test_helpers { .vesting_contract_address } + pub fn all_mixnodes(&self) -> Vec { + mixnode_bonds() + .range(self.deps().storage, None, None, Order::Ascending) + .filter_map(|m| m.map(|(_, node)| node.mix_id).ok()) + .collect::>() + } + pub fn coin(&self, amount: u128) -> Coin { coin(amount, rewarding_denom(self.deps().storage).unwrap()) } @@ -687,7 +831,7 @@ pub mod test_helpers { .unwrap(); } - pub fn add_dummy_mixnodes(&mut self, n: usize) { + pub fn add_legacy_mixnodes(&mut self, n: usize) { for i in 0..n { self.add_legacy_mixnode(&format!("owner{i}"), None); } @@ -738,7 +882,7 @@ pub mod test_helpers { ed25519_sign_message(msg, key) } - pub fn add_dummy_mixnode_with_keypair( + pub fn add_legacy_mixnode_with_keypair( &mut self, owner: &str, stake: Option, @@ -1520,55 +1664,6 @@ pub mod test_helpers { perform_pending_interval_actions(deps.branch(), &env, None).unwrap(); } - // pub fn mixnode_with_signature( - // mut rng: impl RngCore + CryptoRng, - // deps: Deps<'_>, - // sender: &str, - // stake: Option>, - // ) -> (MixNode, MessageSignature, KeyPair) { - // // hehe stupid workaround for bypassing the immutable borrow and removing duplicate code - // - // let stake = stake.unwrap_or(good_mixnode_pledge()); - // - // let keypair = identity::KeyPair::new(&mut rng); - // let identity_key = keypair.public_key().to_base58_string(); - // let legit_sphinx_keys = nym_crypto::asymmetric::encryption::KeyPair::new(&mut rng); - // - // let mixnode = MixNode { - // identity_key, - // sphinx_key: legit_sphinx_keys.public_key().to_base58_string(), - // ..tests::fixtures::mix_node_fixture() - // }; - // let msg = mixnode_bonding_sign_payload(deps, sender, None, mixnode.clone(), stake.clone()); - // let owner_signature = ed25519_sign_message(msg, keypair.private_key()); - // - // (mixnode, owner_signature, keypair) - // } - - // pub fn gateway_with_signature( - // mut rng: impl RngCore + CryptoRng, - // deps: Deps<'_>, - // sender: &str, - // stake: Option>, - // ) -> (Gateway, MessageSignature) { - // let stake = stake.unwrap_or(good_gateway_pledge()); - // - // let keypair = identity::KeyPair::new(&mut rng); - // let identity_key = keypair.public_key().to_base58_string(); - // let legit_sphinx_keys = nym_crypto::asymmetric::encryption::KeyPair::new(&mut rng); - // - // let gateway = Gateway { - // identity_key, - // sphinx_key: legit_sphinx_keys.public_key().to_base58_string(), - // ..tests::fixtures::gateway_fixture() - // }; - // - // let msg = gateway_bonding_sign_payload(deps, sender, None, gateway.clone(), stake.clone()); - // let owner_signature = ed25519_sign_message(msg, keypair.private_key()); - // - // (gateway, owner_signature) - // } - pub fn add_dummy_delegations(mut deps: DepsMut<'_>, env: Env, mix_id: NodeId, n: usize) { for i in 0..n { pending_events::delegate( @@ -1583,23 +1678,6 @@ pub mod test_helpers { } } - // pub fn add_dummy_mixnodes( - // mut rng: impl RngCore + CryptoRng, - // mut deps: DepsMut<'_>, - // env: Env, - // n: usize, - // ) { - // for i in 0..n { - // add_mixnode( - // &mut rng, - // deps.branch(), - // env.clone(), - // &format!("owner{}", i), - // tests::fixtures::good_mixnode_pledge(), - // ); - // } - // } - pub fn add_dummy_unbonded_mixnodes( mut rng: impl RngCore + CryptoRng, mut deps: DepsMut<'_>, diff --git a/contracts/mixnet/src/vesting_migration.rs b/contracts/mixnet/src/vesting_migration.rs index 6bf6f2a688..c67a87518e 100644 --- a/contracts/mixnet/src/vesting_migration.rs +++ b/contracts/mixnet/src/vesting_migration.rs @@ -289,121 +289,19 @@ mod tests { mod migrating_vested_delegation { use super::*; use crate::delegations::storage::delegations; - use crate::mixnodes::storage::mixnode_bonds; use crate::support::tests::test_helpers::{assert_eq_with_leeway, TestSetup}; use cosmwasm_std::testing::mock_info; - use cosmwasm_std::{from_binary, Addr, CosmosMsg, Decimal, Order, Uint128, WasmMsg}; + use cosmwasm_std::{from_binary, Addr, CosmosMsg, Order, Uint128, WasmMsg}; use mixnet_contract_common::helpers::compare_decimals; use mixnet_contract_common::nym_node::Role; use mixnet_contract_common::reward_params::{NodeRewardingParameters, Performance}; use mixnet_contract_common::rewarding::helpers::truncate_reward; use mixnet_contract_common::RoleAssignment; - use rand::distributions::Distribution; - use rand::distributions::WeightedIndex; - use rand::prelude::SliceRandom; use rand::RngCore; - fn setup_state() -> TestSetup { - let mut test = TestSetup::new(); - - let mut nodes = Vec::new(); - - let problematic_delegator = "n1unhappydelegator"; - let problematic_delegator_twin = "n1anotherunhappydelegator"; - - let choices = [true, false]; - - // every epoch there's a 2% chance of somebody bonding a node - let bonding_weights = [2, 98]; - - // and 15% of making a delegation - let delegation_weights = [15, 85]; - - // and 1% of making a VESTED delegation - let vested_delegation_weights = [1, 99]; - - let bonding_dist = WeightedIndex::new(bonding_weights).unwrap(); - let delegation_dist = WeightedIndex::new(delegation_weights).unwrap(); - let vested_delegation_dist = WeightedIndex::new(vested_delegation_weights).unwrap(); - - // make sure we have at least a single node at the beginning - let owner = test.random_address(); - let mix_id = test.add_legacy_mixnode(&owner, None); - nodes.push(mix_id); - - // create a bunch of nodes and delegations and progress through epochs - for epoch_id in 0..1000 { - // go through 1000 epochs - - let owner = test.random_address(); - let min_stake = 100_000_000; - // u32 has max value of 4B, which is ~4k nym tokens, which is a realistic amount somebody could bond/delegate - let variance = test.rng.next_u32(); - let stake = Uint128::new(min_stake as u128 + variance as u128); - - if choices[bonding_dist.sample(&mut test.rng)] { - // bond - let mix_id = test.add_legacy_mixnode(&owner, Some(stake)); - nodes.push(mix_id); - } - - if choices[delegation_dist.sample(&mut test.rng)] { - // uniformly choose a random node to delegate to - let node = nodes.choose(&mut test.rng).unwrap(); - test.add_immediate_delegation(&owner, stake, *node) - } - - if choices[vested_delegation_dist.sample(&mut test.rng)] { - // uniformly choose a random node to make vested delegation to - let node = nodes.choose(&mut test.rng).unwrap(); - test.add_immediate_delegation_with_legal_proxy(&owner, stake, *node) - } - - // make sure we cover our edge case of somebody having both liquid and vested delegation towards the same node - if epoch_id == 123 { - test.add_immediate_delegation(problematic_delegator, stake, 4); - test.add_immediate_delegation(problematic_delegator_twin, stake, 4); - } - - if epoch_id == 666 { - test.add_immediate_delegation_with_legal_proxy(problematic_delegator, stake, 4); - test.add_immediate_delegation_with_legal_proxy( - problematic_delegator_twin, - stake, - 4, - ); - } - - test.skip_to_next_epoch_end(); - // it doesn't matter that they're on the same layer here, we just need to make sure they're rewarded - test.force_assign_rewarded_set(vec![RoleAssignment { - role: Role::Layer1, - nodes: nodes.clone(), - }]); - test.start_epoch_transition(); - - // reward each node - for node in &nodes { - let performance = test.rng.next_u64() % 100; - let work_factor = test.active_node_work(); - test.reward_with_distribution( - *node, - NodeRewardingParameters { - performance: Performance::from_percentage_value(performance).unwrap(), - work_factor, - }, - ); - } - - test.set_epoch_in_progress_state(); - } - - test - } - #[test] fn with_no_delegation() { - let mut test = setup_state(); + let mut test = TestSetup::new_complex(); let env = test.env(); let sender = mock_info("owner-without-any-delegations", &[]); @@ -415,7 +313,7 @@ mod tests { #[test] fn with_just_liquid_delegation() { - let mut test = setup_state(); + let mut test = TestSetup::new_complex(); let env = test.env(); // find a valid delegation @@ -442,7 +340,7 @@ mod tests { #[test] fn with_just_vested_delegation() { - let mut test = setup_state(); + let mut test = TestSetup::new_complex(); let env = test.env(); // find a valid delegation @@ -497,32 +395,11 @@ mod tests { #[test] fn with_both_liquid_and_vested_delegation() { - #[track_caller] - fn ensure_delegation_sync(test: &TestSetup, mix_id: NodeId) { - let mix_info = test.mix_rewarding(mix_id); - let epsilon = "0.001".parse().unwrap(); - - let subtotal: Decimal = delegations() - .prefix(mix_id) - .range(test.deps().storage, None, None, Order::Ascending) - .filter_map(|d| { - d.map(|(_, del)| { - let pending_rewards = - mix_info.determine_delegation_reward(&del).unwrap(); - pending_rewards + del.dec_amount().unwrap() - }) - .ok() - }) - .sum(); - - compare_decimals(mix_info.delegates, subtotal, Some(epsilon)) - } - - let mut test = setup_state(); + let mut test = TestSetup::new_complex(); let env = test.env(); - let problematic_delegator = "n1unhappydelegator"; - let problematic_delegator_twin = "n1anotherunhappydelegator"; + let problematic_delegator = "n1foomp"; + let problematic_delegator_twin = "n1bar"; let mix_id = 4; let liquid_storage_key = Delegation::generate_storage_key( @@ -551,7 +428,7 @@ mod tests { .unwrap(); // sanity check before doing anything - ensure_delegation_sync(&test, mix_id); + test.ensure_delegation_sync(mix_id); // a track message is sent into the vesting contract let sender = mock_info(problematic_delegator, &[]); @@ -605,14 +482,11 @@ mod tests { ); // this assertion must still hold - ensure_delegation_sync(&test, mix_id); + test.ensure_delegation_sync(mix_id); // go through few more rewarding epochs to make sure the rewards and accounting // would be the same as if the delegations remained separate - let all_nodes = mixnode_bonds() - .range(test.deps().storage, None, None, Order::Ascending) - .filter_map(|m| m.map(|(_, node)| node.mix_id).ok()) - .collect::>(); + let all_nodes = test.all_mixnodes(); let twin_liquid_storage_key = Delegation::generate_storage_key( mix_id, @@ -667,7 +541,7 @@ mod tests { } // this assertion must still hold - ensure_delegation_sync(&test, mix_id); + test.ensure_delegation_sync(mix_id); let info = test.mix_rewarding(mix_id);