diff --git a/common/client-libs/validator-client/src/nymd/fee_helpers.rs b/common/client-libs/validator-client/src/nymd/fee_helpers.rs index f63daf83b4..95b505704f 100644 --- a/common/client-libs/validator-client/src/nymd/fee_helpers.rs +++ b/common/client-libs/validator-client/src/nymd/fee_helpers.rs @@ -23,10 +23,11 @@ pub enum Operation { BondGateway, UnbondGateway, - DelegateToGateway, - UndelegateFromGateway, UpdateStateParams, + + BeginMixnodeRewarding, + FinishMixnodeRewarding, } pub(crate) fn calculate_fee(gas_price: &GasPrice, gas_limit: Gas) -> Coin { @@ -43,13 +44,13 @@ impl fmt::Display for Operation { Operation::Send => f.write_str("Send"), Operation::BondMixnode => f.write_str("BondMixnode"), Operation::UnbondMixnode => f.write_str("UnbondMixnode"), - Operation::DelegateToMixnode => f.write_str("DelegateToMixnode"), - Operation::UndelegateFromMixnode => f.write_str("UndelegateFromMixnode"), Operation::BondGateway => f.write_str("BondGateway"), Operation::UnbondGateway => f.write_str("UnbondGateway"), - Operation::DelegateToGateway => f.write_str("DelegateToGateway"), - Operation::UndelegateFromGateway => f.write_str("UndelegateFromGateway"), + Operation::DelegateToMixnode => f.write_str("DelegateToMixnode"), + Operation::UndelegateFromMixnode => f.write_str("UndelegateFromMixnode"), Operation::UpdateStateParams => f.write_str("UpdateStateParams"), + Operation::BeginMixnodeRewarding => f.write_str("BeginMixnodeRewarding"), + Operation::FinishMixnodeRewarding => f.write_str("FinishMixnodeRewarding"), } } } @@ -71,10 +72,10 @@ impl Operation { Operation::BondGateway => 175_000u64.into(), Operation::UnbondGateway => 175_000u64.into(), - Operation::DelegateToGateway => 175_000u64.into(), - Operation::UndelegateFromGateway => 175_000u64.into(), Operation::UpdateStateParams => 175_000u64.into(), + Operation::BeginMixnodeRewarding => 175_000u64.into(), + Operation::FinishMixnodeRewarding => 175_000u64.into(), } } diff --git a/common/client-libs/validator-client/src/nymd/mod.rs b/common/client-libs/validator-client/src/nymd/mod.rs index 074ad80d9d..815076a664 100644 --- a/common/client-libs/validator-client/src/nymd/mod.rs +++ b/common/client-libs/validator-client/src/nymd/mod.rs @@ -711,6 +711,54 @@ impl NymdClient { ) .await } + + pub async fn begin_mixnode_rewarding( + &self, + rewarding_interval_nonce: u32, + ) -> Result + where + C: SigningCosmWasmClient + Sync, + { + let fee = self.get_fee(Operation::BeginMixnodeRewarding); + + let req = ExecuteMsg::BeginMixnodeRewarding { + rewarding_interval_nonce, + }; + self.client + .execute( + self.address(), + self.contract_address()?, + &req, + fee, + "Beginning mixnode rewarding procedure", + Vec::new(), + ) + .await + } + + pub async fn finish_mixnode_rewarding( + &self, + rewarding_interval_nonce: u32, + ) -> Result + where + C: SigningCosmWasmClient + Sync, + { + let fee = self.get_fee(Operation::FinishMixnodeRewarding); + + let req = ExecuteMsg::FinishMixnodeRewarding { + rewarding_interval_nonce, + }; + self.client + .execute( + self.address(), + self.contract_address()?, + &req, + fee, + "Finishing mixnode rewarding procedure", + Vec::new(), + ) + .await + } } fn cosmwasm_coin_to_cosmos_coin(coin: Coin) -> CosmosCoin { diff --git a/common/mixnet-contract/src/msg.rs b/common/mixnet-contract/src/msg.rs index f60ee411f1..e7c4f4e643 100644 --- a/common/mixnet-contract/src/msg.rs +++ b/common/mixnet-contract/src/msg.rs @@ -32,16 +32,32 @@ pub enum ExecuteMsg { mix_identity: IdentityKey, }, + BeginMixnodeRewarding { + // nonce of the current rewarding interval + rewarding_interval_nonce: u32, + }, + RewardMixnode { identity: IdentityKey, // percentage value in range 0-100 uptime: u32, + + // nonce of the current rewarding interval + rewarding_interval_nonce: u32, + }, + + FinishMixnodeRewarding { + // nonce of the current rewarding interval + rewarding_interval_nonce: u32, }, RewardMixnodeV2 { identity: IdentityKey, // percentage value in range 0-100 params: NodeRewardParams, + + // nonce of the current rewarding interval + rewarding_interval_nonce: u32, }, } diff --git a/common/mixnet-contract/src/types.rs b/common/mixnet-contract/src/types.rs index 447bfc710c..ee6ad94477 100644 --- a/common/mixnet-contract/src/types.rs +++ b/common/mixnet-contract/src/types.rs @@ -29,6 +29,7 @@ impl LayerDistribution { #[derive(Debug, Default, Serialize, Deserialize, Copy, Clone, Eq, PartialEq)] pub struct RewardingIntervalResponse { pub current_rewarding_interval_starting_block: u64, + pub current_rewarding_interval_nonce: u32, pub rewarding_in_progress: bool, } diff --git a/common/network-defaults/src/lib.rs b/common/network-defaults/src/lib.rs index 572ccadcec..2e02299ce2 100644 --- a/common/network-defaults/src/lib.rs +++ b/common/network-defaults/src/lib.rs @@ -63,7 +63,7 @@ pub fn default_api_endpoints() -> Vec { } pub const DEFAULT_MIXNET_CONTRACT_ADDRESS: &str = "punk10pyejy66429refv3g35g2t7am0was7yalwrzen"; -pub const NETWORK_MONITOR_ADDRESS: &str = "punk1v9qauwdq5terag6uvfsdytcs2d0sdmfdy7hgk3"; +pub const REWARDING_VALIDATOR_ADDRESS: &str = "punk1v9qauwdq5terag6uvfsdytcs2d0sdmfdy7hgk3"; /// How much bandwidth (in bytes) one token can buy const BYTES_PER_TOKEN: u64 = 1024 * 1024 * 1024; diff --git a/contracts/mixnet/src/contract.rs b/contracts/mixnet/src/contract.rs index 7b9862aff2..42ad4d4244 100644 --- a/contracts/mixnet/src/contract.rs +++ b/contracts/mixnet/src/contract.rs @@ -7,7 +7,7 @@ use crate::helpers::calculate_epoch_reward_rate; use crate::state::State; use crate::storage::{config, layer_distribution}; use crate::{error::ContractError, queries, transactions}; -use config::defaults::NETWORK_MONITOR_ADDRESS; +use config::defaults::REWARDING_VALIDATOR_ADDRESS; use cosmwasm_std::{ entry_point, to_binary, Addr, Decimal, Deps, DepsMut, Env, MessageInfo, QueryResponse, Response, Uint128, @@ -42,7 +42,7 @@ fn default_initial_state(owner: Addr, env: Env) -> State { State { owner, - network_monitor_address: Addr::unchecked(NETWORK_MONITOR_ADDRESS), // we trust our hardcoded value + rewarding_validator_address: Addr::unchecked(REWARDING_VALIDATOR_ADDRESS), // we trust our hardcoded value params: StateParams { epoch_length: INITIAL_DEFAULT_EPOCH_LENGTH, minimum_mixnode_bond: INITIAL_MIXNODE_BOND, @@ -53,6 +53,7 @@ fn default_initial_state(owner: Addr, env: Env) -> State { mixnode_active_set_size: INITIAL_MIXNODE_ACTIVE_SET_SIZE, }, rewarding_interval_starting_block: env.block.height, + latest_rewarding_interval_nonce: 0, rewarding_in_progress: false, mixnode_epoch_bond_reward: calculate_epoch_reward_rate( INITIAL_DEFAULT_EPOCH_LENGTH, @@ -104,18 +105,42 @@ pub fn execute( ExecuteMsg::UpdateStateParams(params) => { transactions::try_update_state_params(deps, info, params) } - ExecuteMsg::RewardMixnode { identity, uptime } => { - transactions::try_reward_mixnode(deps, env, info, identity, uptime) - } - ExecuteMsg::RewardMixnodeV2 { identity, params } => { - transactions::try_reward_mixnode_v2(deps, env, info, identity, params) - } + ExecuteMsg::RewardMixnode { + identity, + uptime, + rewarding_interval_nonce, + } => transactions::try_reward_mixnode( + deps, + env, + info, + identity, + uptime, + rewarding_interval_nonce, + ), + ExecuteMsg::RewardMixnodeV2 { + identity, + params, + rewarding_interval_nonce, + } => transactions::try_reward_mixnode_v2( + deps, + env, + info, + identity, + params, + rewarding_interval_nonce, + ), ExecuteMsg::DelegateToMixnode { mix_identity } => { transactions::try_delegate_to_mixnode(deps, env, info, mix_identity) } ExecuteMsg::UndelegateFromMixnode { mix_identity } => { transactions::try_remove_delegation_from_mixnode(deps, info, mix_identity) } + ExecuteMsg::BeginMixnodeRewarding { + rewarding_interval_nonce, + } => transactions::try_begin_mixnode_rewarding(deps, env, info, rewarding_interval_nonce), + ExecuteMsg::FinishMixnodeRewarding { + rewarding_interval_nonce, + } => transactions::try_finish_mixnode_rewarding(deps, info, rewarding_interval_nonce), } } diff --git a/contracts/mixnet/src/error.rs b/contracts/mixnet/src/error.rs index 18b7289664..027f697407 100644 --- a/contracts/mixnet/src/error.rs +++ b/contracts/mixnet/src/error.rs @@ -88,4 +88,16 @@ pub enum ContractError { #[error("Invalid ratio")] Ratio(#[from] mixnet_contract::error::MixnetContractError), + + #[error("Received invalid rewarding interval nonce. Expected {expected}, received {received}")] + InvalidRewardingIntervalNonce { received: u32, expected: u32 }, + + #[error("Rewarding distribution is currently in progress")] + RewardingInProgress, + + #[error("Rewarding distribution is currently not in progress")] + RewardingNotInProgress, + + #[error("Mixnode {identity} has already been rewarded during the current rewarding interval")] + MixnodeAlreadyRewarded { identity: IdentityKey }, } diff --git a/contracts/mixnet/src/queries.rs b/contracts/mixnet/src/queries.rs index 1d63b87702..e6f0596a44 100644 --- a/contracts/mixnet/src/queries.rs +++ b/contracts/mixnet/src/queries.rs @@ -91,6 +91,7 @@ pub(crate) fn query_rewarding_interval(deps: Deps) -> RewardingIntervalResponse let state = config_read(deps.storage).load().unwrap(); RewardingIntervalResponse { current_rewarding_interval_starting_block: state.rewarding_interval_starting_block, + current_rewarding_interval_nonce: state.latest_rewarding_interval_nonce, rewarding_in_progress: state.rewarding_in_progress, } } @@ -578,7 +579,7 @@ pub(crate) mod tests { let dummy_state = State { owner: Addr::unchecked("someowner"), - network_monitor_address: Addr::unchecked("monitor"), + rewarding_validator_address: Addr::unchecked("monitor"), params: StateParams { epoch_length: 1, minimum_mixnode_bond: 123u128.into(), @@ -589,6 +590,7 @@ pub(crate) mod tests { mixnode_active_set_size: 500, }, rewarding_interval_starting_block: 123, + latest_rewarding_interval_nonce: 0, rewarding_in_progress: false, mixnode_epoch_bond_reward: "1.23".parse().unwrap(), mixnode_epoch_delegation_reward: "7.89".parse().unwrap(), diff --git a/contracts/mixnet/src/state.rs b/contracts/mixnet/src/state.rs index c7cbea5848..4440c776eb 100644 --- a/contracts/mixnet/src/state.rs +++ b/contracts/mixnet/src/state.rs @@ -9,13 +9,14 @@ use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] pub struct State { pub owner: Addr, // only the owner account can update state - pub network_monitor_address: Addr, + pub rewarding_validator_address: Addr, pub params: StateParams, // keep track of the changes to the current rewarding interval, // i.e. at which block has the latest rewarding occurred // and whether another run is already in progress pub rewarding_interval_starting_block: u64, + pub latest_rewarding_interval_nonce: u32, pub rewarding_in_progress: bool, // helper values to avoid having to recalculate them on every single payment operation diff --git a/contracts/mixnet/src/storage.rs b/contracts/mixnet/src/storage.rs index c95e909a03..ec690dce16 100644 --- a/contracts/mixnet/src/storage.rs +++ b/contracts/mixnet/src/storage.rs @@ -37,6 +37,8 @@ const PREFIX_GATEWAYS_OWNERS: &[u8] = b"go"; const PREFIX_MIX_DELEGATION: &[u8] = b"md"; const PREFIX_REVERSE_MIX_DELEGATION: &[u8] = b"dm"; +const PREFIX_REWARDED_MIXNODES: &[u8] = b"rm"; + // Contract-level stuff // TODO Unify bucket and mixnode storage functions @@ -103,22 +105,6 @@ pub(crate) fn read_state_params(storage: &dyn Storage) -> StateParams { config_read(storage).load().unwrap().params } -pub(crate) fn read_mixnode_epoch_bond_reward_rate(storage: &dyn Storage) -> Decimal { - // same justification as in `read_state_params` for the unwrap - config_read(storage) - .load() - .unwrap() - .mixnode_epoch_bond_reward -} - -pub(crate) fn read_mixnode_epoch_delegation_reward_rate(storage: &dyn Storage) -> Decimal { - // same justification as in `read_state_params` for the unwrap - config_read(storage) - .load() - .unwrap() - .mixnode_epoch_delegation_reward -} - pub fn layer_distribution(storage: &mut dyn Storage) -> Singleton { singleton(storage, LAYER_DISTRIBUTION_KEY) } @@ -196,6 +182,35 @@ pub fn mixnodes_owners_read(storage: &dyn Storage) -> ReadonlyBucket Bucket { + Bucket::multilevel( + storage, + &[ + rewarding_interval_nonce.to_be_bytes().as_ref(), + PREFIX_REWARDED_MIXNODES, + ], + ) +} + +// we want to treat this bucket as a set so we don't really care about what type of data is being stored. +// I went with u8 as after serialization it takes only a single byte of space, while if a `()` was used, +// it would have taken 4 bytes (representation of 'null') +pub fn rewarded_mixnodes_read( + storage: &dyn Storage, + rewarding_interval_nonce: u32, +) -> ReadonlyBucket { + ReadonlyBucket::multilevel( + storage, + &[ + rewarding_interval_nonce.to_be_bytes().as_ref(), + PREFIX_REWARDED_MIXNODES, + ], + ) +} + // helpers pub(crate) fn increase_mix_delegated_stakes( storage: &mut dyn Storage, diff --git a/contracts/mixnet/src/transactions.rs b/contracts/mixnet/src/transactions.rs index b91b9af25b..de5e3f5f07 100644 --- a/contracts/mixnet/src/transactions.rs +++ b/contracts/mixnet/src/transactions.rs @@ -15,8 +15,13 @@ use mixnet_contract::{ }; pub(crate) const OLD_DELEGATIONS_CHUNK_SIZE: usize = 500; + +// approximately 1 day (assuming 5s per block) pub(crate) const MINIMUM_BLOCK_AGE_FOR_REWARDING: u64 = 17280; +// approximately 30min (assuming 5s per block) +pub(crate) const MAX_REWARDING_DURATION_IN_BLOCKS: u64 = 360; + fn total_delegations(delegations_bucket: ReadonlyBucket) -> StdResult { Ok(Coin::new( Delegations::new(delegations_bucket) @@ -300,7 +305,7 @@ pub(crate) fn try_update_state_params( // note: In any other case, I wouldn't have attempted to unwrap this result, but in here // if we fail to load the stored state we would already be in the undefined behaviour land, // so we better just blow up immediately. - let mut state = config_read(deps.storage).load().unwrap(); + let mut state = config_read(deps.storage).load()?; // check if this is executed by the owner, if not reject the transaction if info.sender != state.owner { @@ -348,6 +353,57 @@ pub(crate) fn try_update_state_params( Ok(Response::default()) } +// Note: this function is designed to work with only a single validator entity distributing rewards +// The main purpose of this function is to update `latest_rewarding_interval_nonce` which +// will trigger a different seed selection for the pseudorandom generation of the "demanded" set of mixnodes. +pub(crate) fn try_begin_mixnode_rewarding( + deps: DepsMut, + env: Env, + info: MessageInfo, + rewarding_interval_nonce: u32, +) -> Result { + let mut state = config_read(deps.storage).load()?; + + // check if this is executed by the permitted validator, if not reject the transaction + if info.sender != state.rewarding_validator_address { + return Err(ContractError::Unauthorized); + } + + // check whether sufficient number of blocks already elapsed since the previous rewarding happened + // (this implies the validator responsible for rewarding in the previous interval did not call + // `try_finish_mixnode_rewarding` - perhaps they crashed or something. Regardless of the reason + // it shouldn't prevent anyone from distributing rewards in the following interval) + // Do note, however, that calling `try_finish_mixnode_rewarding` is crucial as otherwise the + // "demanded" set won't get updated on the validator API side + if state.rewarding_in_progress + && state.rewarding_interval_starting_block + MAX_REWARDING_DURATION_IN_BLOCKS + > env.block.height + { + return Err(ContractError::RewardingInProgress); + } + + // make sure the validator is in sync with the contract state + if rewarding_interval_nonce != state.latest_rewarding_interval_nonce + 1 { + return Err(ContractError::InvalidRewardingIntervalNonce { + received: rewarding_interval_nonce, + expected: state.latest_rewarding_interval_nonce + 1, + }); + } + + state.rewarding_interval_starting_block = env.block.height; + state.latest_rewarding_interval_nonce = rewarding_interval_nonce; + state.rewarding_in_progress = true; + + config(deps.storage).save(&state)?; + + let mut response = Response::new(); + response.add_attribute( + "rewarding interval nonce", + rewarding_interval_nonce.to_string(), + ); + Ok(response) +} + // Note: if any changes are made to this function or anything it is calling down the stack, // for example delegation reward distribution, the gas limits must be retested and both // validator-api/src/rewarding/mod.rs::{MIXNODE_REWARD_OP_BASE_GAS_LIMIT, PER_MIXNODE_DELEGATION_GAS_INCREASE} @@ -358,16 +414,41 @@ pub(crate) fn try_reward_mixnode( info: MessageInfo, mix_identity: IdentityKey, uptime: u32, + rewarding_interval_nonce: u32, ) -> Result { - let state = config_read(deps.storage).load().unwrap(); + let state = config_read(deps.storage).load()?; - // check if this is executed by the monitor, if not reject the transaction - if info.sender != state.network_monitor_address { + // check if this is executed by the permitted validator, if not reject the transaction + if info.sender != state.rewarding_validator_address { return Err(ContractError::Unauthorized); } + if !state.rewarding_in_progress { + return Err(ContractError::RewardingNotInProgress); + } + + // make sure the transaction is sent for the correct rewarding interval + if rewarding_interval_nonce != state.latest_rewarding_interval_nonce { + return Err(ContractError::InvalidRewardingIntervalNonce { + received: rewarding_interval_nonce, + expected: state.latest_rewarding_interval_nonce, + }); + } + + // check if the mixnode hasn't been rewarded in this rewarding interval already + if rewarded_mixnodes_read(deps.storage, rewarding_interval_nonce) + .may_load(mix_identity.as_bytes())? + .is_some() + { + return Err(ContractError::MixnodeAlreadyRewarded { + identity: mix_identity, + }); + } + // optimisation for uptime being 0. No rewards will be given so just terminate here if uptime == 0 { + rewarded_mixnodes(deps.storage, rewarding_interval_nonce) + .save(mix_identity.as_bytes(), &Default::default())?; return Ok(Response { submessages: vec![], messages: vec![], @@ -390,28 +471,33 @@ pub(crate) fn try_reward_mixnode( } }; - let bond_reward_rate = read_mixnode_epoch_bond_reward_rate(deps.storage); - let delegation_reward_rate = read_mixnode_epoch_delegation_reward_rate(deps.storage); - let bond_scaled_reward_rate = scale_reward_by_uptime(bond_reward_rate, uptime)?; - let delegation_scaled_reward_rate = scale_reward_by_uptime(delegation_reward_rate, uptime)?; - let mut node_reward = Uint128(0); - let total_delegation_reward = increase_mix_delegated_stakes( - deps.storage, - &mix_identity, - delegation_scaled_reward_rate, - env.block.height, - )?; + let mut total_delegation_reward = Uint128(0); // update current bond with the reward given to the node and the delegators // if it has been bonded for long enough if current_bond.block_height + MINIMUM_BLOCK_AGE_FOR_REWARDING <= env.block.height { + let bond_reward_rate = state.mixnode_epoch_bond_reward; + let delegation_reward_rate = state.mixnode_epoch_delegation_reward; + let bond_scaled_reward_rate = scale_reward_by_uptime(bond_reward_rate, uptime)?; + let delegation_scaled_reward_rate = scale_reward_by_uptime(delegation_reward_rate, uptime)?; + + total_delegation_reward = increase_mix_delegated_stakes( + deps.storage, + &mix_identity, + delegation_scaled_reward_rate, + env.block.height, + )?; + node_reward = current_bond.bond_amount.amount * bond_scaled_reward_rate; current_bond.bond_amount.amount += node_reward; current_bond.total_delegation.amount += total_delegation_reward; mixnodes(deps.storage).save(mix_identity.as_bytes(), ¤t_bond)?; } + rewarded_mixnodes(deps.storage, rewarding_interval_nonce) + .save(mix_identity.as_bytes(), &Default::default())?; + Ok(Response { submessages: vec![], messages: vec![], @@ -429,14 +515,37 @@ pub(crate) fn try_reward_mixnode_v2( info: MessageInfo, mix_identity: IdentityKey, params: NodeRewardParams, + rewarding_interval_nonce: u32, ) -> Result { - let state = config_read(deps.storage).load().unwrap(); + let state = config_read(deps.storage).load()?; - // check if this is executed by the monitor, if not reject the transaction - if info.sender != state.network_monitor_address { + // check if this is executed by the permitted validator, if not reject the transaction + if info.sender != state.rewarding_validator_address { return Err(ContractError::Unauthorized); } + if !state.rewarding_in_progress { + return Err(ContractError::RewardingNotInProgress); + } + + // make sure the transaction is sent for the correct rewarding interval + if rewarding_interval_nonce != state.latest_rewarding_interval_nonce { + return Err(ContractError::InvalidRewardingIntervalNonce { + received: rewarding_interval_nonce, + expected: state.latest_rewarding_interval_nonce, + }); + } + + // check if the mixnode hasn't been rewarded in this rewarding interval already + if rewarded_mixnodes_read(deps.storage, rewarding_interval_nonce) + .may_load(mix_identity.as_bytes())? + .is_some() + { + return Err(ContractError::MixnodeAlreadyRewarded { + identity: mix_identity, + }); + } + // check if the bond even exists let mut current_bond = match mixnodes_read(deps.storage).load(mix_identity.as_bytes()) { Ok(bond) => bond, @@ -472,6 +581,9 @@ pub(crate) fn try_reward_mixnode_v2( decr_reward_pool(total_delegation_reward, deps.storage)?; } + rewarded_mixnodes(deps.storage, rewarding_interval_nonce) + .save(mix_identity.as_bytes(), &Default::default())?; + Ok(Response { submessages: vec![], messages: vec![], @@ -482,6 +594,36 @@ pub(crate) fn try_reward_mixnode_v2( data: None, }) } +pub(crate) fn try_finish_mixnode_rewarding( + deps: DepsMut, + info: MessageInfo, + rewarding_interval_nonce: u32, +) -> Result { + let mut state = config_read(deps.storage).load()?; + + // check if this is executed by the permitted validator, if not reject the transaction + if info.sender != state.rewarding_validator_address { + return Err(ContractError::Unauthorized); + } + + if !state.rewarding_in_progress { + return Err(ContractError::RewardingNotInProgress); + } + + // make sure the validator is in sync with the contract state + if rewarding_interval_nonce != state.latest_rewarding_interval_nonce { + return Err(ContractError::InvalidRewardingIntervalNonce { + received: rewarding_interval_nonce, + expected: state.latest_rewarding_interval_nonce, + }); + } + + state.rewarding_in_progress = false; + config(deps.storage).save(&state)?; + + Ok(Response::new()) +} + fn validate_delegation_stake(delegation: &[Coin]) -> Result<(), ContractError> { // check if anything was put as delegation if delegation.is_empty() { @@ -515,8 +657,7 @@ pub(crate) fn try_delegate_to_mixnode( validate_delegation_stake(&info.funds)?; // check if the target node actually exists - let mut mixnodes_bucket = mixnodes(deps.storage); - let mut current_bond = match mixnodes_bucket.load(mix_identity.as_bytes()) { + let mut current_bond = match mixnodes_read(deps.storage).load(mix_identity.as_bytes()) { Ok(bond) => bond, Err(_) => { return Err(ContractError::MixNodeBondNotFound { @@ -528,8 +669,8 @@ pub(crate) fn try_delegate_to_mixnode( let amount = info.funds[0].amount; // update total_delegation of this node - current_bond.total_delegation.amount += amount; - mixnodes_bucket.save(mix_identity.as_bytes(), ¤t_bond)?; + current_bond.total_delegation.amount += info.funds[0].amount; + mixnodes(deps.storage).save(mix_identity.as_bytes(), ¤t_bond)?; let mut delegation_bucket = mix_delegations(deps.storage, &mix_identity); let sender_bytes = info.sender.as_bytes(); @@ -1485,10 +1626,8 @@ pub mod tests { assert_eq!(current_state.params, new_params); // mixnode_epoch_rewards are recalculated if annual reward is changed - let current_mix_bond_reward_rate = - read_mixnode_epoch_bond_reward_rate(deps.as_ref().storage); - let current_mix_delegation_reward_rate = - read_mixnode_epoch_delegation_reward_rate(deps.as_ref().storage); + let current_mix_bond_reward_rate = current_state.mixnode_epoch_bond_reward; + let current_mix_delegation_reward_rate = current_state.mixnode_epoch_delegation_reward; let new_mixnode_bond_reward_rate = Decimal::percent(120); let new_mixnode_delegation_reward_rate = Decimal::percent(120); @@ -1550,25 +1689,350 @@ pub mod tests { assert_eq!(Err(ContractError::InvalidActiveSetSize), res) } + #[cfg(test)] + mod beginning_mixnode_rewarding { + use super::*; + + #[test] + fn can_only_be_called_by_specified_validator_address() { + let mut deps = helpers::init_contract(); + let env = mock_env(); + let current_state = config_read(deps.as_mut().storage).load().unwrap(); + let rewarding_validator_address = current_state.rewarding_validator_address; + + let res = try_begin_mixnode_rewarding( + deps.as_mut(), + env.clone(), + mock_info("not-the-approved-validator", &[]), + 1, + ); + assert_eq!(Err(ContractError::Unauthorized), res); + + let res = try_begin_mixnode_rewarding( + deps.as_mut(), + env.clone(), + mock_info(rewarding_validator_address.as_ref(), &[]), + 1, + ); + assert!(res.is_ok()) + } + + #[test] + fn cannot_be_called_if_rewarding_is_already_in_progress_with_little_day() { + let mut deps = helpers::init_contract(); + let env = mock_env(); + let current_state = config_read(deps.as_mut().storage).load().unwrap(); + let rewarding_validator_address = current_state.rewarding_validator_address; + + try_begin_mixnode_rewarding( + deps.as_mut(), + env.clone(), + mock_info(rewarding_validator_address.as_ref(), &[]), + 1, + ) + .unwrap(); + + let res = try_begin_mixnode_rewarding( + deps.as_mut(), + env.clone(), + mock_info(rewarding_validator_address.as_ref(), &[]), + 2, + ); + assert_eq!(Err(ContractError::RewardingInProgress), res); + } + + #[test] + fn can_be_called_if_rewarding_is_in_progress_if_sufficient_number_of_blocks_elapsed() { + let mut deps = helpers::init_contract(); + let env = mock_env(); + let current_state = config_read(deps.as_mut().storage).load().unwrap(); + let rewarding_validator_address = current_state.rewarding_validator_address; + + try_begin_mixnode_rewarding( + deps.as_mut(), + env.clone(), + mock_info(rewarding_validator_address.as_ref(), &[]), + 1, + ) + .unwrap(); + + let mut new_env = env.clone(); + + new_env.block.height = env.block.height + MAX_REWARDING_DURATION_IN_BLOCKS; + + let res = try_begin_mixnode_rewarding( + deps.as_mut(), + new_env, + mock_info(rewarding_validator_address.as_ref(), &[]), + 2, + ); + assert!(res.is_ok()); + } + + #[test] + fn provided_nonce_must_be_equal_the_current_plus_one() { + let mut deps = helpers::init_contract(); + let env = mock_env(); + let mut current_state = config_read(deps.as_mut().storage).load().unwrap(); + current_state.latest_rewarding_interval_nonce = 42; + config(deps.as_mut().storage).save(¤t_state).unwrap(); + + let rewarding_validator_address = current_state.rewarding_validator_address; + + let res = try_begin_mixnode_rewarding( + deps.as_mut(), + env.clone(), + mock_info(rewarding_validator_address.as_ref(), &[]), + 11, + ); + assert_eq!( + Err(ContractError::InvalidRewardingIntervalNonce { + received: 11, + expected: 43 + }), + res + ); + + let res = try_begin_mixnode_rewarding( + deps.as_mut(), + env.clone(), + mock_info(rewarding_validator_address.as_ref(), &[]), + 44, + ); + assert_eq!( + Err(ContractError::InvalidRewardingIntervalNonce { + received: 44, + expected: 43 + }), + res + ); + + let res = try_begin_mixnode_rewarding( + deps.as_mut(), + env.clone(), + mock_info(rewarding_validator_address.as_ref(), &[]), + 42, + ); + assert_eq!( + Err(ContractError::InvalidRewardingIntervalNonce { + received: 42, + expected: 43 + }), + res + ); + + let res = try_begin_mixnode_rewarding( + deps.as_mut(), + env.clone(), + mock_info(rewarding_validator_address.as_ref(), &[]), + 43, + ); + assert!(res.is_ok()) + } + + #[test] + fn updates_contract_state() { + let mut deps = helpers::init_contract(); + let env = mock_env(); + let start_state = config_read(deps.as_mut().storage).load().unwrap(); + let rewarding_validator_address = start_state.rewarding_validator_address; + + try_begin_mixnode_rewarding( + deps.as_mut(), + env.clone(), + mock_info(rewarding_validator_address.as_ref(), &[]), + 1, + ) + .unwrap(); + + let new_state = config_read(deps.as_mut().storage).load().unwrap(); + assert!(new_state.rewarding_in_progress); + assert_eq!( + new_state.rewarding_interval_starting_block, + env.block.height + ); + assert_eq!( + start_state.latest_rewarding_interval_nonce + 1, + new_state.latest_rewarding_interval_nonce + ); + } + } + + #[cfg(test)] + mod finishing_mixnode_rewarding { + use super::*; + + #[test] + fn can_only_be_called_by_specified_validator_address() { + let mut deps = helpers::init_contract(); + let env = mock_env(); + let current_state = config_read(deps.as_mut().storage).load().unwrap(); + let rewarding_validator_address = current_state.rewarding_validator_address; + + try_begin_mixnode_rewarding( + deps.as_mut(), + env, + mock_info(rewarding_validator_address.as_ref(), &[]), + 1, + ) + .unwrap(); + + let res = try_finish_mixnode_rewarding( + deps.as_mut(), + mock_info("not-the-approved-validator", &[]), + 1, + ); + assert_eq!(Err(ContractError::Unauthorized), res); + + let res = try_finish_mixnode_rewarding( + deps.as_mut(), + mock_info(rewarding_validator_address.as_ref(), &[]), + 1, + ); + assert!(res.is_ok()) + } + + #[test] + fn cannot_be_called_if_rewarding_is_not_in_progress() { + let mut deps = helpers::init_contract(); + let current_state = config_read(deps.as_mut().storage).load().unwrap(); + let rewarding_validator_address = current_state.rewarding_validator_address; + + let res = try_finish_mixnode_rewarding( + deps.as_mut(), + mock_info(rewarding_validator_address.as_ref(), &[]), + 0, + ); + assert_eq!(Err(ContractError::RewardingNotInProgress), res); + } + + #[test] + fn provided_nonce_must_be_equal_the_current_one() { + let mut deps = helpers::init_contract(); + let env = mock_env(); + let mut current_state = config_read(deps.as_mut().storage).load().unwrap(); + current_state.latest_rewarding_interval_nonce = 42; + config(deps.as_mut().storage).save(¤t_state).unwrap(); + + let rewarding_validator_address = current_state.rewarding_validator_address; + + try_begin_mixnode_rewarding( + deps.as_mut(), + env, + mock_info(rewarding_validator_address.as_ref(), &[]), + 43, + ) + .unwrap(); + + let res = try_finish_mixnode_rewarding( + deps.as_mut(), + mock_info(rewarding_validator_address.as_ref(), &[]), + 11, + ); + assert_eq!( + Err(ContractError::InvalidRewardingIntervalNonce { + received: 11, + expected: 43 + }), + res + ); + + let res = try_finish_mixnode_rewarding( + deps.as_mut(), + mock_info(rewarding_validator_address.as_ref(), &[]), + 44, + ); + assert_eq!( + Err(ContractError::InvalidRewardingIntervalNonce { + received: 44, + expected: 43 + }), + res + ); + + let res = try_finish_mixnode_rewarding( + deps.as_mut(), + mock_info(rewarding_validator_address.as_ref(), &[]), + 42, + ); + assert_eq!( + Err(ContractError::InvalidRewardingIntervalNonce { + received: 42, + expected: 43 + }), + res + ); + + let res = try_finish_mixnode_rewarding( + deps.as_mut(), + mock_info(rewarding_validator_address.as_ref(), &[]), + 43, + ); + assert!(res.is_ok()) + } + + #[test] + fn updates_contract_state() { + let mut deps = helpers::init_contract(); + let env = mock_env(); + let current_state = config_read(deps.as_mut().storage).load().unwrap(); + let rewarding_validator_address = current_state.rewarding_validator_address; + + try_begin_mixnode_rewarding( + deps.as_mut(), + env, + mock_info(rewarding_validator_address.as_ref(), &[]), + 1, + ) + .unwrap(); + + try_finish_mixnode_rewarding( + deps.as_mut(), + mock_info(rewarding_validator_address.as_ref(), &[]), + 1, + ) + .unwrap(); + + let new_state = config_read(deps.as_mut().storage).load().unwrap(); + assert!(!new_state.rewarding_in_progress); + } + } + #[test] fn rewarding_mixnode() { let mut deps = helpers::init_contract(); let mut env = mock_env(); - let current_state = config(deps.as_mut().storage).load().unwrap(); - let network_monitor_address = current_state.network_monitor_address; + let current_state = config_read(deps.as_mut().storage).load().unwrap(); + let rewarding_validator_address = current_state.rewarding_validator_address; let node_owner: Addr = Addr::unchecked("node-owner"); let node_identity: IdentityKey = "nodeidentity".into(); // errors out if executed by somebody else than network monitor let info = mock_info("not-the-monitor", &[]); - let res = try_reward_mixnode(deps.as_mut(), env.clone(), info, node_identity.clone(), 100); + let res = try_reward_mixnode( + deps.as_mut(), + env.clone(), + info, + node_identity.clone(), + 100, + 1, + ); assert_eq!(res, Err(ContractError::Unauthorized)); + // begin rewarding period + let info = mock_info(rewarding_validator_address.as_ref(), &[]); + try_begin_mixnode_rewarding(deps.as_mut(), env.clone(), info.clone(), 1).unwrap(); // returns bond not found attribute if the target owner hasn't bonded any mixnodes - let info = mock_info(network_monitor_address.as_ref(), &[]); - let res = try_reward_mixnode(deps.as_mut(), env.clone(), info, node_identity.clone(), 100) - .unwrap(); + let res = try_reward_mixnode( + deps.as_mut(), + env.clone(), + info, + node_identity.clone(), + 100, + 1, + ) + .unwrap(); assert_eq!(vec![attr("result", "bond not found")], res.attributes); let initial_bond = 100_000000; @@ -1599,9 +2063,8 @@ pub mod tests { env.block.height += 2 * MINIMUM_BLOCK_AGE_FOR_REWARDING; - let bond_reward_rate = read_mixnode_epoch_bond_reward_rate(deps.as_ref().storage); - let delegation_reward_rate = - read_mixnode_epoch_delegation_reward_rate(deps.as_ref().storage); + let bond_reward_rate = current_state.mixnode_epoch_bond_reward; + let delegation_reward_rate = current_state.mixnode_epoch_delegation_reward; let expected_bond_reward = Uint128(initial_bond) * bond_reward_rate; let expected_delegation_reward = Uint128(initial_delegation) * delegation_reward_rate; @@ -1610,9 +2073,17 @@ pub mod tests { let expected_bond = expected_bond_reward + Uint128(initial_bond); let expected_delegation = expected_delegation_reward + Uint128(initial_delegation); - let info = mock_info(network_monitor_address.as_ref(), &[]); - let res = try_reward_mixnode(deps.as_mut(), env.clone(), info, node_identity.clone(), 100) - .unwrap(); + let info = mock_info(rewarding_validator_address.as_ref(), &[]); + let res = try_reward_mixnode( + deps.as_mut(), + env.clone(), + info.clone(), + node_identity.clone(), + 100, + 1, + ) + .unwrap(); + try_finish_mixnode_rewarding(deps.as_mut(), info, 1).unwrap(); assert_eq!( expected_bond, @@ -1639,9 +2110,18 @@ pub mod tests { let expected_bond = expected_bond_reward + expected_bond; let expected_delegation = expected_delegation_reward + expected_delegation; - let info = mock_info(network_monitor_address.as_ref(), &[]); - let res = try_reward_mixnode(deps.as_mut(), env.clone(), info, node_identity.clone(), 20) - .unwrap(); + let info = mock_info(rewarding_validator_address.as_ref(), &[]); + + try_begin_mixnode_rewarding(deps.as_mut(), env.clone(), info.clone(), 2).unwrap(); + let res = try_reward_mixnode( + deps.as_mut(), + env.clone(), + info, + node_identity.clone(), + 20, + 2, + ) + .unwrap(); assert_eq!( expected_bond, @@ -1661,12 +2141,185 @@ pub mod tests { ); } + #[test] + fn rewarding_mixnodes_outside_rewarding_period() { + let mut deps = helpers::init_contract(); + let env = mock_env(); + let current_state = config_read(deps.as_mut().storage).load().unwrap(); + let rewarding_validator_address = current_state.rewarding_validator_address; + + // bond the node + let node_owner: Addr = Addr::unchecked("node-owner"); + let node_identity: IdentityKey = "nodeidentity".into(); + + try_add_mixnode( + deps.as_mut(), + env.clone(), + mock_info(node_owner.as_ref(), &good_mixnode_bond()), + MixNode { + identity_key: node_identity.to_string(), + ..helpers::mix_node_fixture() + }, + ) + .unwrap(); + + let info = mock_info(rewarding_validator_address.as_ref(), &[]); + let res = try_reward_mixnode( + deps.as_mut(), + env.clone(), + info.clone(), + node_identity.clone(), + 100, + 1, + ); + assert_eq!(Err(ContractError::RewardingNotInProgress), res); + + try_begin_mixnode_rewarding(deps.as_mut(), env.clone(), info.clone(), 1).unwrap(); + + let res = try_reward_mixnode( + deps.as_mut(), + env.clone(), + info, + node_identity.clone(), + 100, + 1, + ); + assert!(res.is_ok()) + } + + #[test] + fn rewarding_mixnodes_with_incorrect_rewarding_nonce() { + let mut deps = helpers::init_contract(); + let env = mock_env(); + let current_state = config_read(deps.as_mut().storage).load().unwrap(); + let rewarding_validator_address = current_state.rewarding_validator_address; + + // bond the node + let node_owner: Addr = Addr::unchecked("node-owner"); + let node_identity: IdentityKey = "nodeidentity".into(); + + try_add_mixnode( + deps.as_mut(), + env.clone(), + mock_info(node_owner.as_ref(), &good_mixnode_bond()), + MixNode { + identity_key: node_identity.to_string(), + ..helpers::mix_node_fixture() + }, + ) + .unwrap(); + + let info = mock_info(rewarding_validator_address.as_ref(), &[]); + try_begin_mixnode_rewarding(deps.as_mut(), env.clone(), info.clone(), 1).unwrap(); + let res = try_reward_mixnode( + deps.as_mut(), + env.clone(), + info.clone(), + node_identity.clone(), + 100, + 0, + ); + assert_eq!( + Err(ContractError::InvalidRewardingIntervalNonce { + received: 0, + expected: 1 + }), + res + ); + + let res = try_reward_mixnode( + deps.as_mut(), + env.clone(), + info.clone(), + node_identity.clone(), + 100, + 2, + ); + assert_eq!( + Err(ContractError::InvalidRewardingIntervalNonce { + received: 2, + expected: 1 + }), + res + ); + + let res = try_reward_mixnode( + deps.as_mut(), + env.clone(), + info, + node_identity.clone(), + 100, + 1, + ); + assert!(res.is_ok()) + } + + #[test] + fn attempting_rewarding_mixnode_multiple_times_per_interval() { + let mut deps = helpers::init_contract(); + let env = mock_env(); + let current_state = config_read(deps.as_mut().storage).load().unwrap(); + let rewarding_validator_address = current_state.rewarding_validator_address; + + // bond the node + let node_owner: Addr = Addr::unchecked("node-owner"); + let node_identity: IdentityKey = "nodeidentity".into(); + + try_add_mixnode( + deps.as_mut(), + env.clone(), + mock_info(node_owner.as_ref(), &good_mixnode_bond()), + MixNode { + identity_key: node_identity.to_string(), + ..helpers::mix_node_fixture() + }, + ) + .unwrap(); + + let info = mock_info(rewarding_validator_address.as_ref(), &[]); + try_begin_mixnode_rewarding(deps.as_mut(), env.clone(), info.clone(), 1).unwrap(); + + // first reward goes through just fine + let res = try_reward_mixnode( + deps.as_mut(), + env.clone(), + info.clone(), + node_identity.clone(), + 100, + 1, + ); + assert!(res.is_ok()); + + // but the other one fails + let res = try_reward_mixnode( + deps.as_mut(), + env.clone(), + info.clone(), + node_identity.clone(), + 100, + 1, + ); + assert_eq!( + Err(ContractError::MixnodeAlreadyRewarded { + identity: node_identity.clone() + }), + res + ); + + // but rewarding the same node in the following interval is fine again + try_finish_mixnode_rewarding(deps.as_mut(), info.clone(), 1).unwrap(); + try_begin_mixnode_rewarding(deps.as_mut(), env.clone(), info.clone(), 2).unwrap(); + + let res = try_reward_mixnode(deps.as_mut(), env, info, node_identity.clone(), 100, 2); + assert!(res.is_ok()); + } + #[test] fn rewarding_mixnode_blockstamp_based() { let mut deps = helpers::init_contract(); let mut env = mock_env(); - let current_state = config(deps.as_mut().storage).load().unwrap(); - let network_monitor_address = current_state.network_monitor_address; + let current_state = config_read(deps.as_mut().storage).load().unwrap(); + let rewarding_validator_address = current_state.rewarding_validator_address; let node_owner: Addr = Addr::unchecked("node-owner"); let node_identity: IdentityKey = "nodeidentity".into(); @@ -1700,9 +2353,8 @@ pub mod tests { ) .unwrap(); - let bond_reward_rate = read_mixnode_epoch_bond_reward_rate(deps.as_ref().storage); - let delegation_reward_rate = - read_mixnode_epoch_delegation_reward_rate(deps.as_ref().storage); + let bond_reward_rate = current_state.mixnode_epoch_bond_reward; + let delegation_reward_rate = current_state.mixnode_epoch_delegation_reward; let scaled_bond_reward = scale_reward_by_uptime(bond_reward_rate, 100).unwrap(); let scaled_delegation_reward = scale_reward_by_uptime(delegation_reward_rate, 100).unwrap(); @@ -1712,9 +2364,18 @@ pub mod tests { let expected_bond = expected_bond_reward + Uint128(initial_bond); let expected_delegation = expected_delegation_reward + Uint128(initial_delegation); - let info = mock_info(network_monitor_address.as_ref(), &[]); - let res = try_reward_mixnode(deps.as_mut(), env.clone(), info, node_identity.clone(), 100) - .unwrap(); + let info = mock_info(rewarding_validator_address.as_ref(), &[]); + try_begin_mixnode_rewarding(deps.as_mut(), env.clone(), info.clone(), 1).unwrap(); + let res = try_reward_mixnode( + deps.as_mut(), + env.clone(), + info.clone(), + node_identity.clone(), + 100, + 1, + ) + .unwrap(); + try_finish_mixnode_rewarding(deps.as_mut(), info, 1).unwrap(); assert_eq!( expected_bond, @@ -1740,9 +2401,18 @@ pub mod tests { let expected_bond = expected_bond_reward + expected_bond; let expected_delegation = expected_delegation_reward + expected_delegation; - let info = mock_info(network_monitor_address.as_ref(), &[]); - let res = try_reward_mixnode(deps.as_mut(), env.clone(), info, node_identity.clone(), 100) - .unwrap(); + let info = mock_info(rewarding_validator_address.as_ref(), &[]); + try_begin_mixnode_rewarding(deps.as_mut(), env.clone(), info.clone(), 2).unwrap(); + let res = try_reward_mixnode( + deps.as_mut(), + env.clone(), + info.clone(), + node_identity.clone(), + 100, + 2, + ) + .unwrap(); + try_finish_mixnode_rewarding(deps.as_mut(), info, 2).unwrap(); assert_eq!( expected_bond, @@ -1768,9 +2438,18 @@ pub mod tests { let expected_bond = expected_bond_reward + expected_bond; let expected_delegation = expected_delegation_reward + expected_delegation; - let info = mock_info(network_monitor_address.as_ref(), &[]); - let res = try_reward_mixnode(deps.as_mut(), env.clone(), info, node_identity.clone(), 100) - .unwrap(); + let info = mock_info(rewarding_validator_address.as_ref(), &[]); + try_begin_mixnode_rewarding(deps.as_mut(), env.clone(), info.clone(), 3).unwrap(); + let res = try_reward_mixnode( + deps.as_mut(), + env.clone(), + info.clone(), + node_identity.clone(), + 100, + 3, + ) + .unwrap(); + try_finish_mixnode_rewarding(deps.as_mut(), info, 3).unwrap(); assert_eq!( expected_bond, @@ -2443,8 +3122,8 @@ pub mod tests { fn delegators_on_mix_node_reward_rate() { let mut deps = helpers::init_contract(); let mut env = mock_env(); - let current_state = config(deps.as_mut().storage).load().unwrap(); - let network_monitor_address = current_state.network_monitor_address; + let current_state = config_read(deps.as_mut().storage).load().unwrap(); + let rewarding_validator_address = current_state.rewarding_validator_address; let initial_mix_bond = 100_000000; let initial_delegation1 = 50000; // will see single digits rewards @@ -2475,8 +3154,8 @@ pub mod tests { env.block.height += 2 * MINIMUM_BLOCK_AGE_FOR_REWARDING; - let bond_reward = read_mixnode_epoch_bond_reward_rate(deps.as_ref().storage); - let delegation_reward = read_mixnode_epoch_delegation_reward_rate(deps.as_ref().storage); + let bond_reward = current_state.mixnode_epoch_bond_reward; + let delegation_reward = current_state.mixnode_epoch_delegation_reward; // the node's bond and delegations are correctly increased and scaled by uptime // if node was 100% up, it will get full epoch reward @@ -2490,9 +3169,18 @@ pub mod tests { let expected_delegation2 = expected_delegation2_reward + Uint128(initial_delegation2); let expected_delegation3 = expected_delegation3_reward + Uint128(initial_delegation3); - let info = mock_info(network_monitor_address.as_ref(), &[]); - let res = - try_reward_mixnode(deps.as_mut(), env.clone(), info, identity.clone(), 100).unwrap(); + let info = mock_info(rewarding_validator_address.as_ref(), &[]); + try_begin_mixnode_rewarding(deps.as_mut(), env.clone(), info.clone(), 1).unwrap(); + let res = try_reward_mixnode( + deps.as_mut(), + env.clone(), + info.clone(), + identity.clone(), + 100, + 1, + ) + .unwrap(); + try_finish_mixnode_rewarding(deps.as_mut(), info, 1).unwrap(); assert_eq!( expected_bond, @@ -2550,9 +3238,18 @@ pub mod tests { let expected_delegation2 = expected_delegation2_reward + expected_delegation2; let expected_delegation3 = expected_delegation3_reward + expected_delegation3; - let info = mock_info(network_monitor_address.as_ref(), &[]); - let res = - try_reward_mixnode(deps.as_mut(), env.clone(), info, identity.clone(), 20).unwrap(); + let info = mock_info(rewarding_validator_address.as_ref(), &[]); + try_begin_mixnode_rewarding(deps.as_mut(), env.clone(), info.clone(), 2).unwrap(); + let res = try_reward_mixnode( + deps.as_mut(), + env.clone(), + info.clone(), + identity.clone(), + 20, + 2, + ) + .unwrap(); + try_finish_mixnode_rewarding(deps.as_mut(), info, 2).unwrap(); assert_eq!( expected_bond, @@ -2597,9 +3294,18 @@ pub mod tests { ); // if the node was 0% up, nobody will get any rewards - let info = mock_info(network_monitor_address.as_ref(), &[]); - let res = - try_reward_mixnode(deps.as_mut(), env.clone(), info, identity.clone(), 0).unwrap(); + let info = mock_info(rewarding_validator_address.as_ref(), &[]); + try_begin_mixnode_rewarding(deps.as_mut(), env.clone(), info.clone(), 3).unwrap(); + let res = try_reward_mixnode( + deps.as_mut(), + env.clone(), + info.clone(), + identity.clone(), + 0, + 3, + ) + .unwrap(); + try_finish_mixnode_rewarding(deps.as_mut(), info, 3).unwrap(); assert_eq!( expected_bond, @@ -2743,7 +3449,7 @@ pub mod tests { let mut deps = helpers::init_contract(); let mut env = mock_env(); let current_state = config(deps.as_mut().storage).load().unwrap(); - let network_monitor_address = current_state.network_monitor_address; + let rewarding_validator_address = current_state.rewarding_validator_address; let period_reward_pool = (INITIAL_REWARD_POOL / 100) * EPOCH_REWARD_PERCENT as u128; assert_eq!(period_reward_pool, 5_000_000_000_000); let k = 200; // Imagining our active set size is 200 @@ -2786,7 +3492,14 @@ pub mod tests { ) .unwrap(); - let info = mock_info(network_monitor_address.as_ref(), &[]); + let info = mock_info(rewarding_validator_address.as_ref(), &[]); + try_begin_mixnode_rewarding( + deps.as_mut(), + env.clone(), + mock_info(rewarding_validator_address.as_ref(), &[]), + 1, + ) + .unwrap(); env.block.height += 2 * MINIMUM_BLOCK_AGE_FOR_REWARDING; @@ -2836,7 +3549,7 @@ pub mod tests { .u128(); assert_eq!(pre_reward_delegation, 10000_000_000); - try_reward_mixnode_v2(deps.as_mut(), env, info, "alice".to_string(), params).unwrap(); + try_reward_mixnode_v2(deps.as_mut(), env, info, "alice".to_string(), params, 1).unwrap(); assert_eq!( read_mixnode_bond(&deps.storage, b"alice").unwrap().u128(), diff --git a/tauri-wallet/src/types/rust/operation.ts b/tauri-wallet/src/types/rust/operation.ts index f22f63b183..44c6d684c4 100644 --- a/tauri-wallet/src/types/rust/operation.ts +++ b/tauri-wallet/src/types/rust/operation.ts @@ -10,6 +10,6 @@ export type Operation = | "UndelegateFromMixnode" | "BondGateway" | "UnbondGateway" - | "DelegateToGateway" - | "UndelegateFromGateway" - | "UpdateStateParams"; \ No newline at end of file + | "UpdateStateParams" + | "BeginMixnodeRewarding" + | "FinishMixnodeRewarding"; \ No newline at end of file diff --git a/validator-api/src/cache/mod.rs b/validator-api/src/cache/mod.rs index c0e9d32c92..5bdce32952 100644 --- a/validator-api/src/cache/mod.rs +++ b/validator-api/src/cache/mod.rs @@ -162,6 +162,16 @@ impl ValidatorCache { }) } + // NOTE: this does not guarantee consistent results between multiple validator APIs, because + // currently we do not guarantee the list of mixnodes (i.e. `mixnodes: &[MixNodeBond]`) will be the same - + // somebody might bond/unbond a node or change delegation between different cache refreshes. + // + // I guess that's not a problem right now and we can resolve it later. My idea for that would be as follows: + // since the demanded set changes only monthly, just write the identities of those nodes to the smart + // contract upon finished rewarding (this works under assumption of rewards being distributed by a single validator) + // + // alternatively we could have some state locking mechanism for the duration of determining the demanded set + // this could work with multiple validators via some multisig mechanism fn determine_rewarded_set( &self, mixnodes: &[MixNodeBond], diff --git a/validator-api/src/nymd_client.rs b/validator-api/src/nymd_client.rs index 4fa3302092..3ada44491a 100644 --- a/validator-api/src/nymd_client.rs +++ b/validator-api/src/nymd_client.rs @@ -203,9 +203,42 @@ impl Client { .calculate_custom_fee(total_gas_limit) } + pub(crate) async fn begin_mixnode_rewarding( + &self, + rewarding_interval_nonce: u32, + ) -> Result<(), RewardingError> + where + C: SigningCosmWasmClient + Sync, + { + self.0 + .write() + .await + .nymd + .begin_mixnode_rewarding(rewarding_interval_nonce) + .await?; + Ok(()) + } + + pub(crate) async fn finish_mixnode_rewarding( + &self, + rewarding_interval_nonce: u32, + ) -> Result<(), RewardingError> + where + C: SigningCosmWasmClient + Sync, + { + self.0 + .write() + .await + .nymd + .finish_mixnode_rewarding(rewarding_interval_nonce) + .await?; + Ok(()) + } + pub(crate) async fn reward_mixnodes( &self, nodes: &[MixnodeToReward], + rewarding_interval_nonce: u32, ) -> Result<(), RewardingError> where C: SigningCosmWasmClient + Sync, @@ -216,7 +249,7 @@ impl Client { .await; let msgs: Vec<(ExecuteMsg, _)> = nodes .iter() - .map(Into::into) + .map(|node| node.to_execute_msg(rewarding_interval_nonce)) .zip(std::iter::repeat(Vec::new())) .collect(); diff --git a/validator-api/src/rewarding/mod.rs b/validator-api/src/rewarding/mod.rs index b18f8a50e0..f75bbb8ac5 100644 --- a/validator-api/src/rewarding/mod.rs +++ b/validator-api/src/rewarding/mod.rs @@ -55,6 +55,8 @@ pub(crate) struct MixnodeToReward { impl MixnodeToReward { /// Somewhat clumsy way of feature gatting tokenomics payments. In a tokenomics scenario this will never be None at reward time. We levarage that to Into a different ExecuteMsg variant + // TODO: to re-integrate in another PR that combines rewarded/active sets with tokenomics + #[allow(dead_code)] fn params(&self) -> Option { if cfg!(feature = "tokenomics") { self.params @@ -64,6 +66,26 @@ impl MixnodeToReward { } } +impl MixnodeToReward { + pub(crate) fn to_execute_msg(&self, rewarding_interval_nonce: u32) -> ExecuteMsg { + ExecuteMsg::RewardMixnode { + identity: self.identity.clone(), + uptime: self.uptime.u8() as u32, + rewarding_interval_nonce, + } + } + + // TODO: to re-integrate in another PR that combines rewarded/active sets with tokenomics + #[allow(dead_code)] + pub(crate) fn to_execute_msg_v2(&self, rewarding_interval_nonce: u32) -> ExecuteMsg { + ExecuteMsg::RewardMixnodeV2 { + identity: self.identity.clone(), + params: self.params().unwrap(), + rewarding_interval_nonce, + } + } +} + pub(crate) struct FailedMixnodeRewardChunkDetails { possibly_unrewarded: Vec, error_message: String, @@ -74,22 +96,6 @@ pub(crate) struct FailureData { mixnodes: Option>, } -impl<'a> From<&'a MixnodeToReward> for ExecuteMsg { - fn from(node: &MixnodeToReward) -> Self { - if let Some(params) = node.params() { - ExecuteMsg::RewardMixnodeV2 { - identity: node.identity.clone(), - params, - } - } else { - ExecuteMsg::RewardMixnode { - identity: node.identity.clone(), - uptime: node.uptime.u8() as u32, - } - } - } -} - pub(crate) struct Rewarder { nymd_client: Client, validator_cache: ValidatorCache, @@ -285,14 +291,20 @@ impl Rewarder { /// # Arguments /// /// * `eligible_mixnodes`: list of the nodes that are eligible to receive non-zero rewards. + /// * `rewarding_interval_nonce`: nonce associated with the current rewarding interval async fn distribute_rewards_to_mixnodes( &self, eligible_mixnodes: &[MixnodeToReward], + rewarding_interval_nonce: u32, ) -> Option> { let mut failed_chunks = Vec::new(); for (i, mix_chunk) in eligible_mixnodes.chunks(MAX_TO_REWARD_AT_ONCE).enumerate() { - if let Err(err) = self.nymd_client.reward_mixnodes(mix_chunk).await { + if let Err(err) = self + .nymd_client + .reward_mixnodes(mix_chunk, rewarding_interval_nonce) + .await + { // this is a super weird edge case that we didn't catch change to sequence and // resent rewards unnecessarily, but the mempool saved us from executing it again // however, still we want to wait until we're sure we're into the next block @@ -327,13 +339,13 @@ impl Rewarder { /// /// # Arguments /// - /// * `epoch_rewarding_id`: id of the current epoch rewarding. + /// * `epoch_rewarding_id`: id of the current epoch rewarding as stored in the databse. /// /// * `active_monitor_mixnodes`: list of the nodes that were tested at least once by the network monitor /// in the last epoch. async fn distribute_rewards( &self, - epoch_rewarding_id: i64, + epoch_rewarding_database_id: i64, active_monitor_mixnodes: &[MixnodeStatusReport], ) -> Result<(RewardingReport, Option), RewardingError> { let mut failure_data = FailureData::default(); @@ -345,12 +357,20 @@ impl Rewarder { return Err(RewardingError::NoMixnodesToReward); } + let current_rewarding_nonce = self + .nymd_client + .get_current_rewarding_interval() + .await? + .current_rewarding_interval_nonce; + self.nymd_client + .begin_mixnode_rewarding(current_rewarding_nonce + 1) + .await?; failure_data.mixnodes = self - .distribute_rewards_to_mixnodes(&eligible_mixnodes) + .distribute_rewards_to_mixnodes(&eligible_mixnodes, current_rewarding_nonce + 1) .await; let report = RewardingReport { - epoch_rewarding_id, + epoch_rewarding_id: epoch_rewarding_database_id, eligible_mixnodes: eligible_mixnodes.len() as i64, possibly_unrewarded_mixnodes: failure_data .mixnodes @@ -364,6 +384,10 @@ impl Rewarder { .unwrap_or_default(), }; + self.nymd_client + .finish_mixnode_rewarding(current_rewarding_nonce + 1) + .await?; + if failure_data.mixnodes.is_none() { Ok((report, None)) } else {