added unit tests for the migration and fixed rounding errors

This commit is contained in:
Jędrzej Stuczyński
2024-10-09 14:52:39 +01:00
parent 14fd7c77d0
commit 02dd16bcc6
5 changed files with 172 additions and 219 deletions
@@ -14,9 +14,9 @@ use cosmwasm_std::{Coin, Decimal, StdError, StdResult, Uint128};
pub fn compare_decimals(a: Decimal, b: Decimal, epsilon: Option<Decimal>) {
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)
}
}
+5 -4
View File
@@ -156,28 +156,29 @@ pub(crate) mod tests {
test: &mut TestSetup,
stake: Option<Uint128>,
) -> Vec<DummyMixnode> {
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),
+6 -6
View File
@@ -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;
+148 -70
View File
@@ -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<NodeId> {
mixnode_bonds()
.range(self.deps().storage, None, None, Order::Ascending)
.filter_map(|m| m.map(|(_, node)| node.mix_id).ok())
.collect::<Vec<_>>()
}
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<Uint128>,
@@ -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<Vec<Coin>>,
// ) -> (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<Vec<Coin>>,
// ) -> (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<'_>,
+11 -137
View File
@@ -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::<Vec<_>>();
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);