1880 lines
66 KiB
Rust
1880 lines
66 KiB
Rust
// Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net>
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
#[cfg(test)]
|
|
pub mod fixtures;
|
|
pub(crate) mod legacy;
|
|
|
|
#[cfg(test)]
|
|
pub mod test_helpers {
|
|
use crate::constants;
|
|
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,
|
|
};
|
|
use crate::interval::{pending_events, storage as interval_storage};
|
|
use crate::mixnet_contract_settings::storage::{
|
|
self as mixnet_params_storage, minimum_node_pledge, ADMIN,
|
|
};
|
|
use crate::mixnet_contract_settings::storage::{rewarding_denom, rewarding_validator_address};
|
|
use crate::mixnodes::helpers::get_mixnode_details_by_id;
|
|
use crate::mixnodes::storage as mixnodes_storage;
|
|
use crate::mixnodes::storage::mixnode_bonds;
|
|
use crate::mixnodes::transactions::try_remove_mixnode;
|
|
use crate::nodes::helpers::{
|
|
get_node_details_by_id, get_node_details_by_identity, must_get_node_bond_by_owner,
|
|
};
|
|
use crate::nodes::storage as nymnodes_storage;
|
|
use crate::nodes::storage::helpers::RoleStorageBucket;
|
|
use crate::nodes::storage::rewarded_set::{ACTIVE_ROLES_BUCKET, ROLES, ROLES_METADATA};
|
|
use crate::nodes::storage::{
|
|
next_nymnode_id_counter, read_assigned_roles, save_assignment, swap_active_role_bucket,
|
|
};
|
|
use crate::nodes::transactions::{try_add_nym_node, try_remove_nym_node};
|
|
use crate::rewards::helpers::expensive_role_lookup;
|
|
use crate::rewards::queries::{
|
|
query_pending_delegator_reward, query_pending_mixnode_operator_reward,
|
|
};
|
|
use crate::rewards::storage as rewards_storage;
|
|
use crate::rewards::storage::RewardingStorage;
|
|
use crate::rewards::transactions::try_reward_node;
|
|
use crate::signing::storage as signing_storage;
|
|
use crate::support::helpers::ensure_no_existing_bond;
|
|
use crate::support::tests;
|
|
use crate::support::tests::fixtures::{
|
|
good_gateway_pledge, good_mixnode_pledge, good_node_plegge, TEST_COIN_DENOM,
|
|
};
|
|
use crate::support::tests::{legacy, test_helpers};
|
|
use cosmwasm_std::testing::mock_dependencies;
|
|
use cosmwasm_std::testing::mock_env;
|
|
use cosmwasm_std::testing::mock_info;
|
|
use cosmwasm_std::testing::MockApi;
|
|
use cosmwasm_std::testing::MockQuerier;
|
|
use cosmwasm_std::{coin, coins, Addr, Api, BankMsg, CosmosMsg, Storage};
|
|
use cosmwasm_std::{Coin, Order};
|
|
use cosmwasm_std::{Decimal, Empty, MemoryStorage};
|
|
use cosmwasm_std::{Deps, OwnedDeps};
|
|
use cosmwasm_std::{DepsMut, MessageInfo};
|
|
use cosmwasm_std::{Env, Response, Timestamp, Uint128};
|
|
use mixnet_contract_common::error::MixnetContractError;
|
|
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};
|
|
use mixnet_contract_common::reward_params::{
|
|
NodeRewardingParameters, Performance, RewardedSetParams, RewardingParams, WorkFactor,
|
|
};
|
|
use mixnet_contract_common::rewarding::simulator::simulated_node::SimulatedNode;
|
|
use mixnet_contract_common::rewarding::simulator::Simulator;
|
|
use mixnet_contract_common::rewarding::RewardDistribution;
|
|
use mixnet_contract_common::{
|
|
ContractStateParamsUpdate, Delegation, EpochEventId, EpochState, EpochStatus, ExecuteMsg,
|
|
Gateway, GatewayBondingPayload, IdentityKey, InitialRewardingParams, InstantiateMsg,
|
|
Interval, MixNode, MixNodeBond, MixNodeDetails, MixnodeBondingPayload, NodeId, NymNode,
|
|
NymNodeBond, NymNodeBondingPayload, NymNodeDetails, OperatingCostRange,
|
|
OperatorsParamsUpdate, Percent, ProfitMarginRange, RoleAssignment,
|
|
SignableGatewayBondingMsg, SignableMixNodeBondingMsg, SignableNymNodeBondingMsg,
|
|
};
|
|
use nym_contracts_common::signing::{
|
|
ContractMessageContent, MessageSignature, SignableMessage, SigningAlgorithm, SigningPurpose,
|
|
};
|
|
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;
|
|
use std::collections::HashMap;
|
|
use std::fmt::Debug;
|
|
use std::str::FromStr;
|
|
use std::time::Duration;
|
|
|
|
pub(crate) trait ExtractBankMsg {
|
|
fn unwrap_bank_msg(self) -> Option<BankMsg>;
|
|
}
|
|
|
|
impl ExtractBankMsg for Response {
|
|
fn unwrap_bank_msg(self) -> Option<BankMsg> {
|
|
for msg in self.messages {
|
|
match msg.msg {
|
|
CosmosMsg::Bank(bank_msg) => return Some(bank_msg),
|
|
_ => continue,
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|
|
}
|
|
|
|
#[allow(clippy::enum_variant_names)]
|
|
pub enum NodeQueryType {
|
|
ById(NodeId),
|
|
ByIdentity(IdentityKey),
|
|
ByOwner(Addr),
|
|
}
|
|
|
|
impl From<NodeId> for NodeQueryType {
|
|
fn from(value: NodeId) -> Self {
|
|
NodeQueryType::ById(value)
|
|
}
|
|
}
|
|
|
|
impl From<IdentityKey> for NodeQueryType {
|
|
fn from(value: IdentityKey) -> Self {
|
|
NodeQueryType::ByIdentity(value)
|
|
}
|
|
}
|
|
impl From<Addr> for NodeQueryType {
|
|
fn from(value: Addr) -> Self {
|
|
NodeQueryType::ByOwner(value)
|
|
}
|
|
}
|
|
|
|
#[track_caller]
|
|
pub fn assert_eq_with_leeway(a: Uint128, b: Uint128, leeway: Uint128) {
|
|
if a > b {
|
|
assert!(a - b <= leeway, "{} != {}", a, b)
|
|
} else {
|
|
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 {
|
|
assert!(a - b < epsilon, "{} != {}", a, b)
|
|
} else {
|
|
assert!(b - a < epsilon, "{} != {}", a, b)
|
|
}
|
|
}
|
|
|
|
pub struct TestSetup {
|
|
pub deps: OwnedDeps<MemoryStorage, MockApi, MockQuerier<Empty>>,
|
|
pub env: Env,
|
|
pub rng: ChaCha20Rng,
|
|
|
|
pub rewarding_validator: MessageInfo,
|
|
pub owner: MessageInfo,
|
|
}
|
|
|
|
#[allow(unused)]
|
|
impl TestSetup {
|
|
pub fn new() -> Self {
|
|
let deps = init_contract();
|
|
let rewarding_validator_address =
|
|
rewarding_validator_address(deps.as_ref().storage).unwrap();
|
|
let owner = mixnet_params_storage::ADMIN
|
|
.query_admin(deps.as_ref())
|
|
.unwrap()
|
|
.admin
|
|
.unwrap();
|
|
|
|
TestSetup {
|
|
deps,
|
|
env: mock_env(),
|
|
rng: test_rng(),
|
|
rewarding_validator: mock_info(rewarding_validator_address.as_ref(), &[]),
|
|
owner: mock_info(owner.as_str(), &[]),
|
|
}
|
|
}
|
|
|
|
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 3% chance of somebody bonding a node
|
|
let bonding_weights = [3, 97];
|
|
|
|
// 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, 3);
|
|
test.add_immediate_delegation(problematic_delegator_twin, stake, 3);
|
|
}
|
|
|
|
if epoch_id == 666 {
|
|
test.add_immediate_delegation_with_legal_proxy(problematic_delegator, stake, 3);
|
|
test.add_immediate_delegation_with_legal_proxy(
|
|
problematic_delegator_twin,
|
|
stake,
|
|
3,
|
|
);
|
|
}
|
|
|
|
if epoch_id == 234 {
|
|
test.add_immediate_delegation(problematic_delegator_alt_twin, stake, 3);
|
|
}
|
|
|
|
if epoch_id == 420 {
|
|
test.add_immediate_delegation_with_legal_proxy(
|
|
problematic_delegator_alt_twin,
|
|
stake,
|
|
3,
|
|
);
|
|
}
|
|
|
|
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 admin(&self) -> Addr {
|
|
ADMIN.get(self.deps()).unwrap().unwrap()
|
|
}
|
|
|
|
pub fn random_address(&mut self) -> String {
|
|
format!("n1foomp{}", self.rng.next_u64())
|
|
}
|
|
|
|
pub fn deps(&self) -> Deps<'_> {
|
|
self.deps.as_ref()
|
|
}
|
|
|
|
pub fn deps_mut(&mut self) -> DepsMut<'_> {
|
|
self.deps.as_mut()
|
|
}
|
|
|
|
pub fn storage(&self) -> &dyn Storage {
|
|
self.deps().storage
|
|
}
|
|
|
|
pub fn storage_mut(&mut self) -> &mut dyn Storage {
|
|
self.deps_mut().storage
|
|
}
|
|
|
|
pub fn env(&self) -> Env {
|
|
self.env.clone()
|
|
}
|
|
|
|
pub fn execute(
|
|
&mut self,
|
|
info: MessageInfo,
|
|
msg: ExecuteMsg,
|
|
) -> Result<Response, MixnetContractError> {
|
|
let env = self.env.clone();
|
|
execute(self.deps_mut(), env, info, msg)
|
|
}
|
|
|
|
#[allow(unused)]
|
|
pub fn execute_no_funds(
|
|
&mut self,
|
|
sender: impl Into<String>,
|
|
msg: ExecuteMsg,
|
|
) -> Result<Response, MixnetContractError> {
|
|
self.execute(self.mock_info(sender), msg)
|
|
}
|
|
|
|
pub fn execute_fn<F>(
|
|
&mut self,
|
|
exec_fn: F,
|
|
info: MessageInfo,
|
|
) -> Result<Response, MixnetContractError>
|
|
where
|
|
F: FnOnce(DepsMut<'_>, Env, MessageInfo) -> Result<Response, MixnetContractError>,
|
|
{
|
|
let env = self.env().clone();
|
|
exec_fn(self.deps_mut(), env, info)
|
|
}
|
|
|
|
#[allow(unused)]
|
|
pub fn execute_fn_no_funds<F>(
|
|
&mut self,
|
|
exec_fn: F,
|
|
sender: impl Into<String>,
|
|
) -> Result<Response, MixnetContractError>
|
|
where
|
|
F: FnOnce(DepsMut<'_>, Env, MessageInfo) -> Result<Response, MixnetContractError>,
|
|
{
|
|
let info = self.mock_info(sender);
|
|
self.execute_fn(exec_fn, info)
|
|
}
|
|
|
|
#[track_caller]
|
|
pub fn assert_simple_execution<F>(&mut self, exec_fn: F, info: MessageInfo) -> Response
|
|
where
|
|
F: FnOnce(DepsMut<'_>, Env, MessageInfo) -> Result<Response, MixnetContractError>,
|
|
{
|
|
let caller = std::panic::Location::caller();
|
|
self.execute_fn(exec_fn, info)
|
|
.unwrap_or_else(|err| panic!("{caller} failed with: '{err}' ({err:?})"))
|
|
}
|
|
|
|
#[allow(unused)]
|
|
#[track_caller]
|
|
pub fn assert_simple_execution_no_funds<F>(
|
|
&mut self,
|
|
exec_fn: F,
|
|
sender: impl Into<String>,
|
|
) -> Response
|
|
where
|
|
F: FnOnce(DepsMut<'_>, Env, MessageInfo) -> Result<Response, MixnetContractError>,
|
|
{
|
|
let caller = std::panic::Location::caller();
|
|
self.execute_fn_no_funds(exec_fn, sender)
|
|
.unwrap_or_else(|err| panic!("{caller} failed with: '{err}' ({err:?})"))
|
|
}
|
|
|
|
pub fn update_profit_margin_range(&mut self, range: ProfitMarginRange) {
|
|
self.execute(
|
|
self.owner(),
|
|
ExecuteMsg::UpdateContractStateParams {
|
|
update: ContractStateParamsUpdate {
|
|
delegations_params: None,
|
|
operators_params: Some(OperatorsParamsUpdate {
|
|
minimum_pledge: None,
|
|
profit_margin: Some(range),
|
|
interval_operating_cost: None,
|
|
}),
|
|
config_score_params: None,
|
|
},
|
|
},
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
pub fn update_operating_cost_range(&mut self, range: OperatingCostRange) {
|
|
self.execute(
|
|
self.owner(),
|
|
ExecuteMsg::UpdateContractStateParams {
|
|
update: ContractStateParamsUpdate {
|
|
delegations_params: None,
|
|
operators_params: Some(OperatorsParamsUpdate {
|
|
minimum_pledge: None,
|
|
profit_margin: None,
|
|
interval_operating_cost: Some(range),
|
|
}),
|
|
config_score_params: None,
|
|
},
|
|
},
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
pub fn get_node_id(&self, query_type: impl Into<NodeQueryType>) -> NodeId {
|
|
match query_type.into() {
|
|
NodeQueryType::ById(id) => id,
|
|
NodeQueryType::ByIdentity(identity) => {
|
|
get_node_details_by_identity(&self.deps.storage, identity)
|
|
.unwrap()
|
|
.unwrap()
|
|
.node_id()
|
|
}
|
|
NodeQueryType::ByOwner(owner) => {
|
|
must_get_node_bond_by_owner(&self.deps.storage, &owner)
|
|
.unwrap()
|
|
.node_id
|
|
}
|
|
}
|
|
}
|
|
|
|
#[allow(unused)]
|
|
pub fn mock_info(&self, sender: impl Into<String>) -> MessageInfo {
|
|
mock_info(&sender.into(), &[])
|
|
}
|
|
|
|
pub fn rewarding_validator(&self) -> MessageInfo {
|
|
self.rewarding_validator.clone()
|
|
}
|
|
|
|
pub fn rewarding_params(&self) -> RewardingParams {
|
|
rewards_storage::REWARDING_PARAMS
|
|
.load(self.deps().storage)
|
|
.unwrap()
|
|
}
|
|
|
|
pub fn admin_sender(&self) -> MessageInfo {
|
|
mock_info(self.admin().as_ref(), &[])
|
|
}
|
|
|
|
pub fn owner(&self) -> MessageInfo {
|
|
self.owner.clone()
|
|
}
|
|
|
|
pub fn vesting_contract(&self) -> Addr {
|
|
mixnet_params_storage::CONTRACT_STATE
|
|
.load(self.deps().storage)
|
|
.unwrap()
|
|
.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())
|
|
}
|
|
|
|
pub fn coins(&self, amount: u128) -> Vec<Coin> {
|
|
coins(amount, rewarding_denom(self.deps().storage).unwrap())
|
|
}
|
|
|
|
pub fn current_interval(&self) -> Interval {
|
|
interval_storage::current_interval(self.deps().storage).unwrap()
|
|
}
|
|
|
|
pub fn current_epoch_state(&self) -> EpochState {
|
|
interval_storage::current_epoch_status(self.deps().storage)
|
|
.unwrap()
|
|
.state
|
|
}
|
|
|
|
pub fn active_roles_bucket(&self) -> RoleStorageBucket {
|
|
ACTIVE_ROLES_BUCKET.load(self.deps().storage).unwrap()
|
|
}
|
|
|
|
#[allow(unused)]
|
|
pub fn active_roles_metadata(&self) -> RewardedSetMetadata {
|
|
let bucket = self.active_roles_bucket().other();
|
|
ROLES_METADATA
|
|
.load(self.deps().storage, bucket as u8)
|
|
.unwrap()
|
|
}
|
|
|
|
pub fn inactive_roles_metadata(&self) -> RewardedSetMetadata {
|
|
let bucket = self.active_roles_bucket().other();
|
|
ROLES_METADATA
|
|
.load(self.deps().storage, bucket as u8)
|
|
.unwrap()
|
|
}
|
|
|
|
#[allow(unused)]
|
|
pub fn active_roles(&self, role: Role) -> Vec<NodeId> {
|
|
let bucket = self.active_roles_bucket().other();
|
|
ROLES
|
|
.load(self.deps().storage, (bucket as u8, role))
|
|
.unwrap()
|
|
}
|
|
|
|
pub fn inactive_roles(&self, role: Role) -> Vec<NodeId> {
|
|
let bucket = self.active_roles_bucket().other();
|
|
ROLES
|
|
.load(self.deps().storage, (bucket as u8, role))
|
|
.unwrap()
|
|
}
|
|
|
|
pub fn max_role_count(&self, role: Role) -> u32 {
|
|
RewardingStorage::load()
|
|
.global_rewarding_params
|
|
.load(self.deps().storage)
|
|
.unwrap()
|
|
.rewarded_set
|
|
.maximum_role_count(role)
|
|
}
|
|
|
|
pub fn set_pending_pledge_change(
|
|
&mut self,
|
|
mix_id: NodeId,
|
|
event_id: Option<EpochEventId>,
|
|
) {
|
|
let mut changes = mixnodes_storage::PENDING_MIXNODE_CHANGES
|
|
.load(self.deps().storage, mix_id)
|
|
.unwrap_or_default();
|
|
changes.pledge_change = Some(event_id.unwrap_or(12345));
|
|
|
|
mixnodes_storage::PENDING_MIXNODE_CHANGES
|
|
.save(self.deps_mut().storage, mix_id, &changes)
|
|
.unwrap();
|
|
}
|
|
|
|
pub fn lowest_mix_layer(&mut self) -> Role {
|
|
let layer1 = read_assigned_roles(&self.deps.storage, Role::Layer1).unwrap();
|
|
let layer2 = read_assigned_roles(&self.deps.storage, Role::Layer2).unwrap();
|
|
let layer3 = read_assigned_roles(&self.deps.storage, Role::Layer3).unwrap();
|
|
let l1 = layer1.len();
|
|
let l2 = layer2.len();
|
|
let l3 = layer3.len();
|
|
|
|
if l1 <= l2 && l1 <= l3 {
|
|
Role::Layer1
|
|
} else if l2 <= l3 && l2 <= l1 {
|
|
Role::Layer2
|
|
} else {
|
|
Role::Layer3
|
|
}
|
|
}
|
|
|
|
pub fn immediately_assign_lowest_mix_layer(&mut self, node_id: NodeId) -> Role {
|
|
let layer = self.lowest_mix_layer();
|
|
self.immediately_add_to_role(node_id, layer);
|
|
layer
|
|
}
|
|
|
|
pub fn immediately_add_to_role(&mut self, node_id: NodeId, role: Role) {
|
|
let active_bucket = ACTIVE_ROLES_BUCKET.load(&self.deps.storage).unwrap();
|
|
let mut current = read_assigned_roles(self.deps().storage, role).unwrap();
|
|
current.push(node_id);
|
|
ROLES
|
|
.save(
|
|
&mut self.deps.storage,
|
|
(active_bucket as u8, role),
|
|
¤t,
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
pub fn immediately_assign_standby_role(&mut self, node_id: NodeId) {
|
|
self.immediately_add_to_role(node_id, Role::Standby)
|
|
}
|
|
|
|
pub fn immediately_assign_exit_gateway_role(&mut self, node_id: NodeId) {
|
|
self.immediately_add_to_role(node_id, Role::ExitGateway)
|
|
}
|
|
|
|
pub fn immediately_assign_entry_gateway_role(&mut self, node_id: NodeId) {
|
|
self.immediately_add_to_role(node_id, Role::EntryGateway)
|
|
}
|
|
|
|
pub fn add_rewarded_set_nymnode(
|
|
&mut self,
|
|
owner: &str,
|
|
stake: Option<Uint128>,
|
|
) -> (NymNodeBond, MessageSignature, KeyPair) {
|
|
let res = self.add_nymnode(owner, stake);
|
|
let id = res.0.node_id;
|
|
self.immediately_assign_lowest_mix_layer(id);
|
|
|
|
res
|
|
}
|
|
|
|
pub fn add_rewarded_set_nymnode_id(
|
|
&mut self,
|
|
owner: &str,
|
|
stake: Option<Uint128>,
|
|
) -> NodeId {
|
|
self.add_rewarded_set_nymnode(owner, stake).0.node_id
|
|
}
|
|
|
|
pub fn add_nymnode(
|
|
&mut self,
|
|
owner: &str,
|
|
stake: Option<Uint128>,
|
|
) -> (NymNodeBond, MessageSignature, KeyPair) {
|
|
let stake = self.make_node_pledge(stake);
|
|
let (node, owner_signature, keypair) =
|
|
self.node_with_signature(owner, Some(stake.clone()));
|
|
|
|
let info = mock_info(owner, stake.as_ref());
|
|
let env = self.env();
|
|
|
|
try_add_nym_node(
|
|
self.deps_mut(),
|
|
env,
|
|
info.clone(),
|
|
node,
|
|
tests::fixtures::node_cost_params_fixture(),
|
|
owner_signature.clone(),
|
|
)
|
|
.unwrap();
|
|
|
|
let bond = must_get_node_bond_by_owner(&self.deps.storage, &info.sender).unwrap();
|
|
|
|
(bond, owner_signature, keypair)
|
|
}
|
|
|
|
pub fn add_dummy_nymnode(&mut self, owner: &str, stake: Option<Uint128>) -> NodeId {
|
|
self.add_nymnode(owner, stake).0.node_id
|
|
}
|
|
|
|
#[allow(unused)]
|
|
pub fn add_dummy_nym_node_with_keypair(
|
|
&mut self,
|
|
owner: &str,
|
|
stake: Option<Uint128>,
|
|
) -> (NodeId, identity::KeyPair) {
|
|
let (bond, _, keypair) = self.add_nymnode(owner, stake);
|
|
(bond.node_id, keypair)
|
|
}
|
|
|
|
#[track_caller]
|
|
pub fn add_legacy_mixnode(&mut self, owner: &str, stake: Option<Uint128>) -> NodeId {
|
|
let stake = self.make_mix_pledge(stake);
|
|
let (mixnode, _, _) = self.mixnode_with_signature(owner, Some(stake.clone()));
|
|
|
|
let info = mock_info(owner, stake.as_ref());
|
|
let env = self.env();
|
|
|
|
ensure_no_existing_bond(&info.sender, &self.deps.storage).unwrap();
|
|
signing_storage::increment_signing_nonce(&mut self.deps.storage, info.sender.clone())
|
|
.unwrap();
|
|
legacy::save_new_mixnode(
|
|
&mut self.deps.storage,
|
|
env,
|
|
mixnode,
|
|
tests::fixtures::node_cost_params_fixture(),
|
|
info.sender,
|
|
info.funds[0].clone(),
|
|
)
|
|
.unwrap()
|
|
}
|
|
|
|
pub fn add_rewarded_mixing_node(&mut self, owner: &str, stake: Option<Uint128>) -> NodeId {
|
|
let node_id = self.add_dummy_nymnode(owner, stake);
|
|
self.immediately_assign_lowest_mix_layer(node_id);
|
|
node_id
|
|
}
|
|
|
|
pub fn add_rewarded_entry_gateway_node(
|
|
&mut self,
|
|
owner: &str,
|
|
stake: Option<Uint128>,
|
|
) -> NodeId {
|
|
let node_id = self.add_dummy_nymnode(owner, stake);
|
|
self.immediately_assign_entry_gateway_role(node_id);
|
|
node_id
|
|
}
|
|
|
|
pub fn add_rewarded_exit_gateway_node(
|
|
&mut self,
|
|
owner: &str,
|
|
stake: Option<Uint128>,
|
|
) -> NodeId {
|
|
let node_id = self.add_dummy_nymnode(owner, stake);
|
|
self.immediately_assign_exit_gateway_role(node_id);
|
|
node_id
|
|
}
|
|
|
|
pub fn add_standby_node(&mut self, owner: &str, stake: Option<Uint128>) -> NodeId {
|
|
let node_id = self.add_dummy_nymnode(owner, stake);
|
|
self.immediately_assign_standby_role(node_id);
|
|
node_id
|
|
}
|
|
|
|
pub fn add_legacy_mixnode_with_proxy_and_keypair(
|
|
&mut self,
|
|
owner: &str,
|
|
stake: Option<Uint128>,
|
|
) -> (NodeId, identity::KeyPair) {
|
|
let pledge = self.make_mix_pledge(stake).pop().unwrap();
|
|
|
|
let proxy = self.vesting_contract();
|
|
|
|
let keypair = identity::KeyPair::new(&mut self.rng);
|
|
let identity_key = keypair.public_key().to_base58_string();
|
|
let legit_sphinx_keys = nym_crypto::asymmetric::encryption::KeyPair::new(&mut self.rng);
|
|
|
|
let mixnode = MixNode {
|
|
identity_key,
|
|
sphinx_key: legit_sphinx_keys.public_key().to_base58_string(),
|
|
..tests::fixtures::mix_node_fixture()
|
|
};
|
|
|
|
let height = self.env.block.height;
|
|
let storage = self.deps_mut().storage;
|
|
|
|
// manually unroll `save_new_mixnode` to allow for proxy usage
|
|
let mix_id = next_nymnode_id_counter(storage).unwrap();
|
|
|
|
let current_epoch = interval_storage::current_interval(storage)
|
|
.unwrap()
|
|
.current_epoch_absolute_id();
|
|
|
|
let mixnode_rewarding = NodeRewarding::initialise_new(
|
|
tests::fixtures::node_cost_params_fixture(),
|
|
&pledge,
|
|
current_epoch,
|
|
)
|
|
.unwrap();
|
|
let mixnode_bond = MixNodeBond {
|
|
mix_id,
|
|
owner: Addr::unchecked(owner),
|
|
original_pledge: pledge,
|
|
mix_node: mixnode,
|
|
proxy: Some(proxy),
|
|
bonding_height: height,
|
|
is_unbonding: false,
|
|
};
|
|
|
|
mixnode_bonds()
|
|
.save(storage, mix_id, &mixnode_bond)
|
|
.unwrap();
|
|
rewards_storage::MIXNODE_REWARDING
|
|
.save(storage, mix_id, &mixnode_rewarding)
|
|
.unwrap();
|
|
|
|
(mix_id, keypair)
|
|
}
|
|
|
|
pub fn add_legacy_mixnode_with_legal_proxy(
|
|
&mut self,
|
|
owner: &str,
|
|
stake: Option<Uint128>,
|
|
) -> NodeId {
|
|
self.add_legacy_mixnode_with_proxy_and_keypair(owner, stake)
|
|
.0
|
|
}
|
|
|
|
pub fn add_rewarded_legacy_mixnode(
|
|
&mut self,
|
|
owner: &str,
|
|
stake: Option<Uint128>,
|
|
) -> NodeId {
|
|
let node_id = self.add_legacy_mixnode(owner, stake);
|
|
self.immediately_assign_lowest_mix_layer(node_id);
|
|
|
|
node_id
|
|
}
|
|
|
|
pub fn add_legacy_gateway(
|
|
&mut self,
|
|
sender: &str,
|
|
stake: Option<Uint128>,
|
|
) -> (IdentityKey, NodeId) {
|
|
let stake = self.make_gateway_pledge(stake);
|
|
let (gateway, _) = self.gateway_with_signature(sender, Some(stake.clone()));
|
|
|
|
let env = self.env();
|
|
let info = mock_info(sender, &stake);
|
|
|
|
legacy::save_new_gateway(
|
|
&mut self.deps.storage,
|
|
env,
|
|
gateway,
|
|
info.sender,
|
|
info.funds[0].clone(),
|
|
)
|
|
.unwrap()
|
|
}
|
|
|
|
pub fn save_legacy_gateway(&mut self, gateway: Gateway, info: &MessageInfo) {
|
|
let env = self.env();
|
|
|
|
legacy::save_new_gateway(
|
|
&mut self.deps.storage,
|
|
env,
|
|
gateway,
|
|
info.sender.clone(),
|
|
info.funds[0].clone(),
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
pub fn add_legacy_mixnodes(&mut self, n: usize) {
|
|
for i in 0..n {
|
|
self.add_legacy_mixnode(&format!("owner{i}"), None);
|
|
}
|
|
}
|
|
|
|
pub fn add_dummy_gateways(&mut self, n: usize) {
|
|
for i in 0..n {
|
|
self.add_legacy_gateway(&format!("owner{i}"), None);
|
|
}
|
|
}
|
|
|
|
pub fn make_node_pledge(&self, stake: Option<Uint128>) -> Vec<Coin> {
|
|
let stake = match stake {
|
|
Some(amount) => {
|
|
let denom = rewarding_denom(self.deps().storage).unwrap();
|
|
Coin { denom, amount }
|
|
}
|
|
None => minimum_node_pledge(self.deps.as_ref().storage).unwrap(),
|
|
};
|
|
vec![stake]
|
|
}
|
|
|
|
pub fn make_mix_pledge(&self, stake: Option<Uint128>) -> Vec<Coin> {
|
|
self.make_node_pledge(stake)
|
|
}
|
|
|
|
pub fn make_gateway_pledge(&self, stake: Option<Uint128>) -> Vec<Coin> {
|
|
self.make_node_pledge(stake)
|
|
}
|
|
|
|
pub fn mixnode_by_id(&self, node_id: NodeId) -> Option<MixNodeDetails> {
|
|
get_mixnode_details_by_id(self.deps().storage, node_id).unwrap()
|
|
}
|
|
|
|
pub fn nymnode_by_id(&self, node_id: NodeId) -> Option<NymNodeDetails> {
|
|
get_node_details_by_id(self.deps().storage, node_id).unwrap()
|
|
}
|
|
|
|
pub fn mixnode_bonding_signature(
|
|
&mut self,
|
|
key: &identity::PrivateKey,
|
|
owner: &str,
|
|
mixnode: MixNode,
|
|
stake: Option<Uint128>,
|
|
) -> MessageSignature {
|
|
let stake = self.make_mix_pledge(stake);
|
|
let msg = mixnode_bonding_sign_payload(self.deps(), owner, mixnode, stake);
|
|
ed25519_sign_message(msg, key)
|
|
}
|
|
|
|
pub fn add_legacy_mixnode_with_keypair(
|
|
&mut self,
|
|
owner: &str,
|
|
stake: Option<Uint128>,
|
|
) -> (NodeId, identity::KeyPair) {
|
|
let stake = self.make_mix_pledge(stake);
|
|
let (mixnode, _, keypair) = self.mixnode_with_signature(owner, Some(stake.clone()));
|
|
|
|
let info = mock_info(owner, stake.as_ref());
|
|
let env = self.env();
|
|
|
|
ensure_no_existing_bond(&info.sender, &self.deps.storage).unwrap();
|
|
signing_storage::increment_signing_nonce(&mut self.deps.storage, info.sender.clone())
|
|
.unwrap();
|
|
let node_id = legacy::save_new_mixnode(
|
|
&mut self.deps.storage,
|
|
env,
|
|
mixnode,
|
|
tests::fixtures::node_cost_params_fixture(),
|
|
info.sender,
|
|
info.funds[0].clone(),
|
|
)
|
|
.unwrap();
|
|
|
|
(node_id, keypair)
|
|
}
|
|
|
|
pub fn node_with_signature(
|
|
&mut self,
|
|
sender: &str,
|
|
stake: Option<Vec<Coin>>,
|
|
) -> (NymNode, MessageSignature, KeyPair) {
|
|
let stake = stake.unwrap_or(good_node_plegge());
|
|
|
|
let keypair = identity::KeyPair::new(&mut self.rng);
|
|
let identity_key = keypair.public_key().to_base58_string();
|
|
|
|
let node = NymNode {
|
|
host: "1.2.3.4".to_string(),
|
|
custom_http_port: None,
|
|
identity_key,
|
|
};
|
|
let msg = nymnode_bonding_sign_payload(self.deps(), sender, node.clone(), stake);
|
|
let owner_signature = ed25519_sign_message(msg, keypair.private_key());
|
|
|
|
(node, owner_signature, keypair)
|
|
}
|
|
|
|
pub fn mixnode_with_signature(
|
|
&mut self,
|
|
sender: &str,
|
|
stake: Option<Vec<Coin>>,
|
|
) -> (MixNode, MessageSignature, KeyPair) {
|
|
let stake = stake.unwrap_or(good_mixnode_pledge());
|
|
|
|
let keypair = identity::KeyPair::new(&mut self.rng);
|
|
let identity_key = keypair.public_key().to_base58_string();
|
|
let legit_sphinx_keys = nym_crypto::asymmetric::encryption::KeyPair::new(&mut self.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(self.deps(), sender, mixnode.clone(), stake);
|
|
let owner_signature = ed25519_sign_message(msg, keypair.private_key());
|
|
|
|
(mixnode, owner_signature, keypair)
|
|
}
|
|
|
|
pub fn gateway_with_signature(
|
|
&mut self,
|
|
sender: impl Into<String>,
|
|
stake: Option<Vec<Coin>>,
|
|
) -> (Gateway, MessageSignature) {
|
|
let stake = stake.unwrap_or(good_gateway_pledge());
|
|
|
|
let keypair = identity::KeyPair::new(&mut self.rng);
|
|
let identity_key = keypair.public_key().to_base58_string();
|
|
let legit_sphinx_keys = nym_crypto::asymmetric::encryption::KeyPair::new(&mut self.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(
|
|
self.deps(),
|
|
sender.into().as_str(),
|
|
gateway.clone(),
|
|
stake,
|
|
);
|
|
let owner_signature = ed25519_sign_message(msg, keypair.private_key());
|
|
|
|
(gateway, owner_signature)
|
|
}
|
|
|
|
#[track_caller]
|
|
pub fn start_unbonding_mixnode(&mut self, mix_id: NodeId) {
|
|
let bond_details = mixnodes_storage::mixnode_bonds()
|
|
.load(self.deps().storage, mix_id)
|
|
.unwrap();
|
|
|
|
let env = self.env();
|
|
try_remove_mixnode(
|
|
self.deps_mut(),
|
|
env,
|
|
mock_info(bond_details.owner.as_str(), &[]),
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
#[track_caller]
|
|
pub fn start_unbonding_nymnode(&mut self, node_id: NodeId) {
|
|
let bond_details = nymnodes_storage::nym_nodes()
|
|
.load(self.deps().storage, node_id)
|
|
.unwrap();
|
|
|
|
let env = self.env();
|
|
try_remove_nym_node(
|
|
self.deps_mut(),
|
|
env,
|
|
mock_info(bond_details.owner.as_str(), &[]),
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
#[track_caller]
|
|
pub fn immediately_unbond_node(&mut self, node: impl Into<NodeQueryType>) {
|
|
let node_id = self.get_node_id(node);
|
|
let env = self.env();
|
|
pending_events::unbond_nym_node(self.deps_mut(), &env, env.block.height, node_id)
|
|
.unwrap();
|
|
}
|
|
|
|
pub fn immediately_unbond_mixnode(&mut self, mix_id: NodeId) {
|
|
let env = self.env();
|
|
pending_events::unbond_mixnode(self.deps_mut(), &env, env.block.height, mix_id)
|
|
.unwrap();
|
|
}
|
|
|
|
pub fn immediately_unbond_nymnode(&mut self, node_id: NodeId) {
|
|
let env = self.env();
|
|
pending_events::unbond_nym_node(self.deps_mut(), &env, env.block.height, node_id)
|
|
.unwrap();
|
|
}
|
|
|
|
pub fn add_immediate_delegation(
|
|
&mut self,
|
|
delegator: &str,
|
|
amount: impl Into<Uint128>,
|
|
target: NodeId,
|
|
) {
|
|
let denom = rewarding_denom(self.deps().storage).unwrap();
|
|
let amount = Coin {
|
|
denom,
|
|
amount: amount.into(),
|
|
};
|
|
let env = self.env();
|
|
pending_events::delegate(
|
|
self.deps_mut(),
|
|
&env,
|
|
env.block.height,
|
|
Addr::unchecked(delegator),
|
|
target,
|
|
amount,
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
pub fn add_immediate_delegation_with_legal_proxy(
|
|
&mut self,
|
|
delegator: &str,
|
|
amount: impl Into<Uint128>,
|
|
target: NodeId,
|
|
) {
|
|
let denom = rewarding_denom(self.deps().storage).unwrap();
|
|
let amount = Coin {
|
|
denom,
|
|
amount: amount.into(),
|
|
};
|
|
let proxy = self.vesting_contract();
|
|
|
|
let owner = self.deps.api.addr_validate(delegator).unwrap();
|
|
let storage_key = Delegation::generate_storage_key(target, &owner, Some(&proxy));
|
|
|
|
let mut mix_rewarding = self.mix_rewarding(target);
|
|
|
|
let mut stored_delegation_amount = amount;
|
|
|
|
if let Some(existing_delegation) = delegations_storage::delegations()
|
|
.may_load(&self.deps.storage, storage_key.clone())
|
|
.unwrap()
|
|
{
|
|
let og_with_reward = mix_rewarding.undelegate(&existing_delegation).unwrap();
|
|
stored_delegation_amount.amount += og_with_reward.amount;
|
|
}
|
|
|
|
mix_rewarding
|
|
.add_base_delegation(stored_delegation_amount.amount)
|
|
.unwrap();
|
|
|
|
let delegation = Delegation {
|
|
owner,
|
|
node_id: target,
|
|
cumulative_reward_ratio: mix_rewarding.total_unit_reward,
|
|
amount: stored_delegation_amount,
|
|
height: self.env.block.height,
|
|
proxy: Some(proxy),
|
|
};
|
|
|
|
delegations_storage::delegations()
|
|
.save(&mut self.deps.storage, storage_key, &delegation)
|
|
.unwrap();
|
|
rewards_storage::MIXNODE_REWARDING
|
|
.save(&mut self.deps.storage, target, &mix_rewarding)
|
|
.unwrap();
|
|
}
|
|
|
|
#[allow(unused)]
|
|
pub fn add_delegation(
|
|
&mut self,
|
|
delegator: &str,
|
|
amount: impl Into<Uint128>,
|
|
target: NodeId,
|
|
) {
|
|
let denom = rewarding_denom(self.deps().storage).unwrap();
|
|
let amount = Coin {
|
|
denom,
|
|
amount: amount.into(),
|
|
};
|
|
let env = self.env();
|
|
delegate(self.deps_mut(), env, delegator, vec![amount], target)
|
|
}
|
|
|
|
pub fn remove_immediate_delegation(&mut self, delegator: &str, target: NodeId) {
|
|
let height = self.env.block.height;
|
|
pending_events::undelegate(self.deps_mut(), height, Addr::unchecked(delegator), target)
|
|
.unwrap();
|
|
}
|
|
|
|
pub fn start_epoch_transition(&mut self) {
|
|
let env = self.env.clone();
|
|
let sender = self.rewarding_validator.clone();
|
|
try_begin_epoch_transition(self.deps_mut(), env, sender).unwrap();
|
|
}
|
|
|
|
pub fn epoch_state(&self) -> EpochState {
|
|
interval_storage::current_epoch_status(self.deps().storage)
|
|
.unwrap()
|
|
.state
|
|
}
|
|
|
|
pub fn set_epoch_in_progress_state(&mut self) {
|
|
let being_advanced_by = self.rewarding_validator.sender.clone();
|
|
interval_storage::save_current_epoch_status(
|
|
self.deps_mut().storage,
|
|
&EpochStatus {
|
|
being_advanced_by,
|
|
state: EpochState::InProgress,
|
|
},
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
pub fn set_epoch_reconciliation_state(&mut self) {
|
|
let being_advanced_by = self.rewarding_validator.sender.clone();
|
|
interval_storage::save_current_epoch_status(
|
|
self.deps_mut().storage,
|
|
&EpochStatus {
|
|
being_advanced_by,
|
|
state: EpochState::ReconcilingEvents,
|
|
},
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
pub fn set_epoch_role_assignment_state(&mut self) {
|
|
let being_advanced_by = self.rewarding_validator.sender.clone();
|
|
interval_storage::save_current_epoch_status(
|
|
self.deps_mut().storage,
|
|
&EpochStatus {
|
|
being_advanced_by,
|
|
state: EpochState::RoleAssignment {
|
|
next: Role::first(),
|
|
},
|
|
},
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
#[allow(unused)]
|
|
pub fn pending_operator_reward(&mut self, mix: NodeId) -> Decimal {
|
|
query_pending_mixnode_operator_reward(self.deps(), mix)
|
|
.unwrap()
|
|
.amount_earned_detailed
|
|
.expect("no reward!")
|
|
}
|
|
|
|
#[allow(unused)]
|
|
pub fn pending_delegator_reward(&mut self, delegator: &str, target: NodeId) -> Decimal {
|
|
query_pending_delegator_reward(self.deps(), delegator.into(), target, None)
|
|
.unwrap()
|
|
.amount_earned_detailed
|
|
.expect("no reward!")
|
|
}
|
|
|
|
pub fn skip_to_next_epoch_end(&mut self) {
|
|
self.skip_to_next_epoch();
|
|
self.skip_to_current_epoch_end();
|
|
}
|
|
|
|
pub fn skip_to_current_epoch_end(&mut self) {
|
|
let interval = interval_storage::current_interval(self.deps().storage).unwrap();
|
|
let epoch_end = interval.current_epoch_end_unix_timestamp();
|
|
// skip few blocks just in case
|
|
self.env.block.height += 10;
|
|
self.env.block.time = Timestamp::from_seconds(epoch_end as u64);
|
|
}
|
|
|
|
pub fn skip_to_current_interval_end(&mut self) {
|
|
let interval = interval_storage::current_interval(self.deps().storage).unwrap();
|
|
let interval_end = interval.current_interval_end_unix_timestamp();
|
|
// skip few blocks just in case
|
|
self.env.block.height += 10;
|
|
self.env.block.time = Timestamp::from_seconds(interval_end as u64);
|
|
}
|
|
|
|
pub fn skip_to_next_epoch(&mut self) {
|
|
let interval = interval_storage::current_interval(self.deps().storage).unwrap();
|
|
let epoch_end = interval.current_epoch_end_unix_timestamp();
|
|
// skip few blocks just in case
|
|
self.env.block.height += 10;
|
|
self.env.block.time = Timestamp::from_seconds(epoch_end as u64 + 1);
|
|
let advanced = interval.advance_epoch();
|
|
|
|
assert_eq!(
|
|
interval.current_epoch_absolute_id() + 1,
|
|
advanced.current_epoch_absolute_id()
|
|
);
|
|
|
|
if interval.current_epoch_id() != interval.epochs_in_interval() - 1 {
|
|
assert_eq!(interval.current_epoch_id() + 1, advanced.current_epoch_id())
|
|
} else {
|
|
assert_eq!(advanced.current_epoch_id(), 0);
|
|
assert_eq!(
|
|
interval.current_interval_id() + 1,
|
|
advanced.current_interval_id()
|
|
)
|
|
}
|
|
|
|
interval_storage::save_interval(self.deps_mut().storage, &advanced).unwrap();
|
|
// if we're going into next epoch, we're back into in progress
|
|
self.set_epoch_in_progress_state();
|
|
}
|
|
|
|
pub fn reset_role_assignment(&mut self) {
|
|
let active_bucket = ACTIVE_ROLES_BUCKET.load(&self.deps.storage).unwrap();
|
|
|
|
for role in [
|
|
Role::EntryGateway,
|
|
Role::ExitGateway,
|
|
Role::Layer1,
|
|
Role::Layer2,
|
|
Role::Layer3,
|
|
Role::Standby,
|
|
] {
|
|
ROLES
|
|
.save(&mut self.deps.storage, (active_bucket as u8, role), &vec![])
|
|
.unwrap();
|
|
}
|
|
}
|
|
|
|
pub fn force_assign_rewarded_set(&mut self, assignment: Vec<RoleAssignment>) {
|
|
self.reset_role_assignment();
|
|
|
|
// we cheat a bit to write to the 'active' bucket instead
|
|
swap_active_role_bucket(self.deps_mut().storage).unwrap();
|
|
for role_assignment in assignment {
|
|
let mut sorted_assignment = role_assignment.clone();
|
|
sorted_assignment.nodes.sort();
|
|
|
|
save_assignment(self.deps_mut().storage, sorted_assignment).unwrap();
|
|
}
|
|
swap_active_role_bucket(self.deps_mut().storage).unwrap();
|
|
}
|
|
|
|
// note: this does NOT assign gateway role
|
|
pub fn force_change_mix_rewarded_set(&mut self, nodes: Vec<NodeId>) {
|
|
let mut roles = HashMap::new();
|
|
for node in nodes {
|
|
let layer = self.lowest_mix_layer();
|
|
let assigned = roles.entry(layer).or_insert(Vec::new());
|
|
assigned.push(node)
|
|
}
|
|
|
|
let roles = roles
|
|
.into_iter()
|
|
.map(|(role, nodes)| RoleAssignment { role, nodes })
|
|
.collect();
|
|
|
|
self.force_assign_rewarded_set(roles)
|
|
}
|
|
|
|
pub fn instantiate_simulator(&self, node: NodeId) -> Simulator {
|
|
simulator_from_single_node_state(self.deps(), node)
|
|
}
|
|
|
|
pub fn execute_all_pending_events(&mut self) {
|
|
let env = self.env();
|
|
execute_all_pending_events(self.deps_mut(), env)
|
|
}
|
|
|
|
pub fn pending_interval_events(&self) -> Vec<PendingIntervalEventData> {
|
|
interval_storage::PENDING_INTERVAL_EVENTS
|
|
.range(self.deps().storage, None, None, Order::Ascending)
|
|
.map(|res| res.unwrap().1)
|
|
.collect::<Vec<_>>()
|
|
}
|
|
|
|
pub fn pending_epoch_events(&self) -> Vec<PendingEpochEventData> {
|
|
interval_storage::PENDING_EPOCH_EVENTS
|
|
.range(self.deps().storage, None, None, Order::Ascending)
|
|
.map(|res| res.unwrap().1)
|
|
.collect::<Vec<_>>()
|
|
}
|
|
|
|
pub fn active_node_work(&self) -> WorkFactor {
|
|
self.rewarding_params().active_node_work()
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
pub fn standby_node_work(&self) -> WorkFactor {
|
|
self.rewarding_params().standby_node_work()
|
|
}
|
|
|
|
pub fn active_node_params(&self, performance: f32) -> NodeRewardingParameters {
|
|
NodeRewardingParameters {
|
|
performance: test_helpers::performance(performance),
|
|
work_factor: self.active_node_work(),
|
|
}
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
pub fn standby_node_params(&self, performance: f32) -> NodeRewardingParameters {
|
|
NodeRewardingParameters {
|
|
performance: test_helpers::performance(performance),
|
|
work_factor: self.standby_node_work(),
|
|
}
|
|
}
|
|
|
|
#[track_caller]
|
|
pub fn reward_with_distribution_ignore_state(
|
|
&mut self,
|
|
node_id: NodeId,
|
|
params: NodeRewardingParameters,
|
|
) -> RewardDistribution {
|
|
self.reward_with_distribution_with_state_bypass(
|
|
node_id,
|
|
params.performance,
|
|
params.work_factor,
|
|
)
|
|
}
|
|
|
|
#[track_caller]
|
|
pub fn reward_with_distribution_with_state_bypass(
|
|
&mut self,
|
|
node_id: NodeId,
|
|
performance: Performance,
|
|
work_factor: WorkFactor,
|
|
) -> RewardDistribution {
|
|
let initial_status =
|
|
interval_storage::current_epoch_status(self.deps().storage).unwrap();
|
|
self.start_epoch_transition();
|
|
let res = self.reward_with_distribution(
|
|
node_id,
|
|
NodeRewardingParameters::new(performance, work_factor),
|
|
);
|
|
interval_storage::save_current_epoch_status(self.deps_mut().storage, &initial_status)
|
|
.unwrap();
|
|
res
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
#[track_caller]
|
|
pub fn node_role(&self, node_id: NodeId) -> Role {
|
|
if read_assigned_roles(&self.deps.storage, Role::EntryGateway)
|
|
.unwrap()
|
|
.contains(&node_id)
|
|
{
|
|
Role::EntryGateway
|
|
} else if read_assigned_roles(&self.deps.storage, Role::ExitGateway)
|
|
.unwrap()
|
|
.contains(&node_id)
|
|
{
|
|
Role::ExitGateway
|
|
} else if read_assigned_roles(&self.deps.storage, Role::Layer1)
|
|
.unwrap()
|
|
.contains(&node_id)
|
|
{
|
|
Role::Layer1
|
|
} else if read_assigned_roles(&self.deps.storage, Role::Layer2)
|
|
.unwrap()
|
|
.contains(&node_id)
|
|
{
|
|
Role::Layer2
|
|
} else if read_assigned_roles(&self.deps.storage, Role::Layer3)
|
|
.unwrap()
|
|
.contains(&node_id)
|
|
{
|
|
Role::Layer3
|
|
} else if read_assigned_roles(&self.deps.storage, Role::Standby)
|
|
.unwrap()
|
|
.contains(&node_id)
|
|
{
|
|
Role::Standby
|
|
} else {
|
|
let caller = std::panic::Location::caller();
|
|
panic!("{caller}: no assigned roles")
|
|
}
|
|
}
|
|
|
|
pub fn legacy_rewarding_params(
|
|
&self,
|
|
node_id: NodeId,
|
|
performance: f32,
|
|
) -> NodeRewardingParameters {
|
|
let performance = test_helpers::performance(performance);
|
|
let work_factor = self.get_legacy_rewarding_node_work_factor(node_id);
|
|
NodeRewardingParameters {
|
|
performance,
|
|
work_factor,
|
|
}
|
|
}
|
|
|
|
pub fn get_legacy_rewarding_node_work_factor(&self, node_id: NodeId) -> Decimal {
|
|
let global_rewarding_params = self.rewarding_params();
|
|
let work_factor =
|
|
match expensive_role_lookup(self.deps.as_ref().storage, node_id).unwrap() {
|
|
None => Decimal::zero(),
|
|
Some(Role::Standby) => global_rewarding_params.standby_node_work(),
|
|
_ => global_rewarding_params.active_node_work(),
|
|
};
|
|
work_factor
|
|
}
|
|
|
|
#[track_caller]
|
|
pub fn reward_with_distribution(
|
|
&mut self,
|
|
node_id: NodeId,
|
|
rewarding_params: NodeRewardingParameters,
|
|
) -> RewardDistribution {
|
|
let env = self.env();
|
|
let sender = self.rewarding_validator();
|
|
|
|
let res =
|
|
try_reward_node(self.deps_mut(), env, sender, node_id, rewarding_params).unwrap();
|
|
|
|
if rewarding_params.is_zero() {
|
|
return RewardDistribution {
|
|
operator: Decimal::zero(),
|
|
delegates: Decimal::zero(),
|
|
};
|
|
}
|
|
let operator: Decimal = find_attribute(
|
|
Some(MixnetEventType::NodeRewarding.to_string()),
|
|
OPERATOR_REWARD_KEY,
|
|
&res,
|
|
)
|
|
.parse()
|
|
.unwrap();
|
|
let delegates: Decimal = find_attribute(
|
|
Some(MixnetEventType::NodeRewarding.to_string()),
|
|
DELEGATES_REWARD_KEY,
|
|
&res,
|
|
)
|
|
.parse()
|
|
.unwrap();
|
|
|
|
RewardDistribution {
|
|
operator,
|
|
delegates,
|
|
}
|
|
}
|
|
|
|
pub fn read_delegation(
|
|
&mut self,
|
|
mix: NodeId,
|
|
owner: &str,
|
|
proxy: Option<&str>,
|
|
) -> Delegation {
|
|
read_delegation(
|
|
self.deps().storage,
|
|
mix,
|
|
&Addr::unchecked(owner),
|
|
&proxy.map(Addr::unchecked),
|
|
)
|
|
.unwrap()
|
|
}
|
|
|
|
pub fn mix_rewarding(&self, node: NodeId) -> NodeRewarding {
|
|
rewards_storage::MIXNODE_REWARDING
|
|
.load(self.deps().storage, node)
|
|
.unwrap()
|
|
}
|
|
|
|
#[allow(unused)]
|
|
pub fn mix_bond(&self, mix_id: NodeId) -> MixNodeBond {
|
|
mixnode_bonds().load(self.deps().storage, mix_id).unwrap()
|
|
}
|
|
|
|
#[track_caller]
|
|
pub fn delegation(&self, mix: NodeId, owner: &str, proxy: &Option<Addr>) -> Delegation {
|
|
let caller = std::panic::Location::caller();
|
|
|
|
read_delegation(self.deps().storage, mix, &Addr::unchecked(owner), proxy)
|
|
.unwrap_or_else(|| {
|
|
panic!("{caller} failed with: delegation for {mix}/{owner} doesn't exist")
|
|
})
|
|
}
|
|
}
|
|
|
|
pub fn ed25519_sign_message<T: Serialize + SigningPurpose>(
|
|
message: SignableMessage<T>,
|
|
private_key: &identity::PrivateKey,
|
|
) -> MessageSignature {
|
|
match message.algorithm {
|
|
SigningAlgorithm::Ed25519 => {
|
|
let plaintext = message.to_plaintext().unwrap();
|
|
let signature = private_key.sign(plaintext);
|
|
MessageSignature::from(signature.to_bytes().as_ref())
|
|
}
|
|
SigningAlgorithm::Secp256k1 => {
|
|
unimplemented!()
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn simulator_from_single_node_state(deps: Deps<'_>, node: NodeId) -> Simulator {
|
|
let mix_rewarding = rewards_storage::MIXNODE_REWARDING
|
|
.load(deps.storage, node)
|
|
.unwrap();
|
|
let delegations = query_node_delegations_paged(deps, node, None, None).unwrap();
|
|
if delegations.delegations.len() as u32
|
|
== constants::DELEGATION_PAGE_DEFAULT_RETRIEVAL_LIMIT
|
|
{
|
|
// can't be bothered to deal with paging for this test case since it's incredibly unlikely
|
|
// we'd ever need it
|
|
panic!("too many delegations")
|
|
}
|
|
let rewarding_params = rewards_storage::REWARDING_PARAMS
|
|
.load(deps.storage)
|
|
.unwrap();
|
|
let interval = interval_storage::current_interval(deps.storage).unwrap();
|
|
let mut simulator = Simulator::new(rewarding_params, interval);
|
|
simulator.nodes.insert(
|
|
0,
|
|
SimulatedNode {
|
|
mix_id: 0,
|
|
rewarding_details: mix_rewarding,
|
|
delegations: delegations
|
|
.delegations
|
|
.into_iter()
|
|
.map(|d| (d.owner.to_string(), d))
|
|
.collect(),
|
|
},
|
|
);
|
|
|
|
simulator
|
|
}
|
|
|
|
pub fn get_bank_send_msg(response: &Response) -> Option<(String, Vec<Coin>)> {
|
|
for msg in &response.messages {
|
|
if let CosmosMsg::Bank(BankMsg::Send { to_address, amount }) = &msg.msg {
|
|
return Some((to_address.clone(), amount.clone()));
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
#[track_caller]
|
|
pub fn find_attribute<S: Into<String>>(
|
|
event_type: Option<S>,
|
|
attribute: &str,
|
|
response: &Response,
|
|
) -> String {
|
|
let event_type = event_type.map(Into::into);
|
|
for event in &response.events {
|
|
if let Some(typ) = &event_type {
|
|
if &event.ty != typ {
|
|
continue;
|
|
}
|
|
}
|
|
if let Some(attr) = may_find_attribute(event, attribute) {
|
|
return attr;
|
|
}
|
|
}
|
|
// this is only used in tests so panic here is fine
|
|
panic!("did not find the attribute")
|
|
}
|
|
|
|
pub(crate) trait FindAttribute {
|
|
fn attribute<E, S>(&self, event_type: E, attribute: &str) -> String
|
|
where
|
|
E: Into<Option<S>>,
|
|
S: Into<String>;
|
|
|
|
fn any_attribute(&self, attribute: &str) -> String {
|
|
self.attribute::<_, String>(None, attribute)
|
|
}
|
|
|
|
fn any_parsed_attribute<T>(&self, attribute: &str) -> T
|
|
where
|
|
T: FromStr,
|
|
<T as FromStr>::Err: Debug,
|
|
{
|
|
self.parsed_attribute::<_, String, T>(None, attribute)
|
|
}
|
|
|
|
fn parsed_attribute<E, S, T>(&self, event_type: E, attribute: &str) -> T
|
|
where
|
|
E: Into<Option<S>>,
|
|
S: Into<String>,
|
|
T: FromStr,
|
|
<T as FromStr>::Err: Debug;
|
|
|
|
fn decimal<E, S>(&self, event_type: E, attribute: &str) -> Decimal
|
|
where
|
|
E: Into<Option<S>>,
|
|
S: Into<String>,
|
|
{
|
|
self.parsed_attribute(event_type, attribute)
|
|
}
|
|
}
|
|
|
|
impl FindAttribute for Response {
|
|
fn attribute<E, S>(&self, event_type: E, attribute: &str) -> String
|
|
where
|
|
E: Into<Option<S>>,
|
|
S: Into<String>,
|
|
{
|
|
find_attribute(event_type.into(), attribute, self)
|
|
}
|
|
|
|
fn parsed_attribute<E, S, T>(&self, event_type: E, attribute: &str) -> T
|
|
where
|
|
E: Into<Option<S>>,
|
|
S: Into<String>,
|
|
T: FromStr,
|
|
<T as FromStr>::Err: Debug,
|
|
{
|
|
find_attribute(event_type.into(), attribute, self)
|
|
.parse()
|
|
.unwrap()
|
|
}
|
|
}
|
|
|
|
// using floats in tests is fine
|
|
// (what it does is converting % value, like 12.34 into `Performance` (`Percent`)
|
|
// which internally is represented by decimal `0.1234`
|
|
pub fn performance(val: f32) -> Performance {
|
|
assert!(val <= 100.0);
|
|
assert!(val >= 0.0);
|
|
|
|
// hehe, that's such a nasty conversion, but it works for test purposes
|
|
let str = (val / 100.0).to_string();
|
|
let dec = str.parse().unwrap();
|
|
Performance::new(dec).unwrap()
|
|
}
|
|
|
|
// use rng with constant seed for all tests so that they would be deterministic
|
|
pub fn test_rng() -> ChaCha20Rng {
|
|
let dummy_seed = [42u8; 32];
|
|
rand_chacha::ChaCha20Rng::from_seed(dummy_seed)
|
|
}
|
|
|
|
pub fn execute_all_pending_events(mut deps: DepsMut<'_>, env: Env) {
|
|
perform_pending_epoch_actions(deps.branch(), &env, None).unwrap();
|
|
perform_pending_interval_actions(deps.branch(), &env, None).unwrap();
|
|
}
|
|
|
|
pub fn add_dummy_delegations(mut deps: DepsMut<'_>, env: Env, mix_id: NodeId, n: usize) {
|
|
for i in 0..n {
|
|
pending_events::delegate(
|
|
deps.branch(),
|
|
&env,
|
|
env.block.height,
|
|
Addr::unchecked(format!("owner{}", i)),
|
|
mix_id,
|
|
tests::fixtures::good_mixnode_pledge().pop().unwrap(),
|
|
)
|
|
.unwrap();
|
|
}
|
|
}
|
|
|
|
pub fn add_dummy_unbonded_mixnodes(
|
|
mut rng: impl RngCore + CryptoRng,
|
|
mut deps: DepsMut<'_>,
|
|
n: usize,
|
|
) {
|
|
for i in 0..n {
|
|
add_unbonded_mixnode(&mut rng, deps.branch(), None, &format!("owner{}", i));
|
|
}
|
|
}
|
|
|
|
pub fn add_dummy_unbonded_mixnodes_with_owner(
|
|
mut rng: impl RngCore + CryptoRng,
|
|
mut deps: DepsMut<'_>,
|
|
owner: &str,
|
|
n: usize,
|
|
) {
|
|
for _ in 0..n {
|
|
add_unbonded_mixnode(&mut rng, deps.branch(), None, owner);
|
|
}
|
|
}
|
|
|
|
pub fn add_dummy_unbonded_mixnodes_with_identity(
|
|
mut rng: impl RngCore + CryptoRng,
|
|
mut deps: DepsMut<'_>,
|
|
identity: &str,
|
|
n: usize,
|
|
) {
|
|
for i in 0..n {
|
|
add_unbonded_mixnode(
|
|
&mut rng,
|
|
deps.branch(),
|
|
Some(identity),
|
|
&format!("owner{}", i),
|
|
);
|
|
}
|
|
}
|
|
|
|
// same note as with `add_mixnode`
|
|
pub fn add_unbonded_mixnode(
|
|
mut rng: impl RngCore + CryptoRng,
|
|
deps: DepsMut<'_>,
|
|
identity_key: Option<&str>,
|
|
owner: &str,
|
|
) -> NodeId {
|
|
let id = loop {
|
|
let candidate = rng.next_u32();
|
|
if !mixnodes_storage::unbonded_mixnodes().has(deps.storage, candidate) {
|
|
break candidate;
|
|
}
|
|
};
|
|
|
|
// we don't care about 'correctness' of the identity key here
|
|
mixnodes_storage::unbonded_mixnodes()
|
|
.save(
|
|
deps.storage,
|
|
id,
|
|
&UnbondedMixnode {
|
|
identity_key: identity_key
|
|
.unwrap_or(&*format!("identity{}", id))
|
|
.to_string(),
|
|
owner: Addr::unchecked(owner),
|
|
proxy: None,
|
|
unbonding_height: 12345,
|
|
},
|
|
)
|
|
.unwrap();
|
|
|
|
id
|
|
}
|
|
|
|
pub fn nymnode_bonding_sign_payload(
|
|
deps: Deps<'_>,
|
|
owner: &str,
|
|
node: NymNode,
|
|
stake: Vec<Coin>,
|
|
) -> SignableNymNodeBondingMsg {
|
|
let cost_params = tests::fixtures::node_cost_params_fixture();
|
|
let nonce =
|
|
signing_storage::get_signing_nonce(deps.storage, Addr::unchecked(owner)).unwrap();
|
|
|
|
let payload = NymNodeBondingPayload::new(node, cost_params);
|
|
let content = ContractMessageContent::new(Addr::unchecked(owner), stake, payload);
|
|
SignableNymNodeBondingMsg::new(nonce, content)
|
|
}
|
|
|
|
pub fn mixnode_bonding_sign_payload(
|
|
deps: Deps<'_>,
|
|
owner: &str,
|
|
mixnode: MixNode,
|
|
stake: Vec<Coin>,
|
|
) -> SignableMixNodeBondingMsg {
|
|
let cost_params = tests::fixtures::node_cost_params_fixture();
|
|
let nonce =
|
|
signing_storage::get_signing_nonce(deps.storage, Addr::unchecked(owner)).unwrap();
|
|
|
|
let payload = MixnodeBondingPayload::new(mixnode, cost_params);
|
|
let content = ContractMessageContent::new(Addr::unchecked(owner), stake, payload);
|
|
SignableMixNodeBondingMsg::new(nonce, content)
|
|
}
|
|
|
|
pub fn gateway_bonding_sign_payload(
|
|
deps: Deps<'_>,
|
|
owner: &str,
|
|
gateway: Gateway,
|
|
stake: Vec<Coin>,
|
|
) -> SignableGatewayBondingMsg {
|
|
let nonce =
|
|
signing_storage::get_signing_nonce(deps.storage, Addr::unchecked(owner)).unwrap();
|
|
|
|
let payload = GatewayBondingPayload::new(gateway);
|
|
let content = ContractMessageContent::new(Addr::unchecked(owner), stake, payload);
|
|
SignableGatewayBondingMsg::new(nonce, content)
|
|
}
|
|
|
|
fn intial_rewarded_set_params() -> RewardedSetParams {
|
|
RewardedSetParams {
|
|
entry_gateways: 50,
|
|
exit_gateways: 70,
|
|
mixnodes: 120,
|
|
standby: 50,
|
|
}
|
|
}
|
|
|
|
fn initial_rewarding_params() -> InitialRewardingParams {
|
|
let reward_pool = 250_000_000_000_000u128;
|
|
let staking_supply = 100_000_000_000_000u128;
|
|
|
|
InitialRewardingParams {
|
|
initial_reward_pool: Decimal::from_atomics(reward_pool, 0).unwrap(), // 250M * 1M (we're expressing it all in base tokens)
|
|
initial_staking_supply: Decimal::from_atomics(staking_supply, 0).unwrap(), // 100M * 1M
|
|
staking_supply_scale_factor: Percent::hundred(),
|
|
sybil_resistance: Percent::from_percentage_value(30).unwrap(),
|
|
active_set_work_factor: Decimal::from_atomics(10u32, 0).unwrap(),
|
|
interval_pool_emission: Percent::from_percentage_value(2).unwrap(),
|
|
rewarded_set_params: intial_rewarded_set_params(),
|
|
}
|
|
}
|
|
|
|
pub fn init_contract() -> OwnedDeps<MemoryStorage, MockApi, MockQuerier<Empty>> {
|
|
let mut deps = mock_dependencies();
|
|
let msg = InstantiateMsg {
|
|
rewarding_validator_address: "rewarder".into(),
|
|
vesting_contract_address: "vesting-contract".to_string(),
|
|
rewarding_denom: TEST_COIN_DENOM.to_string(),
|
|
epochs_in_interval: 720,
|
|
epoch_duration: Duration::from_secs(60 * 60),
|
|
initial_rewarding_params: initial_rewarding_params(),
|
|
current_nym_node_version: "1.1.10".to_string(),
|
|
version_score_weights: Default::default(),
|
|
version_score_params: Default::default(),
|
|
profit_margin: Default::default(),
|
|
interval_operating_cost: Default::default(),
|
|
};
|
|
let env = mock_env();
|
|
let info = mock_info("creator", &[]);
|
|
instantiate(deps.as_mut(), env, info, msg).unwrap();
|
|
deps
|
|
}
|
|
|
|
pub fn delegate(deps: DepsMut<'_>, env: Env, sender: &str, stake: Vec<Coin>, mix_id: NodeId) {
|
|
let info = mock_info(sender, &stake);
|
|
try_delegate_to_node(deps, env, info, mix_id).unwrap();
|
|
}
|
|
|
|
pub(crate) fn read_delegation(
|
|
storage: &dyn Storage,
|
|
mix: NodeId,
|
|
owner: &Addr,
|
|
proxy: &Option<Addr>,
|
|
) -> Option<Delegation> {
|
|
delegations_storage::delegations()
|
|
.may_load(
|
|
storage,
|
|
Delegation::generate_storage_key(mix, owner, proxy.as_ref()),
|
|
)
|
|
.unwrap()
|
|
}
|
|
}
|