From 085761a9fbf6031cfe19767f9fcab9848ba00e36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Tue, 23 Nov 2021 15:36:20 +0000 Subject: [PATCH] Feature/batch delegator rewarding (#898) * Unnrolled the loop into separate function * Ugly way of saving rewarding status * Initial way of rewarding next page of delegators * Attribute passing * Promoted transactions to directory * Moved rewarding-related functionalities into separate file * [ci skip] Generate TS types * Better errors on double rewarding attempt * Removed old rewarding call * Test fixes * Some cleanup * Paged mixnode rewarding test + serde fixes * Tests for delegator rewarding * ExecuteMsg for MixDelegatorRewarding * Made validator-api code compliable with bunch of todo!() macros * Removed Option wrapper from params in MixnodeToReward * Calculating uptime for entire epoch * Created shared MIXNODE_DELEGATORS_PAGE_LIMIT constant * Using new rewarding messages in validator API * cargo fmt * Updated wallet state types * Additional test for correct rewarding information * Query for rewarding status * Additional test regarding delegator rewarding * Client methods for obtaining rewarding status * Validator API checking for full rewarding * Removed unused field from validator api config template * Waiting for MINIMUM number of test routes * Waiting initialisation_backoff in the early return case * Fixes crash condition in validator API when calculating last day uptime * Fixed typo * Dealing with the case of rewarding mixnode with 0 uptime * Removed temporary unwrap * Guarding against 0-size rewarded/active sets Co-authored-by: jstuczyn --- Cargo.lock | 1 + .../validator-client/src/client.rs | 16 +- .../validator-client/src/nymd/mod.rs | 25 +- common/mixnet-contract/Cargo.toml | 2 +- common/mixnet-contract/src/lib.rs | 7 +- common/mixnet-contract/src/mixnode.rs | 117 +- common/mixnet-contract/src/msg.rs | 19 +- common/mixnet-contract/src/types.rs | 50 +- contracts/mixnet/Cargo.lock | 687 +----- contracts/mixnet/src/contract.rs | 55 +- contracts/mixnet/src/error.rs | 23 +- contracts/mixnet/src/helpers.rs | 120 +- contracts/mixnet/src/queries.rs | 269 ++- contracts/mixnet/src/state.rs | 6 +- contracts/mixnet/src/storage.rs | 330 +-- contracts/mixnet/src/support/tests.rs | 19 +- .../{transactions.rs => transactions/mod.rs} | 1564 +------------ .../mixnet/src/transactions/rewarding.rs | 2045 +++++++++++++++++ nym-wallet/Cargo.lock | 1 + nym-wallet/src-tauri/src/operations/admin.rs | 11 - nym-wallet/src/types/rust/stateparams.ts | 3 - validator-api/Cargo.toml | 2 - validator-api/src/config/template.rs | 3 - validator-api/src/node_status_api/models.rs | 2 +- validator-api/src/nymd_client.rs | 110 +- validator-api/src/rewarding/mod.rs | 385 ++-- validator-api/src/storage/mod.rs | 52 +- 27 files changed, 2990 insertions(+), 2934 deletions(-) rename contracts/mixnet/src/{transactions.rs => transactions/mod.rs} (56%) create mode 100644 contracts/mixnet/src/transactions/rewarding.rs diff --git a/Cargo.lock b/Cargo.lock index 4f8341854a..8b9e98a4d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1791,6 +1791,7 @@ dependencies = [ "az", "bytemuck", "half", + "serde", "typenum", ] diff --git a/common/client-libs/validator-client/src/client.rs b/common/client-libs/validator-client/src/client.rs index c50a9cffd0..f0d531de6f 100644 --- a/common/client-libs/validator-client/src/client.rs +++ b/common/client-libs/validator-client/src/client.rs @@ -10,7 +10,7 @@ use mixnet_contract::StateParams; use crate::{validator_api, ValidatorClientError}; use coconut_interface::{BlindSignRequestBody, BlindedSignatureResponse, VerificationKeyResponse}; -use mixnet_contract::{GatewayBond, MixNodeBond}; +use mixnet_contract::{GatewayBond, MixNodeBond, MixnodeRewardingStatusResponse}; #[cfg(feature = "nymd-client")] use mixnet_contract::{RawDelegationData, RewardingIntervalResponse}; use url::Url; @@ -181,6 +181,20 @@ impl Client { Ok(self.nymd.get_current_rewarding_interval().await?) } + pub async fn get_rewarding_status( + &self, + mix_identity: mixnet_contract::IdentityKey, + rewarding_interval_nonce: u32, + ) -> Result + where + C: CosmWasmClient + Sync, + { + Ok(self + .nymd + .get_rewarding_status(mix_identity, rewarding_interval_nonce) + .await?) + } + pub async fn get_reward_pool(&self) -> Result where C: CosmWasmClient + Sync, diff --git a/common/client-libs/validator-client/src/nymd/mod.rs b/common/client-libs/validator-client/src/nymd/mod.rs index 815076a664..d05e5dd176 100644 --- a/common/client-libs/validator-client/src/nymd/mod.rs +++ b/common/client-libs/validator-client/src/nymd/mod.rs @@ -14,10 +14,10 @@ use cosmrs::rpc::{Error as TendermintRpcError, HttpClientUrl}; use cosmwasm_std::{Coin, Uint128}; use mixnet_contract::{ Addr, Delegation, ExecuteMsg, Gateway, GatewayOwnershipResponse, IdentityKey, - LayerDistribution, MixNode, MixOwnershipResponse, PagedAllDelegationsResponse, - PagedGatewayResponse, PagedMixDelegationsResponse, PagedMixnodeResponse, - PagedReverseMixDelegationsResponse, QueryMsg, RawDelegationData, RewardingIntervalResponse, - StateParams, + LayerDistribution, MixNode, MixOwnershipResponse, MixnodeRewardingStatusResponse, + PagedAllDelegationsResponse, PagedGatewayResponse, PagedMixDelegationsResponse, + PagedMixnodeResponse, PagedReverseMixDelegationsResponse, QueryMsg, RawDelegationData, + RewardingIntervalResponse, StateParams, }; use serde::Serialize; use std::collections::HashMap; @@ -230,6 +230,23 @@ impl NymdClient { .await } + pub async fn get_rewarding_status( + &self, + mix_identity: mixnet_contract::IdentityKey, + rewarding_interval_nonce: u32, + ) -> Result + where + C: CosmWasmClient + Sync, + { + let request = QueryMsg::GetRewardingStatus { + mix_identity, + rewarding_interval_nonce, + }; + self.client + .query_contract_smart(self.contract_address()?, &request) + .await + } + pub async fn get_layer_distribution(&self) -> Result where C: CosmWasmClient + Sync, diff --git a/common/mixnet-contract/Cargo.toml b/common/mixnet-contract/Cargo.toml index 4267e290d4..adc5d7beab 100644 --- a/common/mixnet-contract/Cargo.toml +++ b/common/mixnet-contract/Cargo.toml @@ -17,7 +17,7 @@ schemars = "0.8" ts-rs = { version = "5.1", optional = true } thiserror = "1.0" network-defaults = { path = "../network-defaults" } -fixed = "1.1" +fixed = { version = "1.1", features = ["serde"] } az = "1.1" log = "0.4.14" diff --git a/common/mixnet-contract/src/lib.rs b/common/mixnet-contract/src/lib.rs index c3162adf4a..584e3d1d8d 100644 --- a/common/mixnet-contract/src/lib.rs +++ b/common/mixnet-contract/src/lib.rs @@ -8,6 +8,8 @@ pub mod mixnode; mod msg; mod types; +pub const MIXNODE_DELEGATORS_PAGE_LIMIT: usize = 250; + pub use cosmwasm_std::{Addr, Coin}; pub use delegation::{ Delegation, PagedAllDelegationsResponse, PagedMixDelegationsResponse, @@ -16,7 +18,4 @@ pub use delegation::{ pub use gateway::{Gateway, GatewayBond, GatewayOwnershipResponse, PagedGatewayResponse}; pub use mixnode::{Layer, MixNode, MixNodeBond, MixOwnershipResponse, PagedMixnodeResponse}; pub use msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; -pub use types::{ - IdentityKey, IdentityKeyRef, LayerDistribution, RewardingIntervalResponse, SphinxKey, - StateParams, -}; +pub use types::*; diff --git a/common/mixnet-contract/src/mixnode.rs b/common/mixnet-contract/src/mixnode.rs index 287271e373..f38ec10255 100644 --- a/common/mixnet-contract/src/mixnode.rs +++ b/common/mixnet-contract/src/mixnode.rs @@ -14,6 +14,10 @@ use std::fmt::Display; type U128 = fixed::types::U75F53; // u128 with 18 significant digits +fixed::const_fixed_from_int! { + const ONE: U128 = 1; +} + #[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))] #[derive(Clone, Debug, Deserialize, PartialEq, PartialOrd, Serialize, JsonSchema)] pub struct MixNode { @@ -110,7 +114,7 @@ impl NodeRewardParams { } pub fn one_over_k(&self) -> U128 { - U128::from_num(1) / U128::from_num(self.k.u128()) + ONE / U128::from_num(self.k.u128()) } pub fn alpha(&self) -> U128 { @@ -118,6 +122,90 @@ impl NodeRewardParams { } } +// cosmwasm's limited serde doesn't work with U128 directly +#[allow(non_snake_case)] +pub mod fixed_U128_as_string { + use super::U128; + use serde::de::Error; + use serde::Deserialize; + use std::str::FromStr; + + pub fn serialize(val: &U128, serializer: S) -> Result + where + S: serde::Serializer, + { + let s = (*val).to_string(); + serializer.serialize_str(&s) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + U128::from_str(&s).map_err(|err| { + D::Error::custom(format!( + "failed to deserialize U128 with its string representation - {}", + err + )) + }) + } +} + +// everything required to reward delegator of given mixnode +#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)] +pub struct DelegatorRewardParams { + node_reward_params: NodeRewardParams, + + // to be completely honest I don't understand all consequences of using `#[schemars(with = "String")]` + // for U128 here, but it seems that CosmWasm is using the same attribute for their Uint128 + #[schemars(with = "String")] + #[serde(with = "fixed_U128_as_string")] + sigma: U128, + #[schemars(with = "String")] + #[serde(with = "fixed_U128_as_string")] + profit_margin: U128, + #[schemars(with = "String")] + #[serde(with = "fixed_U128_as_string")] + node_profit: U128, +} + +impl DelegatorRewardParams { + pub fn new(mixnode_bond: &MixNodeBond, node_reward_params: NodeRewardParams) -> Self { + DelegatorRewardParams { + sigma: mixnode_bond.sigma(&node_reward_params), + profit_margin: mixnode_bond.profit_margin(), + node_profit: mixnode_bond.node_profit(&node_reward_params), + node_reward_params, + } + } + + pub fn determine_delegation_reward(&self, delegation_amount: Uint128) -> u128 { + // change all values into their fixed representations + let delegation_amount = U128::from_num(delegation_amount.u128()); + let circulating_supply = U128::from_num(self.node_reward_params.circulating_supply()); + + let scaled_delegation_amount = delegation_amount / circulating_supply; + let delegator_reward = + (ONE - self.profit_margin) * scaled_delegation_amount / self.sigma * self.node_profit; + + let reward = delegator_reward.max(U128::ZERO); + if let Some(int_reward) = reward.checked_cast() { + int_reward + } else { + error!( + "Could not cast delegator reward ({}) to u128, returning 0", + reward, + ); + 0u128 + } + } + + pub fn node_reward_params(&self) -> &NodeRewardParams { + &self.node_reward_params + } +} + #[derive(Debug)] pub struct NodeRewardResult { reward: U128, @@ -228,14 +316,14 @@ impl MixNodeBond { pub fn reward(&self, params: &NodeRewardParams) -> NodeRewardResult { // Assuming uniform work distribution across the network this is one_over_k * k - let omega_k = U128::from_num(1u128); + let omega_k = ONE; let lambda = self.lambda(params); let sigma = self.sigma(params); let reward = params.performance() * params.period_reward_pool() * (sigma * omega_k + params.alpha() * lambda * sigma * params.k()) - / (U128::from_num(1) + params.alpha()); + / (ONE + params.alpha()); NodeRewardResult { reward, @@ -261,7 +349,7 @@ impl MixNodeBond { }; let operator_base_reward = reward.reward.min(params.operator_cost()); let operator_reward = (self.profit_margin() - + (U128::from_num(1) - self.profit_margin()) * reward.lambda / reward.sigma) + + (ONE - self.profit_margin()) * reward.lambda / reward.sigma) * profit; let reward = (operator_reward + operator_base_reward).max(U128::from_num(0)); @@ -288,25 +376,8 @@ impl MixNodeBond { } pub fn reward_delegation(&self, delegation_amount: Uint128, params: &NodeRewardParams) -> u128 { - let scaled_delegation_amount = - U128::from_num(delegation_amount.u128()) / U128::from_num(params.circulating_supply()); - - let delegator_reward = (U128::from_num(1) - self.profit_margin()) - * scaled_delegation_amount - / self.sigma(params) - * self.node_profit(params); - - let reward = delegator_reward.max(U128::from_num(0)); - if let Some(int_reward) = reward.checked_cast() { - int_reward - } else { - error!( - "Could not cast delegator reward ({}) to u128, returning 0 - mixnode {}", - reward, - self.identity() - ); - 0u128 - } + let reward_params = DelegatorRewardParams::new(self, *params); + reward_params.determine_delegation_reward(delegation_amount) } } diff --git a/common/mixnet-contract/src/msg.rs b/common/mixnet-contract/src/msg.rs index e7c4f4e643..def7d52476 100644 --- a/common/mixnet-contract/src/msg.rs +++ b/common/mixnet-contract/src/msg.rs @@ -37,15 +37,6 @@ pub enum ExecuteMsg { 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, @@ -59,6 +50,12 @@ pub enum ExecuteMsg { // nonce of the current rewarding interval rewarding_interval_nonce: u32, }, + + RewardNextMixDelegators { + mix_identity: IdentityKey, + // nonce of the current rewarding interval + rewarding_interval_nonce: u32, + }, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] @@ -103,6 +100,10 @@ pub enum QueryMsg { GetCirculatingSupply {}, GetEpochRewardPercent {}, GetSybilResistancePercent {}, + GetRewardingStatus { + mix_identity: IdentityKey, + rewarding_interval_nonce: u32, + }, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] diff --git a/common/mixnet-contract/src/types.rs b/common/mixnet-contract/src/types.rs index ee6ad94477..6f48ec8478 100644 --- a/common/mixnet-contract/src/types.rs +++ b/common/mixnet-contract/src/types.rs @@ -1,8 +1,9 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::mixnode::DelegatorRewardParams; use crate::Layer; -use cosmwasm_std::{Decimal, Uint128}; +use cosmwasm_std::Uint128; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::fmt::{self, Display, Formatter}; @@ -35,14 +36,13 @@ pub struct RewardingIntervalResponse { #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] pub struct StateParams { - pub epoch_length: u32, // length of a rewarding epoch/interval, expressed in hours - + // so currently epoch_length is being unused and validator API performs rewarding + // based on its own epoch length config value. I guess that's fine for time being + // however, in the future, the contract constant should be controlling it instead. + // pub epoch_length: u32, // length of a rewarding epoch/interval, expressed in hours pub minimum_mixnode_bond: Uint128, // minimum amount a mixnode must bond to get into the system pub minimum_gateway_bond: Uint128, // minimum amount a gateway must bond to get into the system - pub mixnode_bond_reward_rate: Decimal, // annual reward rate, expressed as a decimal like 1.25 - pub mixnode_delegation_reward_rate: Decimal, // annual reward rate, expressed as a decimal like 1.25 - // number of mixnode that are going to get rewarded during current rewarding interval (k_m) // based on overall demand for private bandwidth- pub mixnode_rewarded_set_size: u32, @@ -55,19 +55,8 @@ pub struct StateParams { impl Display for StateParams { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { write!(f, "Contract state parameters: [ ")?; - write!(f, "epoch length: {}; ", self.epoch_length)?; write!(f, "minimum mixnode bond: {}; ", self.minimum_mixnode_bond)?; write!(f, "minimum gateway bond: {}; ", self.minimum_gateway_bond)?; - write!( - f, - "mixnode bond reward rate: {}; ", - self.mixnode_bond_reward_rate - )?; - write!( - f, - "mixnode delegation reward rate: {}; ", - self.mixnode_delegation_reward_rate - )?; write!( f, "mixnode rewarded set size: {}", @@ -81,6 +70,33 @@ impl Display for StateParams { } } +#[derive(Default, Debug, Serialize, Deserialize, PartialEq)] +pub struct RewardingResult { + pub operator_reward: Uint128, + pub total_delegator_reward: Uint128, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct PendingDelegatorRewarding { + // keep track of the running rewarding results so we'd known how much was the operator and its delegators rewarded + pub running_results: RewardingResult, + + pub next_start: String, + + pub rewarding_params: DelegatorRewardParams, +} + +#[derive(Debug, Serialize, Deserialize)] +pub enum RewardingStatus { + Complete(RewardingResult), + PendingNextDelegatorPage(PendingDelegatorRewarding), +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct MixnodeRewardingStatusResponse { + pub status: Option, +} + // type aliases for better reasoning about available data pub type IdentityKey = String; pub type IdentityKeyRef<'a> = &'a str; diff --git a/contracts/mixnet/Cargo.lock b/contracts/mixnet/Cargo.lock index 357afd9bed..de88924205 100644 --- a/contracts/mixnet/Cargo.lock +++ b/contracts/mixnet/Cargo.lock @@ -2,45 +2,6 @@ # It is not intended for manual editing. version = 3 -[[package]] -name = "Inflector" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" -dependencies = [ - "lazy_static", - "regex", -] - -[[package]] -name = "aho-corasick" -version = "0.7.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" -dependencies = [ - "memchr", -] - -[[package]] -name = "ast_node" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e96d5444b02f3080edac8a144f6baf29b2fb6ff589ad4311559731a7c7529381" -dependencies = [ - "darling", - "pmutil", - "proc-macro2", - "quote", - "swc_macros_common", - "syn", -] - -[[package]] -name = "autocfg" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" - [[package]] name = "az" version = "1.1.2" @@ -53,12 +14,6 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - [[package]] name = "block-buffer" version = "0.7.3" @@ -89,12 +44,6 @@ dependencies = [ "byte-tools", ] -[[package]] -name = "bumpalo" -version = "3.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f1e260c3a9040a7c19a12468758f4c16f31a81a1fe087482be9570ec864bb6c" - [[package]] name = "byte-tools" version = "0.3.1" @@ -113,12 +62,6 @@ version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" -[[package]] -name = "cfg-if" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" - [[package]] name = "cfg-if" version = "1.0.0" @@ -247,41 +190,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "darling" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn", -] - -[[package]] -name = "darling_macro" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72" -dependencies = [ - "darling_core", - "quote", - "syn", -] - [[package]] name = "der" version = "0.4.4" @@ -309,45 +217,6 @@ dependencies = [ "generic-array 0.14.4", ] -[[package]] -name = "dprint-core" -version = "0.35.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93bd44f40b1881477837edc7112695d4b174f058c36c1cbc4c50f8d0482e2ac8" -dependencies = [ - "bumpalo", - "fnv", - "serde", -] - -[[package]] -name = "dprint-plugin-typescript" -version = "0.43.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67ba0077bd2ab9235848e793fbbfb563e6a04b4c8e4149827802a84063c15805" -dependencies = [ - "dprint-core", - "dprint-swc-ecma-ast-view", - "fnv", - "serde", - "swc_common", - "swc_ecmascript", -] - -[[package]] -name = "dprint-swc-ecma-ast-view" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecf692a2ee5c5f699ed0e95f21686cf6367f3a591e5d8e7bd3041bbf184651f9" -dependencies = [ - "bumpalo", - "fnv", - "num-bigint", - "swc_atoms", - "swc_common", - "swc_ecmascript", -] - [[package]] name = "dyn-clone" version = "1.0.4" @@ -380,12 +249,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "either" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" - [[package]] name = "elliptic-curve" version = "0.10.6" @@ -402,18 +265,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "enum_kind" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b940da354ae81ef0926c5eaa428207b8f4f091d3956c891dfbd124162bed99" -dependencies = [ - "pmutil", - "proc-macro2", - "swc_macros_common", - "syn", -] - [[package]] name = "fake-simd" version = "0.1.2" @@ -439,15 +290,10 @@ dependencies = [ "az", "bytemuck", "half", + "serde", "typenum", ] -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - [[package]] name = "form_urlencoded" version = "1.0.1" @@ -458,27 +304,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "from_variant" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0951635027ca477be98f8774abd6f0345233439d63f307e47101acb40c7cc63d" -dependencies = [ - "pmutil", - "proc-macro2", - "swc_macros_common", - "syn", -] - -[[package]] -name = "fxhash" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" -dependencies = [ - "byteorder", -] - [[package]] name = "generic-array" version = "0.12.4" @@ -504,7 +329,7 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "libc", "wasi 0.9.0+wasi-snapshot-preview1", ] @@ -515,7 +340,7 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "libc", "wasi 0.10.2+wasi-snapshot-preview1", ] @@ -589,12 +414,6 @@ dependencies = [ "serde", ] -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - [[package]] name = "idna" version = "0.2.3" @@ -606,28 +425,6 @@ dependencies = [ "unicode-normalization", ] -[[package]] -name = "instant" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" -dependencies = [ - "cfg-if 1.0.0", -] - -[[package]] -name = "is-macro" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a322dd16d960e322c3d92f541b4c1a4f0a2e81e1fdeee430d8cecc8b72e8015f" -dependencies = [ - "Inflector", - "pmutil", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "itoa" version = "0.4.8" @@ -640,40 +437,25 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "903ae2481bcdfdb7b68e0a9baa4b7c9aff600b9ae2e8e5bb5833b8c91ab851ea" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "ecdsa", "elliptic-curve", "sha2", ] -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" - [[package]] name = "libc" version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "869d572136620d55835903746bcb5cdc54cb2851fd0aeec53220b4bb65ef3013" -[[package]] -name = "lock_api" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109" -dependencies = [ - "scopeguard", -] - [[package]] name = "log" version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", ] [[package]] @@ -688,12 +470,6 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" -[[package]] -name = "memchr" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" - [[package]] name = "mixnet-contract" version = "0.1.0" @@ -707,7 +483,6 @@ dependencies = [ "serde", "serde_repr", "thiserror", - "ts-rs", ] [[package]] @@ -735,49 +510,6 @@ dependencies = [ "url", ] -[[package]] -name = "new_debug_unreachable" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" - -[[package]] -name = "num-bigint" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "090c7f9998ee0ff65aa5b723e4009f7b217707f1fb5ea551329cc4d6231fb304" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", - "serde", -] - -[[package]] -name = "num-integer" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" -dependencies = [ - "autocfg", - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" -dependencies = [ - "autocfg", -] - -[[package]] -name = "once_cell" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" - [[package]] name = "opaque-debug" version = "0.2.3" @@ -790,40 +522,6 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" -[[package]] -name = "owning_ref" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ff55baddef9e4ad00f88b6c743a2a8062d4c6ade126c2a528644b8e444d52ce" -dependencies = [ - "stable_deref_trait", -] - -[[package]] -name = "parking_lot" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" -dependencies = [ - "instant", - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" -dependencies = [ - "cfg-if 1.0.0", - "instant", - "libc", - "redox_syscall", - "smallvec", - "winapi", -] - [[package]] name = "percent-encoding" version = "2.1.0" @@ -873,25 +571,6 @@ dependencies = [ "sha-1", ] -[[package]] -name = "phf_generator" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" -dependencies = [ - "phf_shared", - "rand", -] - -[[package]] -name = "phf_shared" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" -dependencies = [ - "siphasher", -] - [[package]] name = "pkcs8" version = "0.7.6" @@ -902,29 +581,6 @@ dependencies = [ "spki", ] -[[package]] -name = "pmutil" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3894e5d549cccbe44afecf72922f277f603cd4bb0219c8342631ef18fffbe004" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "ppv-lite86" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed0cfbc8191465bed66e1718596ee0b0b35d5ee1f41c5df2189d0fe8bde535ba" - -[[package]] -name = "precomputed-hash" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" - [[package]] name = "proc-macro2" version = "1.0.32" @@ -949,30 +605,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "rand" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" -dependencies = [ - "getrandom 0.1.16", - "libc", - "rand_chacha", - "rand_core 0.5.1", - "rand_hc", - "rand_pcg", -] - -[[package]] -name = "rand_chacha" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" -dependencies = [ - "ppv-lite86", - "rand_core 0.5.1", -] - [[package]] name = "rand_core" version = "0.5.1" @@ -991,50 +623,6 @@ dependencies = [ "getrandom 0.2.3", ] -[[package]] -name = "rand_hc" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" -dependencies = [ - "rand_core 0.5.1", -] - -[[package]] -name = "rand_pcg" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" -dependencies = [ - "rand_core 0.5.1", -] - -[[package]] -name = "redox_syscall" -version = "0.2.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" -dependencies = [ - "bitflags", -] - -[[package]] -name = "regex" -version = "1.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.6.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" - [[package]] name = "ryu" version = "1.0.5" @@ -1065,18 +653,6 @@ dependencies = [ "syn", ] -[[package]] -name = "scoped-tls" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" - -[[package]] -name = "scopeguard" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" - [[package]] name = "serde" version = "1.0.130" @@ -1158,7 +734,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b69f9a4c9740d74c5baa3fd2e547f9525fa8088a8a958e0ca2409a514e33f5fa" dependencies = [ "block-buffer 0.9.0", - "cfg-if 1.0.0", + "cfg-if", "cpufeatures", "digest 0.9.0", "opaque-debug 0.3.0", @@ -1174,18 +750,6 @@ dependencies = [ "rand_core 0.6.3", ] -[[package]] -name = "siphasher" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "533494a8f9b724d33625ab53c6c4800f7cc445895924a8ef649222dcb76e938b" - -[[package]] -name = "smallvec" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309" - [[package]] name = "spki" version = "0.4.1" @@ -1195,207 +759,18 @@ dependencies = [ "der", ] -[[package]] -name = "stable_deref_trait" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" - [[package]] name = "static_assertions" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" -[[package]] -name = "string_cache" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "923f0f39b6267d37d23ce71ae7235602134b250ace715dd2c90421998ddac0c6" -dependencies = [ - "lazy_static", - "new_debug_unreachable", - "parking_lot", - "phf_shared", - "precomputed-hash", - "serde", -] - -[[package]] -name = "string_cache_codegen" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f24c8e5e19d22a726626f1a5e16fe15b132dcf21d10177fa5a45ce7962996b97" -dependencies = [ - "phf_generator", - "phf_shared", - "proc-macro2", - "quote", -] - -[[package]] -name = "string_enum" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f584cc881e9e5f1fd6bf827b0444aa94c30d8fe6378cf241071b5f5700b2871f" -dependencies = [ - "pmutil", - "proc-macro2", - "quote", - "swc_macros_common", - "syn", -] - -[[package]] -name = "strsim" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" - [[package]] name = "subtle" version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" -[[package]] -name = "swc_atoms" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f5229fe227ff0060e13baa386d6e368797700eab909523f730008d191ee53ae" -dependencies = [ - "string_cache", - "string_cache_codegen", -] - -[[package]] -name = "swc_common" -version = "0.10.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c93df65683ec1a001e15ce1de438c7c2c226c0c2462d1cb93fa1bd2a7664170b" -dependencies = [ - "ast_node", - "cfg-if 0.1.10", - "either", - "from_variant", - "fxhash", - "log", - "num-bigint", - "once_cell", - "owning_ref", - "scoped-tls", - "serde", - "string_cache", - "swc_eq_ignore_macros", - "swc_visit", - "unicode-width", -] - -[[package]] -name = "swc_ecma_ast" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83eb6a73820660a5af3c24ae1d436e84e4d4c13822021140011361e678df247b" -dependencies = [ - "is-macro", - "num-bigint", - "serde", - "string_enum", - "swc_atoms", - "swc_common", -] - -[[package]] -name = "swc_ecma_parser" -version = "0.52.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c03250697857164f16fa98f8e1726f566652d13e52ea3f0c3ecea9deb63ee327" -dependencies = [ - "either", - "enum_kind", - "fxhash", - "log", - "num-bigint", - "serde", - "smallvec", - "swc_atoms", - "swc_common", - "swc_ecma_ast", - "swc_ecma_visit", - "unicode-xid", -] - -[[package]] -name = "swc_ecma_visit" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd3d60b9dc97ae4f181d4d60f43142d8ac9669953db410bcedefb29a14627e19" -dependencies = [ - "num-bigint", - "swc_atoms", - "swc_common", - "swc_ecma_ast", - "swc_visit", -] - -[[package]] -name = "swc_ecmascript" -version = "0.29.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ffb53afe008c15d4dc4957e80148c4b457659f93e4d4e8736eaeae352e48ec8" -dependencies = [ - "swc_ecma_ast", - "swc_ecma_parser", -] - -[[package]] -name = "swc_eq_ignore_macros" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c8f200a2eaed938e7c1a685faaa66e6d42fa9e17da5f62572d3cbc335898f5e" -dependencies = [ - "pmutil", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "swc_macros_common" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf7c68e78ffbcba3d38abe6d0b76a0e1a37888b5c9301db3426537207090ada3" -dependencies = [ - "pmutil", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "swc_visit" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8511a4788ab29daf00bee23e425aac92c9be4eec74c98fec4a45d0e710be695" -dependencies = [ - "either", - "swc_visit_macros", -] - -[[package]] -name = "swc_visit_macros" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3b2825fee79f10d0166e8e650e79c7a862fb991db275743083f07555d7641f0" -dependencies = [ - "Inflector", - "pmutil", - "proc-macro2", - "quote", - "swc_macros_common", - "syn", -] - [[package]] name = "syn" version = "1.0.81" @@ -1467,28 +842,6 @@ dependencies = [ "serde", ] -[[package]] -name = "ts-rs" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "369e48de67506679b3a576b0faf666fa9f9acf2fd00b4c61e28bdb6c8e08ec06" -dependencies = [ - "dprint-plugin-typescript", - "ts-rs-macros", -] - -[[package]] -name = "ts-rs-macros" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f269e8fd28e26b4cdbd01f81f345aaf666131511e54a735a76a614b5062d0a5a" -dependencies = [ - "Inflector", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "typenum" version = "1.14.0" @@ -1528,12 +881,6 @@ dependencies = [ "tinyvec", ] -[[package]] -name = "unicode-width" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" - [[package]] name = "unicode-xid" version = "0.2.2" @@ -1570,28 +917,6 @@ version = "0.10.2+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - [[package]] name = "zeroize" version = "1.4.2" diff --git a/contracts/mixnet/src/contract.rs b/contracts/mixnet/src/contract.rs index 42ad4d4244..80f7de72b7 100644 --- a/contracts/mixnet/src/contract.rs +++ b/contracts/mixnet/src/contract.rs @@ -3,29 +3,21 @@ use std::u128; -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::REWARDING_VALIDATOR_ADDRESS; use cosmwasm_std::{ - entry_point, to_binary, Addr, Decimal, Deps, DepsMut, Env, MessageInfo, QueryResponse, - Response, Uint128, + entry_point, to_binary, Addr, Deps, DepsMut, Env, MessageInfo, QueryResponse, Response, Uint128, }; use mixnet_contract::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, StateParams}; -pub const INITIAL_DEFAULT_EPOCH_LENGTH: u32 = 2; - /// Constant specifying minimum of coin required to bond a gateway pub const INITIAL_GATEWAY_BOND: Uint128 = Uint128(100_000000); /// Constant specifying minimum of coin required to bond a mixnode pub const INITIAL_MIXNODE_BOND: Uint128 = Uint128(100_000000); -// percentage annual increase. Given starting value of x, we expect to have 1.1x at the end of the year -pub const INITIAL_MIXNODE_BOND_REWARD_RATE: u64 = 110; -pub const INITIAL_MIXNODE_DELEGATION_REWARD_RATE: u64 = 110; - pub const INITIAL_MIXNODE_REWARDED_SET_SIZE: u32 = 200; pub const INITIAL_MIXNODE_ACTIVE_SET_SIZE: u32 = 100; @@ -33,36 +25,22 @@ pub const INITIAL_REWARD_POOL: u128 = 250_000_000_000_000; pub const EPOCH_REWARD_PERCENT: u8 = 2; // Used to calculate epoch reward pool pub const DEFAULT_SYBIL_RESISTANCE_PERCENT: u8 = 30; -// We'll be assuming a few more things, profit margin and cost function. Since we don't have relialable package measurement, we'll be using uptime. We'll also set the value of 1 Nym to 1 $, to be able to translate epoch costs to Nyms. We'll also assume a cost of 40$ per epoch(month), converting that to Nym at our 1$ rate translates to 40_000_000 uNyms +// We'll be assuming a few more things, profit margin and cost function. Since we don't have reliable package measurement, we'll be using uptime. We'll also set the value of 1 Nym to 1 $, to be able to translate epoch costs to Nyms. We'll also assume a cost of 40$ per epoch(month), converting that to Nym at our 1$ rate translates to 40_000_000 uNyms pub const DEFAULT_COST_PER_EPOCH: u32 = 40_000_000; fn default_initial_state(owner: Addr, env: Env) -> State { - let mixnode_bond_reward_rate = Decimal::percent(INITIAL_MIXNODE_BOND_REWARD_RATE); - let mixnode_delegation_reward_rate = Decimal::percent(INITIAL_MIXNODE_DELEGATION_REWARD_RATE); - State { owner, 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, minimum_gateway_bond: INITIAL_GATEWAY_BOND, - mixnode_bond_reward_rate, - mixnode_delegation_reward_rate, mixnode_rewarded_set_size: INITIAL_MIXNODE_REWARDED_SET_SIZE, 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, - mixnode_bond_reward_rate, - ), - mixnode_epoch_delegation_reward: calculate_epoch_reward_rate( - INITIAL_DEFAULT_EPOCH_LENGTH, - mixnode_delegation_reward_rate, - ), } } @@ -105,18 +83,6 @@ pub fn execute( ExecuteMsg::UpdateStateParams(params) => { transactions::try_update_state_params(deps, info, params) } - ExecuteMsg::RewardMixnode { - identity, - uptime, - rewarding_interval_nonce, - } => transactions::try_reward_mixnode( - deps, - env, - info, - identity, - uptime, - rewarding_interval_nonce, - ), ExecuteMsg::RewardMixnodeV2 { identity, params, @@ -141,6 +107,15 @@ pub fn execute( ExecuteMsg::FinishMixnodeRewarding { rewarding_interval_nonce, } => transactions::try_finish_mixnode_rewarding(deps, info, rewarding_interval_nonce), + ExecuteMsg::RewardNextMixDelegators { + mix_identity, + rewarding_interval_nonce, + } => transactions::try_reward_next_mixnode_delegators_v2( + deps, + info, + mix_identity, + rewarding_interval_nonce, + ), } } @@ -199,6 +174,14 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> Result to_binary(&queries::query_circulating_supply(deps)), QueryMsg::GetEpochRewardPercent {} => to_binary(&EPOCH_REWARD_PERCENT), QueryMsg::GetSybilResistancePercent {} => to_binary(&DEFAULT_SYBIL_RESISTANCE_PERCENT), + QueryMsg::GetRewardingStatus { + mix_identity, + rewarding_interval_nonce, + } => to_binary(&queries::query_rewarding_status( + deps, + mix_identity, + rewarding_interval_nonce, + )?), }; Ok(query_res?) diff --git a/contracts/mixnet/src/error.rs b/contracts/mixnet/src/error.rs index 027f697407..c4131303a0 100644 --- a/contracts/mixnet/src/error.rs +++ b/contracts/mixnet/src/error.rs @@ -45,15 +45,15 @@ pub enum ContractError { #[error("No coin was sent for the bonding, you must send {}", DENOM)] NoBondFound, - #[error("The bond reward rate for mixnode was set to be lower than 1")] - DecreasingMixnodeBondReward, - - #[error("The delegation reward rate for mixnode was set to be lower than 1")] - DecreasingMixnodeDelegationReward, - #[error("Provided active set size is bigger than the demanded set")] InvalidActiveSetSize, + #[error("Provided active set size is zero")] + ZeroActiveSet, + + #[error("Provided rewarded set size is zero")] + ZeroRewardedSet, + #[error("The node had uptime larger than 100%")] UnexpectedUptime, @@ -80,15 +80,10 @@ pub enum ContractError { identity: IdentityKey, address: Addr, }, - #[error("Overflow error!")] - Overflow(#[from] cosmwasm_std::OverflowError), #[error("We tried to remove more funds then are available in the Reward pool. Wanted to remove {to_remove}, but have only {reward_pool}")] OutOfFunds { to_remove: u128, reward_pool: u128 }, - #[error("Invalid ratio")] - Ratio(#[from] mixnet_contract::error::MixnetContractError), - #[error("Received invalid rewarding interval nonce. Expected {expected}, received {received}")] InvalidRewardingIntervalNonce { received: u32, expected: u32 }, @@ -100,4 +95,10 @@ pub enum ContractError { #[error("Mixnode {identity} has already been rewarded during the current rewarding interval")] MixnodeAlreadyRewarded { identity: IdentityKey }, + + #[error("Some of mixnodes {identity} delegators are still pending reward")] + DelegatorsPendingReward { identity: IdentityKey }, + + #[error("Mixnode's {identity} operator has not been rewarded yet - cannot perform delegator rewarding until that happens")] + MixnodeOperatorNotRewarded { identity: IdentityKey }, } diff --git a/contracts/mixnet/src/helpers.rs b/contracts/mixnet/src/helpers.rs index 6c86b4034e..9e9aebf001 100644 --- a/contracts/mixnet/src/helpers.rs +++ b/contracts/mixnet/src/helpers.rs @@ -1,78 +1,16 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::error::ContractError; use crate::transactions::OLD_DELEGATIONS_CHUNK_SIZE; -use cosmwasm_std::{Decimal, Order, StdError, StdResult, Uint128}; +use cosmwasm_std::{Order, StdError, StdResult}; use cosmwasm_storage::ReadonlyBucket; use mixnet_contract::{Addr, IdentityKey, PagedAllDelegationsResponse, UnpackedDelegation}; use serde::de::DeserializeOwned; use serde::Serialize; -use std::ops::Sub; - -// for time being completely ignore concept of a leap year and assume each year is exactly 365 days -// i.e. 8760 hours -const HOURS_IN_YEAR: u128 = 8760; - -const DECIMAL_FRACTIONAL: Uint128 = Uint128(1_000_000_000_000_000_000u128); // cosmwasm bucket internal value const NAMESPACE_LENGTH: usize = 2; -pub fn decimal_to_uint128(value: Decimal) -> Uint128 { - value * DECIMAL_FRACTIONAL -} - -pub fn uint128_to_decimal(value: Uint128) -> Decimal { - Decimal::from_ratio(value, DECIMAL_FRACTIONAL) -} - -pub(crate) fn calculate_epoch_reward_rate( - epoch_length: u32, - annual_reward_rate: Decimal, -) -> Decimal { - // this is more of a sanity check as the contract does not allow setting annual reward rates - // to be lower than 1. - debug_assert!(annual_reward_rate >= Decimal::one()); - - // converts reward rate, like 1.25 into the expected gain, like 0.25 - let annual_reward = annual_reward_rate.sub(Decimal::one()); - // do a simple cross-multiplication: - // `annual_reward` - `HOURS_IN_YEAR` - // x - `epoch_length` - // - // x = `annual_reward` * `epoch_length` / `HOURS_IN_YEAR` - - // converts reward, like 0.25 into 250000000000000000 - let annual_reward_uint128 = decimal_to_uint128(annual_reward); - - // calculates `annual_reward_uint128` * `epoch_length` / `HOURS_IN_YEAR` - let epoch_reward_uint128 = annual_reward_uint128.multiply_ratio(epoch_length, HOURS_IN_YEAR); - - // note: this returns a % reward, like 0.05 rather than reward rate (like 1.05) - uint128_to_decimal(epoch_reward_uint128) -} - -pub(crate) fn scale_reward_by_uptime( - reward: Decimal, - uptime: u32, -) -> Result { - if uptime > 100 { - return Err(ContractError::UnexpectedUptime); - } - let uptime_ratio = Decimal::from_ratio(uptime, 100u128); - // if we do not convert into a more precise representation, we might end up with, for example, - // reward 0.05 and uptime of 50% which would produce 0.50 * 0.05 = 0 (because of u128 representation) - // and also the above would be impossible to compute as Mul for Decimal is not implemented - // - // but with the intermediate conversion, we would have - // 0.50 * 50_000_000_000_000_000 = 25_000_000_000_000_000 - // which converted back would give us the proper 0.025 - let uptime_ratio_u128 = decimal_to_uint128(uptime_ratio); - let scaled = reward * uptime_ratio_u128; - Ok(uint128_to_decimal(scaled)) -} - // Extracts the node identity and owner of a delegation from the bytes used as // key in the delegation buckets. fn extract_identity_and_owner(bytes: Vec) -> StdResult<(Addr, IdentityKey)> { @@ -213,7 +151,6 @@ mod tests { use crate::support::tests::helpers; use cosmwasm_std::testing::mock_dependencies; use mixnet_contract::RawDelegationData; - use std::str::FromStr; #[test] fn delegations_iterator() { @@ -249,61 +186,6 @@ mod tests { assert!(delegations.next().is_none()); } - #[test] - fn calculating_epoch_reward_rate() { - // 1.10 - let annual_reward_rate = Decimal::from_ratio(110u128, 100u128); - - // if the epoch is (for some reason) exactly one year, - // the reward rate should be unchanged - let per_epoch_rate = calculate_epoch_reward_rate(HOURS_IN_YEAR as u32, annual_reward_rate); - // 0.10 - let expected = annual_reward_rate.sub(Decimal::one()); - assert_eq!(expected, per_epoch_rate); - - // 24 hours - let per_epoch_rate = calculate_epoch_reward_rate(24, annual_reward_rate); - // 0.1 / 365 - let expected = Decimal::from_ratio(1u128, 3650u128); - assert_eq!(expected, per_epoch_rate); - - let expected_per_epoch_rate_excel = Decimal::from_str("0.000273972602739726").unwrap(); - assert_eq!(expected_per_epoch_rate_excel, per_epoch_rate); - - // 1 hour - let per_epoch_rate = calculate_epoch_reward_rate(1, annual_reward_rate); - // 0.1 / 8760 - let expected = Decimal::from_ratio(1u128, 87600u128); - assert_eq!(expected, per_epoch_rate); - } - - #[test] - fn scaling_reward_by_uptime() { - // 0.05 - let epoch_reward = Decimal::from_ratio(5u128, 100u128); - - // scaling by 100 does nothing - let scaled = scale_reward_by_uptime(epoch_reward, 100).unwrap(); - assert_eq!(epoch_reward, scaled); - - // scaling by 0 makes the reward 0 - let scaled = scale_reward_by_uptime(epoch_reward, 0).unwrap(); - assert_eq!(Decimal::zero(), scaled); - - // 50 halves it - let scaled = scale_reward_by_uptime(epoch_reward, 50).unwrap(); - let expected = Decimal::from_ratio(25u128, 1000u128); - assert_eq!(expected, scaled); - - // 10 takes 1/10th - let scaled = scale_reward_by_uptime(epoch_reward, 10).unwrap(); - let expected = Decimal::from_ratio(5u128, 1000u128); - assert_eq!(expected, scaled); - - // anything larger than 100 returns an error - assert!(scale_reward_by_uptime(epoch_reward, 101).is_err()) - } - #[test] fn identity_and_owner_deserialization() { assert!(extract_identity_and_owner(vec![]).is_err()); diff --git a/contracts/mixnet/src/queries.rs b/contracts/mixnet/src/queries.rs index e6f0596a44..0239943287 100644 --- a/contracts/mixnet/src/queries.rs +++ b/contracts/mixnet/src/queries.rs @@ -6,23 +6,23 @@ use crate::helpers::get_all_delegations_paged; use crate::storage::{ all_mix_delegations_read, circulating_supply, config_read, gateways_owners_read, gateways_read, mix_delegations_read, mixnodes_owners_read, mixnodes_read, read_layer_distribution, - read_state_params, reverse_mix_delegations_read, reward_pool_value, + read_state_params, reverse_mix_delegations_read, reward_pool_value, rewarded_mixnodes_read, }; use config::defaults::DENOM; use cosmwasm_std::{coin, Addr, Deps, Order, StdResult, Uint128}; use mixnet_contract::{ Delegation, GatewayBond, GatewayOwnershipResponse, IdentityKey, LayerDistribution, MixNodeBond, - MixOwnershipResponse, PagedAllDelegationsResponse, PagedGatewayResponse, - PagedMixDelegationsResponse, PagedMixnodeResponse, PagedReverseMixDelegationsResponse, - RawDelegationData, RewardingIntervalResponse, StateParams, + MixOwnershipResponse, MixnodeRewardingStatusResponse, PagedAllDelegationsResponse, + PagedGatewayResponse, PagedMixDelegationsResponse, PagedMixnodeResponse, + PagedReverseMixDelegationsResponse, RawDelegationData, RewardingIntervalResponse, StateParams, }; const BOND_PAGE_MAX_LIMIT: u32 = 100; const BOND_PAGE_DEFAULT_LIMIT: u32 = 50; // currently the maximum limit before running into memory issue is somewhere between 1150 and 1200 -pub(crate) const DELEGATION_PAGE_MAX_LIMIT: u32 = 750; -pub(crate) const DELEGATION_PAGE_DEFAULT_LIMIT: u32 = 500; +const DELEGATION_PAGE_MAX_LIMIT: u32 = 500; +const DELEGATION_PAGE_DEFAULT_LIMIT: u32 = 250; pub fn query_mixnodes_paged( deps: Deps, @@ -226,6 +226,17 @@ pub(crate) fn query_mixnode_delegation( } } +pub(crate) fn query_rewarding_status( + deps: Deps, + mix_identity: IdentityKey, + rewarding_interval_nonce: u32, +) -> StdResult { + let status = rewarded_mixnodes_read(deps.storage, rewarding_interval_nonce) + .may_load(mix_identity.as_bytes())?; + + Ok(MixnodeRewardingStatusResponse { status }) +} + #[cfg(test)] pub(crate) mod tests { use super::*; @@ -581,19 +592,14 @@ pub(crate) mod tests { owner: Addr::unchecked("someowner"), rewarding_validator_address: Addr::unchecked("monitor"), params: StateParams { - epoch_length: 1, minimum_mixnode_bond: 123u128.into(), minimum_gateway_bond: 456u128.into(), - mixnode_bond_reward_rate: "1.23".parse().unwrap(), - mixnode_delegation_reward_rate: "7.89".parse().unwrap(), mixnode_rewarded_set_size: 1000, 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(), }; config(deps.as_mut().storage).save(&dummy_state).unwrap(); @@ -1114,4 +1120,245 @@ pub(crate) mod tests { assert_eq!(2, page2.delegated_nodes.len()); } } + + #[cfg(test)] + mod querying_for_rewarding_status { + use super::*; + use crate::support::tests::helpers::{add_mixnode, node_rewarding_params_fixture}; + use crate::transactions::{ + try_add_mixnode, try_begin_mixnode_rewarding, try_delegate_to_mixnode, + try_finish_mixnode_rewarding, try_reward_mixnode_v2, + try_reward_next_mixnode_delegators_v2, MINIMUM_BLOCK_AGE_FOR_REWARDING, + }; + use mixnet_contract::{RewardingResult, RewardingStatus, MIXNODE_DELEGATORS_PAGE_LIMIT}; + + #[test] + fn returns_empty_status_for_unrewarded_nodes() { + 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 node_identity = add_mixnode("bob", good_mixnode_bond(), &mut deps); + + assert!( + query_rewarding_status(deps.as_ref(), node_identity.clone(), 1) + .unwrap() + .status + .is_none() + ); + + // node was rewarded but for different epoch + let info = mock_info(rewarding_validator_address.as_ref(), &[]); + try_begin_mixnode_rewarding(deps.as_mut(), env.clone(), info.clone(), 1).unwrap(); + try_reward_mixnode_v2( + deps.as_mut(), + env.clone(), + info.clone(), + node_identity.clone(), + node_rewarding_params_fixture(100), + 1, + ) + .unwrap(); + try_finish_mixnode_rewarding(deps.as_mut(), info.clone(), 1).unwrap(); + + assert!(query_rewarding_status(deps.as_ref(), node_identity, 2) + .unwrap() + .status + .is_none()); + } + + #[test] + fn returns_complete_status_for_fully_rewarded_node() { + // with single page + let mut deps = helpers::init_contract(); + let mut env = mock_env(); + let current_state = config_read(deps.as_mut().storage).load().unwrap(); + let rewarding_validator_address = current_state.rewarding_validator_address; + + let node_identity = "bobsnode".to_string(); + try_add_mixnode( + deps.as_mut(), + env.clone(), + mock_info("bob", &good_mixnode_bond()), + MixNode { + identity_key: node_identity.clone(), + ..helpers::mix_node_fixture() + }, + ) + .unwrap(); + + env.block.height += MINIMUM_BLOCK_AGE_FOR_REWARDING; + + let info = mock_info(rewarding_validator_address.as_ref(), &[]); + try_begin_mixnode_rewarding(deps.as_mut(), env.clone(), info.clone(), 1).unwrap(); + try_reward_mixnode_v2( + deps.as_mut(), + env.clone(), + info.clone(), + node_identity.clone(), + node_rewarding_params_fixture(100), + 1, + ) + .unwrap(); + try_finish_mixnode_rewarding(deps.as_mut(), info.clone(), 1).unwrap(); + + let res = query_rewarding_status(deps.as_ref(), node_identity, 1).unwrap(); + assert!(matches!(res.status, Some(RewardingStatus::Complete(..)))); + + match res.status.unwrap() { + RewardingStatus::Complete(result) => { + assert_ne!( + RewardingResult::default().operator_reward, + result.operator_reward + ); + assert_eq!( + RewardingResult::default().total_delegator_reward, + result.total_delegator_reward + ); + } + _ => unreachable!(), + } + + // with multiple pages + + let node_identity = "alicesnode".to_string(); + try_add_mixnode( + deps.as_mut(), + env.clone(), + mock_info("alice", &good_mixnode_bond()), + MixNode { + identity_key: node_identity.clone(), + ..helpers::mix_node_fixture() + }, + ) + .unwrap(); + + for i in 0..MIXNODE_DELEGATORS_PAGE_LIMIT + 123 { + try_delegate_to_mixnode( + deps.as_mut(), + env.clone(), + mock_info( + &*format!("delegator{:04}", i), + &vec![coin(200_000000, DENOM)], + ), + node_identity.clone(), + ) + .unwrap(); + } + + env.block.height += MINIMUM_BLOCK_AGE_FOR_REWARDING; + + let info = mock_info(rewarding_validator_address.as_ref(), &[]); + try_begin_mixnode_rewarding(deps.as_mut(), env.clone(), info.clone(), 2).unwrap(); + + try_reward_mixnode_v2( + deps.as_mut(), + env.clone(), + info.clone(), + node_identity.clone(), + node_rewarding_params_fixture(100), + 2, + ) + .unwrap(); + + // rewards all pending + try_reward_next_mixnode_delegators_v2( + deps.as_mut(), + info.clone(), + node_identity.to_string(), + 2, + ) + .unwrap(); + + let res = query_rewarding_status(deps.as_ref(), node_identity, 2).unwrap(); + assert!(matches!(res.status, Some(RewardingStatus::Complete(..)))); + + match res.status.unwrap() { + RewardingStatus::Complete(result) => { + assert_ne!( + RewardingResult::default().operator_reward, + result.operator_reward + ); + assert_ne!( + RewardingResult::default().total_delegator_reward, + result.total_delegator_reward + ); + } + _ => unreachable!(), + } + } + + #[test] + fn returns_pending_next_delegator_page_status_when_there_are_more_delegators_to_reward() { + let mut deps = helpers::init_contract(); + let mut env = mock_env(); + let current_state = config_read(deps.as_mut().storage).load().unwrap(); + let rewarding_validator_address = current_state.rewarding_validator_address; + + let node_identity = "bobsnode".to_string(); + try_add_mixnode( + deps.as_mut(), + env.clone(), + mock_info("bob", &good_mixnode_bond()), + MixNode { + identity_key: node_identity.clone(), + ..helpers::mix_node_fixture() + }, + ) + .unwrap(); + + for i in 0..MIXNODE_DELEGATORS_PAGE_LIMIT + 123 { + try_delegate_to_mixnode( + deps.as_mut(), + env.clone(), + mock_info( + &*format!("delegator{:04}", i), + &vec![coin(200_000000, DENOM)], + ), + node_identity.clone(), + ) + .unwrap(); + } + + env.block.height += MINIMUM_BLOCK_AGE_FOR_REWARDING; + + let info = mock_info(rewarding_validator_address.as_ref(), &[]); + try_begin_mixnode_rewarding(deps.as_mut(), env.clone(), info.clone(), 1).unwrap(); + + try_reward_mixnode_v2( + deps.as_mut(), + env.clone(), + info, + node_identity.clone(), + node_rewarding_params_fixture(100), + 1, + ) + .unwrap(); + + let res = query_rewarding_status(deps.as_ref(), node_identity, 1).unwrap(); + assert!(matches!( + res.status, + Some(RewardingStatus::PendingNextDelegatorPage(..)) + )); + + match res.status.unwrap() { + RewardingStatus::PendingNextDelegatorPage(result) => { + assert_ne!( + RewardingResult::default().operator_reward, + result.running_results.operator_reward + ); + assert_ne!( + RewardingResult::default().total_delegator_reward, + result.running_results.total_delegator_reward + ); + assert_eq!( + &*format!("delegator{:04}", MIXNODE_DELEGATORS_PAGE_LIMIT), + result.next_start + ); + } + _ => unreachable!(), + } + } + } } diff --git a/contracts/mixnet/src/state.rs b/contracts/mixnet/src/state.rs index 4440c776eb..e436c2b64d 100644 --- a/contracts/mixnet/src/state.rs +++ b/contracts/mixnet/src/state.rs @@ -1,7 +1,7 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use cosmwasm_std::{Addr, Decimal}; +use cosmwasm_std::Addr; use mixnet_contract::StateParams; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -18,8 +18,4 @@ pub struct State { 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 - pub mixnode_epoch_bond_reward: Decimal, // reward per epoch expressed as a decimal like 0.05 - pub mixnode_epoch_delegation_reward: Decimal, // reward per epoch expressed as a decimal like 0.05 } diff --git a/contracts/mixnet/src/storage.rs b/contracts/mixnet/src/storage.rs index ec690dce16..ffd32696aa 100644 --- a/contracts/mixnet/src/storage.rs +++ b/contracts/mixnet/src/storage.rs @@ -1,19 +1,17 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use crate::contract::INITIAL_REWARD_POOL; +use crate::error::ContractError; use crate::state::State; -use crate::transactions::MINIMUM_BLOCK_AGE_FOR_REWARDING; -use crate::{error::ContractError, queries}; use config::defaults::TOTAL_SUPPLY; -use cosmwasm_std::{Decimal, Order, StdResult, Storage, Uint128}; +use cosmwasm_std::{StdResult, Storage, Uint128}; use cosmwasm_storage::{ bucket, bucket_read, singleton, singleton_read, Bucket, ReadonlyBucket, ReadonlySingleton, Singleton, }; -use mixnet_contract::mixnode::NodeRewardParams; use mixnet_contract::{ Addr, GatewayBond, IdentityKey, IdentityKeyRef, Layer, LayerDistribution, MixNodeBond, - RawDelegationData, StateParams, + RawDelegationData, RewardingStatus, StateParams, }; use serde::de::DeserializeOwned; use serde::Serialize; @@ -185,7 +183,10 @@ pub fn mixnodes_owners_read(storage: &dyn Storage) -> ReadonlyBucket Bucket { +pub(crate) fn rewarded_mixnodes( + storage: &mut dyn Storage, + rewarding_interval_nonce: u32, +) -> Bucket { Bucket::multilevel( storage, &[ @@ -195,13 +196,10 @@ pub fn rewarded_mixnodes(storage: &mut dyn Storage, rewarding_interval_nonce: u3 ) } -// 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( +pub(crate) fn rewarded_mixnodes_read( storage: &dyn Storage, rewarding_interval_nonce: u32, -) -> ReadonlyBucket { +) -> ReadonlyBucket { ReadonlyBucket::multilevel( storage, &[ @@ -211,105 +209,6 @@ pub fn rewarded_mixnodes_read( ) } -// helpers -pub(crate) fn increase_mix_delegated_stakes( - storage: &mut dyn Storage, - mix_identity: IdentityKeyRef, - scaled_reward_rate: Decimal, - reward_blockstamp: u64, -) -> StdResult { - let chunk_size = queries::DELEGATION_PAGE_MAX_LIMIT as usize; - - let mut total_rewarded = Uint128::zero(); - let mut chunk_start: Option> = None; - loop { - // get `chunk_size` of delegations - let delegations_chunk = mix_delegations_read(storage, mix_identity) - .range(chunk_start.as_deref(), None, Order::Ascending) - .take(chunk_size) - .collect::>>()?; - - if delegations_chunk.is_empty() { - break; - } - - // append 0 byte to the last value to start with whatever is the next succeeding key - chunk_start = Some( - delegations_chunk - .last() - .unwrap() - .0 - .iter() - .cloned() - .chain(std::iter::once(0u8)) - .collect(), - ); - - // and for each of them increase the stake proportionally to the reward - // if at least `MINIMUM_BLOCK_AGE_FOR_REWARDING` blocks have been created - // since they delegated - for (delegator_address, mut delegation) in delegations_chunk.into_iter() { - if delegation.block_height + MINIMUM_BLOCK_AGE_FOR_REWARDING <= reward_blockstamp { - let reward = delegation.amount * scaled_reward_rate; - delegation.amount += reward; - total_rewarded += reward; - mix_delegations(storage, mix_identity).save(&delegator_address, &delegation)?; - } - } - } - - Ok(total_rewarded) -} - -pub(crate) fn increase_mix_delegated_stakes_v2( - storage: &mut dyn Storage, - bond: &MixNodeBond, - params: &NodeRewardParams, -) -> Result { - let chunk_size = queries::DELEGATION_PAGE_MAX_LIMIT as usize; - - let mut total_rewarded = Uint128::zero(); - let mut chunk_start: Option> = None; - loop { - // get `chunk_size` of delegations - let delegations_chunk = mix_delegations_read(storage, bond.identity()) - .range(chunk_start.as_deref(), None, Order::Ascending) - .take(chunk_size) - .collect::>>()?; - - if delegations_chunk.is_empty() { - break; - } - - // append 0 byte to the last value to start with whatever is the next succeeding key - chunk_start = Some( - delegations_chunk - .last() - .unwrap() - .0 - .iter() - .cloned() - .chain(std::iter::once(0u8)) - .collect(), - ); - - // and for each of them increase the stake proportionally to the reward - // if at least `MINIMUM_BLOCK_AGE_FOR_REWARDING` blocks have been created - // since they delegated - for (delegator_address, mut delegation) in delegations_chunk.into_iter() { - if delegation.block_height + MINIMUM_BLOCK_AGE_FOR_REWARDING - <= params.reward_blockstamp() - { - let reward = bond.reward_delegation(delegation.amount, params); - delegation.amount += Uint128(reward); - total_rewarded += Uint128(reward); - mix_delegations(storage, bond.identity()).save(&delegator_address, &delegation)?; - } - } - } - - Ok(total_rewarded) -} // currently not used outside tests #[cfg(test)] pub(crate) fn read_mixnode_bond( @@ -401,7 +300,6 @@ mod tests { use crate::helpers::identity_and_owner_to_bytes; use crate::support::tests::helpers::{ gateway_bond_fixture, gateway_fixture, mix_node_fixture, mixnode_bond_fixture, - raw_delegation_fixture, }; use config::defaults::DENOM; use cosmwasm_std::testing::{mock_dependencies, MockStorage}; @@ -539,216 +437,6 @@ mod tests { assert_eq!(raw_delegation2, res2); } - #[cfg(test)] - mod increasing_mix_delegated_stakes { - use super::*; - use crate::queries::query_mixnode_delegations_paged; - use cosmwasm_std::testing::mock_dependencies; - - #[test] - fn when_there_are_no_delegations() { - let mut deps = mock_dependencies(&[]); - let node_identity: IdentityKey = "nodeidentity".into(); - - // 0.001 - let reward = Decimal::from_ratio(1u128, 1000u128); - - let total_increase = increase_mix_delegated_stakes( - &mut deps.storage, - node_identity.as_ref(), - reward, - 42, - ) - .unwrap(); - - // there was no increase - assert!(total_increase.is_zero()); - - // there are no 'new' delegations magically added - assert!( - query_mixnode_delegations_paged(deps.as_ref(), node_identity, None, None) - .unwrap() - .delegations - .is_empty() - ) - } - - #[test] - fn when_there_is_a_single_delegation() { - let mut deps = mock_dependencies(&[]); - let node_identity: IdentityKey = "nodeidentity".into(); - let delegation_blockstamp = 42; - - // 0.001 - let reward = Decimal::from_ratio(1u128, 1000u128); - - let delegator_address = Addr::unchecked("bob"); - mix_delegations(&mut deps.storage, &node_identity) - .save( - delegator_address.as_bytes(), - &RawDelegationData::new(1000u128.into(), delegation_blockstamp), - ) - .unwrap(); - - let total_increase = increase_mix_delegated_stakes( - &mut deps.storage, - node_identity.as_ref(), - reward, - delegation_blockstamp + 2 * MINIMUM_BLOCK_AGE_FOR_REWARDING, - ) - .unwrap(); - - assert_eq!(Uint128(1), total_increase); - - // amount is incremented, block height remains the same - assert_eq!( - RawDelegationData::new(1001u128.into(), 42), - mix_delegations_read(&mut deps.storage, &node_identity) - .load(delegator_address.as_bytes()) - .unwrap() - ) - } - - #[test] - fn when_there_is_a_single_delegation_depending_on_blockstamp() { - let mut deps = mock_dependencies(&[]); - let node_identity: IdentityKey = "nodeidentity".into(); - let delegation_blockstamp = 42; - - // 0.001 - let reward = Decimal::from_ratio(1u128, 1000u128); - - let delegator_address = Addr::unchecked("bob"); - mix_delegations(&mut deps.storage, &node_identity) - .save( - delegator_address.as_bytes(), - &RawDelegationData::new(1000u128.into(), delegation_blockstamp), - ) - .unwrap(); - - let total_increase = increase_mix_delegated_stakes( - &mut deps.storage, - node_identity.as_ref(), - reward, - delegation_blockstamp + MINIMUM_BLOCK_AGE_FOR_REWARDING - 1, - ) - .unwrap(); - - // there was no increase - assert!(total_increase.is_zero()); - - // amount is not incremented - assert_eq!( - RawDelegationData::new(1000u128.into(), delegation_blockstamp), - mix_delegations_read(&mut deps.storage, &node_identity) - .load(delegator_address.as_bytes()) - .unwrap() - ); - - let total_increase = increase_mix_delegated_stakes( - &mut deps.storage, - node_identity.as_ref(), - reward, - delegation_blockstamp + MINIMUM_BLOCK_AGE_FOR_REWARDING, - ) - .unwrap(); - - // there is an increase now, that the lock period has passed - assert_eq!(Uint128(1), total_increase); - - // amount is incremented - assert_eq!( - RawDelegationData::new(1001u128.into(), delegation_blockstamp), - mix_delegations_read(&mut deps.storage, &node_identity) - .load(delegator_address.as_bytes()) - .unwrap() - ) - } - - #[test] - fn when_there_are_multiple_delegations() { - let mut deps = mock_dependencies(&[]); - let node_identity: IdentityKey = "nodeidentity".into(); - let delegation_blockstamp = 42; - - // 0.001 - let reward = Decimal::from_ratio(1u128, 1000u128); - - for i in 0..100 { - let delegator_address = Addr::unchecked(format!("address{}", i)); - mix_delegations(&mut deps.storage, &node_identity) - .save( - delegator_address.as_bytes(), - &RawDelegationData::new(1000u128.into(), delegation_blockstamp), - ) - .unwrap(); - } - - let total_increase = increase_mix_delegated_stakes( - &mut deps.storage, - node_identity.as_ref(), - reward, - delegation_blockstamp + 2 * MINIMUM_BLOCK_AGE_FOR_REWARDING, - ) - .unwrap(); - - assert_eq!(Uint128(100), total_increase); - - for i in 0..100 { - let delegator_address = Addr::unchecked(format!("address{}", i)); - assert_eq!( - raw_delegation_fixture(1001), - mix_delegations_read(&mut deps.storage, &node_identity) - .load(delegator_address.as_bytes()) - .unwrap() - ) - } - } - - #[test] - fn when_there_are_more_delegations_than_page_size() { - let mut deps = mock_dependencies(&[]); - let node_identity: IdentityKey = "nodeidentity".into(); - let delegation_blockstamp = 42; - - // 0.001 - let reward = Decimal::from_ratio(1u128, 1000u128); - - for i in 0..queries::DELEGATION_PAGE_MAX_LIMIT * 10 { - let delegator_address = Addr::unchecked(format!("address{}", i)); - mix_delegations(&mut deps.storage, &node_identity) - .save( - delegator_address.as_bytes(), - &RawDelegationData::new(1000u128.into(), delegation_blockstamp), - ) - .unwrap(); - } - - let total_increase = increase_mix_delegated_stakes( - &mut deps.storage, - node_identity.as_ref(), - reward, - delegation_blockstamp + 2 * MINIMUM_BLOCK_AGE_FOR_REWARDING, - ) - .unwrap(); - - assert_eq!( - Uint128(queries::DELEGATION_PAGE_MAX_LIMIT as u128 * 10), - total_increase - ); - - for i in 0..queries::DELEGATION_PAGE_MAX_LIMIT * 10 { - let delegator_address = Addr::unchecked(format!("address{}", i)); - assert_eq!( - raw_delegation_fixture(1001), - mix_delegations_read(&mut deps.storage, &node_identity) - .load(delegator_address.as_bytes()) - .unwrap() - ) - } - } - } - #[cfg(test)] mod reverse_mix_delegations { use super::*; diff --git a/contracts/mixnet/src/support/tests.rs b/contracts/mixnet/src/support/tests.rs index 0b36814fb7..c49b8cb160 100644 --- a/contracts/mixnet/src/support/tests.rs +++ b/contracts/mixnet/src/support/tests.rs @@ -1,10 +1,12 @@ #[cfg(test)] pub mod helpers { use super::*; - use crate::contract::query; use crate::contract::{instantiate, INITIAL_MIXNODE_BOND}; + use crate::contract::{ + query, DEFAULT_SYBIL_RESISTANCE_PERCENT, EPOCH_REWARD_PERCENT, INITIAL_REWARD_POOL, + }; use crate::transactions::{try_add_gateway, try_add_mixnode}; - use config::defaults::DENOM; + use config::defaults::{DENOM, TOTAL_SUPPLY}; use cosmwasm_std::from_binary; use cosmwasm_std::testing::mock_dependencies; use cosmwasm_std::testing::mock_env; @@ -17,6 +19,7 @@ pub mod helpers { use cosmwasm_std::OwnedDeps; use cosmwasm_std::{coin, Uint128}; use cosmwasm_std::{Empty, MemoryStorage}; + use mixnet_contract::mixnode::NodeRewardParams; use mixnet_contract::{ Gateway, GatewayBond, InstantiateMsg, Layer, MixNode, MixNodeBond, PagedGatewayResponse, PagedMixnodeResponse, QueryMsg, RawDelegationData, @@ -188,4 +191,16 @@ pub mod helpers { amount: INITIAL_MIXNODE_BOND, }] } + + // when exact values are irrelevant and what matters is the action of rewarding + pub fn node_rewarding_params_fixture(uptime: u128) -> NodeRewardParams { + NodeRewardParams::new( + (INITIAL_REWARD_POOL / 100) * EPOCH_REWARD_PERCENT as u128, + 50 as u128, + 0, + TOTAL_SUPPLY - INITIAL_REWARD_POOL, + uptime, + DEFAULT_SYBIL_RESISTANCE_PERCENT, + ) + } } diff --git a/contracts/mixnet/src/transactions.rs b/contracts/mixnet/src/transactions/mod.rs similarity index 56% rename from contracts/mixnet/src/transactions.rs rename to contracts/mixnet/src/transactions/mod.rs index de5e3f5f07..f0701701e3 100644 --- a/contracts/mixnet/src/transactions.rs +++ b/contracts/mixnet/src/transactions/mod.rs @@ -1,19 +1,21 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use crate::error::ContractError; -use crate::helpers::{calculate_epoch_reward_rate, scale_reward_by_uptime, Delegations}; +use crate::helpers::Delegations; use crate::queries; use crate::storage::*; use config::defaults::DENOM; use cosmwasm_std::{ - attr, coins, BankMsg, Coin, Decimal, DepsMut, Env, MessageInfo, Response, StdResult, Uint128, + attr, coins, BankMsg, Coin, DepsMut, Env, MessageInfo, Response, StdResult, Uint128, }; use cosmwasm_storage::ReadonlyBucket; -use mixnet_contract::mixnode::NodeRewardParams; use mixnet_contract::{ Gateway, GatewayBond, IdentityKey, Layer, MixNode, MixNodeBond, RawDelegationData, StateParams, }; +pub(crate) mod rewarding; +pub(crate) use rewarding::*; + pub(crate) const OLD_DELEGATIONS_CHUNK_SIZE: usize = 500; // approximately 1 day (assuming 5s per block) @@ -312,12 +314,12 @@ pub(crate) fn try_update_state_params( return Err(ContractError::Unauthorized); } - if params.mixnode_bond_reward_rate < Decimal::one() { - return Err(ContractError::DecreasingMixnodeBondReward); + if params.mixnode_rewarded_set_size == 0 { + return Err(ContractError::ZeroRewardedSet); } - if params.mixnode_delegation_reward_rate < Decimal::one() { - return Err(ContractError::DecreasingMixnodeDelegationReward); + if params.mixnode_active_set_size == 0 { + return Err(ContractError::ZeroActiveSet); } // note: rewarded_set = active_set + idle_set @@ -326,26 +328,6 @@ pub(crate) fn try_update_state_params( return Err(ContractError::InvalidActiveSetSize); } - // if we're updating epoch length, recalculate rewards for mixnodes - if state.params.epoch_length != params.epoch_length { - state.mixnode_epoch_bond_reward = - calculate_epoch_reward_rate(params.epoch_length, params.mixnode_bond_reward_rate); - state.mixnode_epoch_delegation_reward = - calculate_epoch_reward_rate(params.epoch_length, params.mixnode_delegation_reward_rate); - } else { - // if mixnode rewards changed, recalculate respective values - if state.params.mixnode_bond_reward_rate != params.mixnode_bond_reward_rate { - state.mixnode_epoch_bond_reward = - calculate_epoch_reward_rate(params.epoch_length, params.mixnode_bond_reward_rate); - } - if state.params.mixnode_delegation_reward_rate != params.mixnode_delegation_reward_rate { - state.mixnode_epoch_delegation_reward = calculate_epoch_reward_rate( - params.epoch_length, - params.mixnode_delegation_reward_rate, - ); - } - } - state.params = params; config(deps.storage).save(&state)?; @@ -353,277 +335,6 @@ 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} -// must be updated appropriately. -pub(crate) fn try_reward_mixnode( - deps: DepsMut, - env: Env, - info: MessageInfo, - mix_identity: IdentityKey, - uptime: u32, - rewarding_interval_nonce: u32, -) -> Result { - let 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 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![], - attributes: vec![ - attr("bond increase", Uint128(0)), - attr("total delegation increase", Uint128(0)), - ], - data: None, - }); - } - - // check if the bond even exists - let mut current_bond = match mixnodes_read(deps.storage).load(mix_identity.as_bytes()) { - Ok(bond) => bond, - Err(_) => { - return Ok(Response { - attributes: vec![attr("result", "bond not found")], - ..Default::default() - }); - } - }; - - let mut node_reward = Uint128(0); - 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![], - attributes: vec![ - attr("bond increase", node_reward), - attr("total delegation increase", total_delegation_reward), - ], - data: None, - }) -} - -pub(crate) fn try_reward_mixnode_v2( - deps: DepsMut, - env: Env, - info: MessageInfo, - mix_identity: IdentityKey, - params: NodeRewardParams, - rewarding_interval_nonce: u32, -) -> Result { - let 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 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, - Err(_) => { - return Ok(Response { - attributes: vec![attr("result", "bond not found")], - ..Default::default() - }); - } - }; - - let mut reward_params = params; - - reward_params.set_reward_blockstamp(env.block.height); - - let reward_result = current_bond.reward(&reward_params); - - // Omitting the price per packet function now, it follows that base operator reward is the node_reward - let operator_reward = current_bond.operator_reward(&reward_params); - - let total_delegation_reward = - increase_mix_delegated_stakes_v2(deps.storage, ¤t_bond, &reward_params)?; - - // 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 - <= reward_params.reward_blockstamp() - { - current_bond.bond_amount.amount += Uint128(operator_reward); - current_bond.total_delegation.amount += total_delegation_reward; - mixnodes(deps.storage).save(mix_identity.as_bytes(), ¤t_bond)?; - decr_reward_pool(Uint128(operator_reward), deps.storage)?; - 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![], - attributes: vec![ - attr("bond increase", reward_result.reward()), - attr("total delegation increase", total_delegation_reward), - ], - 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() { @@ -740,23 +451,17 @@ pub(crate) fn try_remove_delegation_from_mixnode( #[cfg(test)] pub mod tests { use super::*; - use crate::contract::{ - execute, query, DEFAULT_SYBIL_RESISTANCE_PERCENT, INITIAL_DEFAULT_EPOCH_LENGTH, - INITIAL_GATEWAY_BOND, INITIAL_MIXNODE_BOND, INITIAL_MIXNODE_BOND_REWARD_RATE, - INITIAL_MIXNODE_DELEGATION_REWARD_RATE, - }; - use crate::helpers::calculate_epoch_reward_rate; - use crate::queries::DELEGATION_PAGE_DEFAULT_LIMIT; - use crate::storage::{layer_distribution_read, mix_delegations_read, read_mixnode_bond}; + use crate::contract::{execute, query, INITIAL_GATEWAY_BOND, INITIAL_MIXNODE_BOND}; + use crate::storage::{layer_distribution_read, mix_delegations_read}; use crate::support::tests::helpers; use crate::support::tests::helpers::{ - add_mixnode, good_gateway_bond, good_mixnode_bond, mix_node_fixture, raw_delegation_fixture, + good_gateway_bond, good_mixnode_bond, raw_delegation_fixture, }; use cosmwasm_std::testing::{mock_env, mock_info}; use cosmwasm_std::{coin, coins, from_binary, Addr, Uint128}; use mixnet_contract::{ ExecuteMsg, LayerDistribution, PagedGatewayResponse, PagedMixnodeResponse, QueryMsg, - UnpackedDelegation, + UnpackedDelegation, MIXNODE_DELEGATORS_PAGE_LIMIT, }; use queries::tests::store_n_mix_delegations; @@ -1600,13 +1305,8 @@ pub mod tests { let mut deps = helpers::init_contract(); let new_params = StateParams { - epoch_length: INITIAL_DEFAULT_EPOCH_LENGTH, minimum_mixnode_bond: INITIAL_MIXNODE_BOND, minimum_gateway_bond: INITIAL_GATEWAY_BOND, - mixnode_bond_reward_rate: Decimal::percent(INITIAL_MIXNODE_BOND_REWARD_RATE), - mixnode_delegation_reward_rate: Decimal::percent( - INITIAL_MIXNODE_DELEGATION_REWARD_RATE, - ), mixnode_rewarded_set_size: 100, mixnode_active_set_size: 50, }; @@ -1625,848 +1325,26 @@ pub mod tests { let current_state = config_read(deps.as_ref().storage).load().unwrap(); assert_eq!(current_state.params, new_params); - // mixnode_epoch_rewards are recalculated if annual reward is changed - 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); - - // sanity check to make sure we are actually updating the values (in case we changed defaults at some point) - assert_ne!(new_mixnode_bond_reward_rate, current_mix_bond_reward_rate); - assert_ne!( - new_mixnode_delegation_reward_rate, - current_mix_delegation_reward_rate - ); - - let mut new_params = current_state.params.clone(); - new_params.mixnode_bond_reward_rate = new_mixnode_bond_reward_rate; - new_params.mixnode_delegation_reward_rate = new_mixnode_delegation_reward_rate; - - let info = mock_info("creator", &[]); - try_update_state_params(deps.as_mut(), info, new_params.clone()).unwrap(); - - let new_state = config_read(deps.as_ref().storage).load().unwrap(); - let expected_bond = - calculate_epoch_reward_rate(new_params.epoch_length, new_mixnode_bond_reward_rate); - let expected_delegation = calculate_epoch_reward_rate( - new_params.epoch_length, - new_mixnode_delegation_reward_rate, - ); - assert_eq!(expected_bond, new_state.mixnode_epoch_bond_reward); - assert_eq!( - expected_delegation, - new_state.mixnode_epoch_delegation_reward - ); - - // mixnode_epoch_rewards is updated on epoch length change - let new_epoch_length = 42; - // sanity check to make sure we are actually updating the value (in case we changed defaults at some point) - assert_ne!(new_epoch_length, current_state.params.epoch_length); - let mut new_params = current_state.params.clone(); - new_params.epoch_length = new_epoch_length; - - let info = mock_info("creator", &[]); - try_update_state_params(deps.as_mut(), info, new_params.clone()).unwrap(); - - let new_state = config_read(deps.as_ref().storage).load().unwrap(); - let expected_mixnode_bond = - calculate_epoch_reward_rate(new_epoch_length, new_params.mixnode_bond_reward_rate); - let expected_mixnode_delegation = calculate_epoch_reward_rate( - new_epoch_length, - new_params.mixnode_delegation_reward_rate, - ); - assert_eq!(expected_mixnode_bond, new_state.mixnode_epoch_bond_reward); - assert_eq!( - expected_mixnode_delegation, - new_state.mixnode_epoch_delegation_reward - ); - // error is thrown if rewarded set is smaller than the active set let info = mock_info("creator", &[]); let mut new_params = current_state.params.clone(); new_params.mixnode_rewarded_set_size = new_params.mixnode_active_set_size - 1; let res = try_update_state_params(deps.as_mut(), info, new_params.clone()); - 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_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, - 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 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; - let initial_delegation = 200_000000; - let mixnode_bond = MixNodeBond { - bond_amount: coin(initial_bond, DENOM), - total_delegation: coin(initial_delegation, DENOM), - owner: node_owner.clone(), - layer: Layer::One, - block_height: env.block.height, - mix_node: MixNode { - identity_key: node_identity.clone(), - ..mix_node_fixture() - }, - profit_margin_percent: Some(10), - }; - - mixnodes(deps.as_mut().storage) - .save(node_identity.as_bytes(), &mixnode_bond) - .unwrap(); - - mix_delegations(&mut deps.storage, &node_identity) - .save( - b"delegator", - &RawDelegationData::new(initial_delegation.into(), env.block.height), - ) - .unwrap(); - - env.block.height += 2 * MINIMUM_BLOCK_AGE_FOR_REWARDING; - - 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; - - // the node's bond and delegations are correctly increased and scaled by uptime - // if node was 100% up, it will get full epoch reward - let expected_bond = expected_bond_reward + Uint128(initial_bond); - let expected_delegation = expected_delegation_reward + Uint128(initial_delegation); - - 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, - read_mixnode_bond(deps.as_ref().storage, node_identity.as_bytes()).unwrap() - ); - assert_eq!( - expected_delegation, - read_mixnode_delegation(deps.as_ref().storage, node_identity.as_bytes()).unwrap() - ); - - assert_eq!( - vec![ - attr("bond increase", expected_bond_reward), - attr("total delegation increase", expected_delegation_reward), - ], - res.attributes - ); - - // if node was 20% up, it will get 1/5th of epoch reward - let scaled_bond_reward = scale_reward_by_uptime(bond_reward_rate, 20).unwrap(); - let scaled_delegation_reward = scale_reward_by_uptime(delegation_reward_rate, 20).unwrap(); - let expected_bond_reward = expected_bond * scaled_bond_reward; - let expected_delegation_reward = expected_delegation * scaled_delegation_reward; - let expected_bond = expected_bond_reward + expected_bond; - let expected_delegation = expected_delegation_reward + expected_delegation; - - 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, - read_mixnode_bond(deps.as_ref().storage, node_identity.as_bytes()).unwrap() - ); - assert_eq!( - expected_delegation, - read_mixnode_delegation(deps.as_ref().storage, node_identity.as_bytes()).unwrap() - ); - - assert_eq!( - vec![ - attr("bond increase", expected_bond_reward), - attr("total delegation increase", expected_delegation_reward), - ], - res.attributes - ); - } - - #[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_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(); - - let initial_bond = 100_000000; - let initial_delegation = 200_000000; - let mixnode_bond = MixNodeBond { - bond_amount: coin(initial_bond, DENOM), - total_delegation: coin(initial_delegation, DENOM), - owner: node_owner.clone(), - layer: Layer::One, - block_height: env.block.height, - mix_node: MixNode { - identity_key: node_identity.clone(), - ..mix_node_fixture() - }, - profit_margin_percent: Some(10), - }; - - mixnodes(deps.as_mut().storage) - .save(node_identity.as_bytes(), &mixnode_bond) - .unwrap(); - - // delegation happens later, but not later enough - env.block.height += MINIMUM_BLOCK_AGE_FOR_REWARDING - 1; - - mix_delegations(&mut deps.storage, &node_identity) - .save( - b"delegator", - &RawDelegationData::new(initial_delegation.into(), env.block.height), - ) - .unwrap(); - - 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(); - - // no reward is due - let expected_bond_reward = Uint128(0); - let expected_delegation_reward = Uint128(0); - let expected_bond = expected_bond_reward + Uint128(initial_bond); - let expected_delegation = expected_delegation_reward + Uint128(initial_delegation); - - 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, - read_mixnode_bond(deps.as_ref().storage, node_identity.as_bytes()).unwrap() - ); - assert_eq!( - expected_delegation, - read_mixnode_delegation(deps.as_ref().storage, node_identity.as_bytes()).unwrap() - ); - - assert_eq!( - vec![ - attr("bond increase", expected_bond_reward), - attr("total delegation increase", expected_delegation_reward), - ], - res.attributes - ); - - // reward can happen now, but only for bonded node - env.block.height += 1; - let expected_bond_reward = expected_bond * scaled_bond_reward; - let expected_delegation_reward = Uint128(0); - let expected_bond = expected_bond_reward + expected_bond; - let expected_delegation = expected_delegation_reward + expected_delegation; - - 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, - read_mixnode_bond(deps.as_ref().storage, node_identity.as_bytes()).unwrap() - ); - assert_eq!( - expected_delegation, - read_mixnode_delegation(deps.as_ref().storage, node_identity.as_bytes()).unwrap() - ); - - assert_eq!( - vec![ - attr("bond increase", expected_bond_reward), - attr("total delegation increase", expected_delegation_reward), - ], - res.attributes - ); - - // reward happens now, both for node owner and delegators - env.block.height += MINIMUM_BLOCK_AGE_FOR_REWARDING - 1; - let expected_bond_reward = expected_bond * scaled_bond_reward; - let expected_delegation_reward = expected_delegation * scaled_delegation_reward; - let expected_bond = expected_bond_reward + expected_bond; - let expected_delegation = expected_delegation_reward + expected_delegation; - - 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, - read_mixnode_bond(deps.as_ref().storage, node_identity.as_bytes()).unwrap() - ); - assert_eq!( - expected_delegation, - read_mixnode_delegation(deps.as_ref().storage, node_identity.as_bytes()).unwrap() - ); - - assert_eq!( - vec![ - attr("bond increase", expected_bond_reward), - attr("total delegation increase", expected_delegation_reward), - ], - res.attributes - ); + assert_eq!(Err(ContractError::InvalidActiveSetSize), res); + + // error is thrown for 0 size rewarded set + let info = mock_info("creator", &[]); + let mut new_params = current_state.params.clone(); + new_params.mixnode_rewarded_set_size = 0; + let res = try_update_state_params(deps.as_mut(), info, new_params.clone()); + assert_eq!(Err(ContractError::ZeroRewardedSet), res); + + // error is thrown for 0 size active set + let info = mock_info("creator", &[]); + let mut new_params = current_state.params.clone(); + new_params.mixnode_active_set_size = 0; + let res = try_update_state_params(deps.as_mut(), info, new_params.clone()); + assert_eq!(Err(ContractError::ZeroActiveSet), res); } #[cfg(test)] @@ -2532,7 +1410,7 @@ pub mod tests { deps.as_mut(), mock_env(), mock_info("sender", &coins(123, DENOM)), - "non-existent-mix-identity".into() + "non-existent-mix-identity".into(), ) ); } @@ -2549,7 +1427,7 @@ pub mod tests { deps.as_mut(), mock_env(), mock_info(delegation_owner.as_str(), &vec![delegation.clone()]), - identity.clone() + identity.clone(), ) .is_ok()); @@ -2593,7 +1471,7 @@ pub mod tests { deps.as_mut(), mock_env(), mock_info(delegation_owner.as_str(), &coins(123, DENOM)), - identity + identity, ) ); } @@ -2613,7 +1491,7 @@ pub mod tests { deps.as_mut(), mock_env(), mock_info(delegation_owner.as_str(), &vec![delegation.clone()]), - identity.clone() + identity.clone(), ) .is_ok()); @@ -2668,7 +1546,7 @@ pub mod tests { assert_eq!( RawDelegationData::new( delegation1.amount + delegation2.amount, - mock_env().block.height + mock_env().block.height, ), mix_delegations_read(&deps.storage, &identity) .load(delegation_owner.as_bytes()) @@ -2817,7 +1695,7 @@ pub mod tests { deps.as_mut(), mock_env(), mock_info(delegation_owner.as_str(), &coins(50, DENOM)), - identity + identity, ) ); } @@ -2835,7 +1713,7 @@ pub mod tests { deps.as_mut(), mock_env(), mock_info(delegation_owner.as_str(), &coins(123, DENOM)), - identity1.clone() + identity1.clone(), ) .is_ok()); @@ -2843,7 +1721,7 @@ pub mod tests { deps.as_mut(), mock_env(), mock_info(delegation_owner.as_str(), &coins(42, DENOM)), - identity2.clone() + identity2.clone(), ) .is_ok()); @@ -2885,7 +1763,7 @@ pub mod tests { deps.as_mut(), mock_env(), mock_info("sender1", &vec![delegation1.clone()]), - identity.clone() + identity.clone(), ) .is_ok()); @@ -2893,7 +1771,7 @@ pub mod tests { deps.as_mut(), mock_env(), mock_info("sender2", &vec![delegation2.clone()]), - identity.clone() + identity.clone(), ) .is_ok()); @@ -3086,7 +1964,7 @@ pub mod tests { deps.as_mut(), mock_env(), mock_info(delegation_owner1.as_str(), &vec![delegation1.clone()]), - identity.clone() + identity.clone(), ) .is_ok()); @@ -3094,7 +1972,7 @@ pub mod tests { deps.as_mut(), mock_env(), mock_info(delegation_owner2.as_str(), &vec![delegation2.clone()]), - identity.clone() + identity.clone(), ) .is_ok()); @@ -3118,240 +1996,13 @@ pub mod tests { } } - #[test] - fn delegators_on_mix_node_reward_rate() { - let mut deps = helpers::init_contract(); - let mut env = mock_env(); - 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 - let initial_delegation2 = 100; // won't see any rewards due to such a small delegation - let initial_delegation3 = 100000_000000; // will see big proper rewards - - let node_owner = "node-owner"; - let identity = add_mixnode(node_owner, good_mixnode_bond(), &mut deps); - - mix_delegations(&mut deps.storage, &identity) - .save( - b"delegator1", - &RawDelegationData::new(initial_delegation1.into(), env.block.height), - ) - .unwrap(); - mix_delegations(&mut deps.storage, &identity) - .save( - b"delegator2", - &RawDelegationData::new(initial_delegation2.into(), env.block.height), - ) - .unwrap(); - mix_delegations(&mut deps.storage, &identity) - .save( - b"delegator3", - &RawDelegationData::new(initial_delegation3.into(), env.block.height), - ) - .unwrap(); - - env.block.height += 2 * MINIMUM_BLOCK_AGE_FOR_REWARDING; - - 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 - let expected_mix_reward = Uint128(initial_mix_bond) * bond_reward; - let expected_delegation1_reward = Uint128(initial_delegation1) * delegation_reward; - let expected_delegation2_reward = Uint128(initial_delegation2) * delegation_reward; - let expected_delegation3_reward = Uint128(initial_delegation3) * delegation_reward; - - let expected_bond = expected_mix_reward + Uint128(initial_mix_bond); - let expected_delegation1 = expected_delegation1_reward + Uint128(initial_delegation1); - let expected_delegation2 = expected_delegation2_reward + Uint128(initial_delegation2); - let expected_delegation3 = expected_delegation3_reward + Uint128(initial_delegation3); - - 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, - read_mixnode_bond(deps.as_ref().storage, identity.as_bytes()).unwrap() - ); - - assert_eq!( - expected_delegation1, - mix_delegations_read(deps.as_ref().storage, &identity) - .load("delegator1".as_bytes()) - .unwrap() - .amount - ); - - assert_eq!( - expected_delegation2, - mix_delegations_read(deps.as_ref().storage, &identity) - .load("delegator2".as_bytes()) - .unwrap() - .amount - ); - - assert_eq!( - expected_delegation3, - mix_delegations_read(deps.as_ref().storage, &identity) - .load("delegator3".as_bytes()) - .unwrap() - .amount - ); - - assert_eq!( - vec![ - attr("bond increase", expected_mix_reward), - attr( - "total delegation increase", - expected_delegation1_reward - + expected_delegation2_reward - + expected_delegation3_reward - ), - ], - res.attributes - ); - - // if node was 20% up, it will get 1/5th of epoch reward - let scaled_bond_reward = scale_reward_by_uptime(bond_reward, 20).unwrap(); - let scaled_delegation_reward = scale_reward_by_uptime(delegation_reward, 20).unwrap(); - - let expected_mix_reward = expected_bond * scaled_bond_reward; - let expected_delegation1_reward = expected_delegation1 * scaled_delegation_reward; - let expected_delegation2_reward = expected_delegation2 * scaled_delegation_reward; - let expected_delegation3_reward = expected_delegation3 * scaled_delegation_reward; - - let expected_bond = expected_mix_reward + expected_bond; - let expected_delegation1 = expected_delegation1_reward + expected_delegation1; - let expected_delegation2 = expected_delegation2_reward + expected_delegation2; - let expected_delegation3 = expected_delegation3_reward + expected_delegation3; - - 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, - read_mixnode_bond(deps.as_ref().storage, identity.as_bytes()).unwrap() - ); - - assert_eq!( - expected_delegation1, - mix_delegations_read(deps.as_ref().storage, &identity) - .load("delegator1".as_bytes()) - .unwrap() - .amount - ); - - assert_eq!( - expected_delegation2, - mix_delegations_read(deps.as_ref().storage, &identity) - .load("delegator2".as_bytes()) - .unwrap() - .amount - ); - - assert_eq!( - expected_delegation3, - mix_delegations_read(deps.as_ref().storage, &identity) - .load("delegator3".as_bytes()) - .unwrap() - .amount - ); - - assert_eq!( - vec![ - attr("bond increase", expected_mix_reward), - attr( - "total delegation increase", - expected_delegation1_reward - + expected_delegation2_reward - + expected_delegation3_reward - ), - ], - res.attributes - ); - - // if the node was 0% up, nobody will get any rewards - 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, - read_mixnode_bond(deps.as_ref().storage, identity.as_bytes()).unwrap() - ); - - assert_eq!( - expected_delegation1, - mix_delegations_read(deps.as_ref().storage, &identity) - .load("delegator1".as_bytes()) - .unwrap() - .amount - ); - - assert_eq!( - expected_delegation2, - mix_delegations_read(deps.as_ref().storage, &identity) - .load("delegator2".as_bytes()) - .unwrap() - .amount - ); - - assert_eq!( - expected_delegation3, - mix_delegations_read(deps.as_ref().storage, &identity) - .load("delegator3".as_bytes()) - .unwrap() - .amount - ); - - assert_eq!( - vec![ - attr("bond increase", Uint128(0)), - attr("total delegation increase", Uint128(0)), - ], - res.attributes - ); - } - #[test] fn multiple_page_delegations() { let mut deps = helpers::init_contract(); let node_identity: IdentityKey = "foo".into(); store_n_mix_delegations( - DELEGATION_PAGE_DEFAULT_LIMIT * 10, + MIXNODE_DELEGATORS_PAGE_LIMIT as u32 * 10, &mut deps.storage, &node_identity, ); @@ -3359,7 +2010,7 @@ pub mod tests { let mix_delegations = Delegations::new(mix_bucket).collect::>>(); assert_eq!( - DELEGATION_PAGE_DEFAULT_LIMIT * 10, + MIXNODE_DELEGATORS_PAGE_LIMIT as u32 * 10, mix_delegations.len() as u32 ); } @@ -3439,135 +2090,4 @@ pub mod tests { assert_eq!(bob_node.mix_node.identity_key, "bob"); assert_eq!(bob_node.layer, Layer::Two); } - - #[test] - fn test_tokenomics_rewarding() { - use crate::contract::{EPOCH_REWARD_PERCENT, INITIAL_REWARD_POOL}; - - type U128 = fixed::types::U75F53; - - let mut deps = helpers::init_contract(); - let mut env = mock_env(); - let current_state = config(deps.as_mut().storage).load().unwrap(); - 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 - let circulating_supply = circulating_supply(&deps.storage).u128(); - assert_eq!(circulating_supply, 750_000_000_000_000u128); - // mut_reward_pool(deps.as_mut().storage) - // .save(&Uint128(period_reward_pool)) - // .unwrap(); - - try_add_mixnode( - deps.as_mut(), - mock_env(), - mock_info( - "alice", - &vec![Coin { - denom: DENOM.to_string(), - amount: Uint128(10000_000_000), - }], - ), - MixNode { - identity_key: "alice".to_string(), - ..helpers::mix_node_fixture() - }, - ) - .unwrap(); - - try_delegate_to_mixnode( - deps.as_mut(), - mock_env(), - mock_info("d1", &vec![coin(8000_000000, DENOM)]), - "alice".to_string(), - ) - .unwrap(); - - try_delegate_to_mixnode( - deps.as_mut(), - mock_env(), - mock_info("d2", &vec![coin(2000_000000, DENOM)]), - "alice".to_string(), - ) - .unwrap(); - - 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; - - let mix_1 = mixnodes_read(&deps.storage).load(b"alice").unwrap(); - let mix_1_uptime = 100; - - let mut params = NodeRewardParams::new( - period_reward_pool, - k, - 0, - circulating_supply, - mix_1_uptime, - DEFAULT_SYBIL_RESISTANCE_PERCENT, - ); - - params.set_reward_blockstamp(env.block.height); - - assert_eq!(params.performance(), 1); - - let mix_1_reward_result = mix_1.reward(¶ms); - - assert_eq!( - mix_1_reward_result.sigma(), - U128::from_num(0.0000266666666666) - ); - assert_eq!( - mix_1_reward_result.lambda(), - U128::from_num(0.0000133333333333) - ); - assert_eq!(mix_1_reward_result.reward().int(), 102646153); - - let mix1_operator_profit = mix_1.operator_reward(¶ms); - - let mix1_delegator1_reward = mix_1.reward_delegation(Uint128(8000_000000), ¶ms); - - let mix1_delegator2_reward = mix_1.reward_delegation(Uint128(2000_000000), ¶ms); - - assert_eq!(mix1_operator_profit, U128::from_num(74455384)); - assert_eq!(mix1_delegator1_reward, U128::from_num(22552615)); - assert_eq!(mix1_delegator2_reward, U128::from_num(5638153)); - - let pre_reward_bond = read_mixnode_bond(&deps.storage, b"alice").unwrap().u128(); - assert_eq!(pre_reward_bond, 10000_000_000); - - let pre_reward_delegation = read_mixnode_delegation(&deps.storage, b"alice") - .unwrap() - .u128(); - assert_eq!(pre_reward_delegation, 10000_000_000); - - 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(), - U128::from_num(pre_reward_bond) + U128::from_num(mix1_operator_profit) - ); - assert_eq!( - read_mixnode_delegation(&deps.storage, b"alice") - .unwrap() - .u128(), - pre_reward_delegation + mix1_delegator1_reward + mix1_delegator2_reward - ); - - assert_eq!( - reward_pool_value(&deps.storage).u128(), - U128::from_num(INITIAL_REWARD_POOL) - - (U128::from_num(mix1_operator_profit) - + U128::from_num(mix1_delegator1_reward) - + U128::from_num(mix1_delegator2_reward)) - ) - } } diff --git a/contracts/mixnet/src/transactions/rewarding.rs b/contracts/mixnet/src/transactions/rewarding.rs new file mode 100644 index 0000000000..2609f3b653 --- /dev/null +++ b/contracts/mixnet/src/transactions/rewarding.rs @@ -0,0 +1,2045 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::ContractError; +use crate::storage::{ + config, config_read, decr_reward_pool, mix_delegations, mixnodes, mixnodes_read, + rewarded_mixnodes, rewarded_mixnodes_read, +}; +use crate::transactions::{MAX_REWARDING_DURATION_IN_BLOCKS, MINIMUM_BLOCK_AGE_FOR_REWARDING}; +use cosmwasm_std::{attr, DepsMut, Env, MessageInfo, Response, StdResult, Storage, Uint128}; +use mixnet_contract::mixnode::{DelegatorRewardParams, NodeRewardParams}; +use mixnet_contract::{ + IdentityKey, IdentityKeyRef, PendingDelegatorRewarding, RewardingResult, RewardingStatus, + MIXNODE_DELEGATORS_PAGE_LIMIT, +}; + +#[derive(Debug)] +struct MixDelegationRewardingResult { + total_rewarded: Uint128, + start_next: Option, +} + +// 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) +} + +fn reward_mix_delegators_v2( + storage: &mut dyn Storage, + mix_identity: IdentityKeyRef, + start: Option, + params: DelegatorRewardParams, +) -> StdResult { + // TODO: some checks to make sure stuff is not TOO stale. + + let chunk_size = MIXNODE_DELEGATORS_PAGE_LIMIT; + let start_value = start.as_ref().map(|addr| addr.as_bytes()); + + let mut delegations = mix_delegations(storage, mix_identity); + + let mut total_rewarded = Uint128::zero(); + let mut items = 0; + + // keep track of the last iterated address so that we'd known what is the starting point + // of the next call (if required) + let mut start_next = None; + + // I really hate this intermediate allocation, but I don't think there's a nice + // way around it as we need to have immutable borrow into the bucket to retrieve delegation + // itself and then we need a mutable one to insert an updated one back + let mut rewarded_delegations = Vec::new(); + + // get `chunk_size` + 1 of delegations + // we get the additional one to know the optional starting point of the next call + // TODO: optimization for the future: we're reading 1 additional item than what's strictly + // required for this transaction thus slightly increasing the gas costs. + // however this makes the logic slightly simpler (I hope) + // Note: we can't just return last key of `chunk_size` with appended 0 byte as that + // would not be a valid utf8 string + for delegation in delegations + .range(start_value, None, cosmwasm_std::Order::Ascending) + .take(chunk_size + 1) + { + items += 1; + + let (delegator_address, mut delegation) = delegation?; + + if items == chunk_size + 1 { + // we shouldn't process this data, it's for the next call + start_next = Some(String::from_utf8(delegator_address)?); + break; + } else { + // and for each of them increase the stake proportionally to the reward + // if at least `MINIMUM_BLOCK_AGE_FOR_REWARDING` blocks have been created + // since they delegated + if delegation.block_height + MINIMUM_BLOCK_AGE_FOR_REWARDING + <= params.node_reward_params().reward_blockstamp() + { + let reward = params.determine_delegation_reward(delegation.amount); + delegation.amount += Uint128(reward); + total_rewarded += Uint128(reward); + + rewarded_delegations.push((delegator_address, delegation)); + } + } + } + + // finally save all delegation data back into the bucket + for rewarded_delegation in rewarded_delegations { + delegations.save(&rewarded_delegation.0, &rewarded_delegation.1)?; + } + + Ok(MixDelegationRewardingResult { + total_rewarded, + start_next, + }) +} + +/// Checks whether under the current context, any rewarding-related functionalities can be called. +/// The following must be true: +/// - the call has originated from the address of the authorised rewarding validator, +/// - the rewarding procedure has been initialised and has not concluded yet, +/// - the call has been made with the nonce corresponding to the current rewarding procedure, +/// +/// # Arguments +/// +/// * `storage`: reference (kinda) to the underlying storage pool of the contract used to read the current state +/// * `info`: contains the essential info for authorization, such as identity of the call +/// * `rewarding_interval_nonce`: nonce of the rewarding procedure sent alongside the call +fn verify_rewarding_state( + storage: &dyn Storage, + info: MessageInfo, + rewarding_interval_nonce: u32, +) -> Result<(), ContractError> { + let state = config_read(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 if rewarding is currently in progress, if not reject the transaction + if !state.rewarding_in_progress { + return Err(ContractError::RewardingNotInProgress); + } + + // make sure the transaction is sent for the correct rewarding interval + // (guard ourselves against somebody trying to send stale results; + // realistically it's never going to happen in a single rewarding validator case + // but this check is not expensive (since we already had to read the state), + // so we might as well) + if rewarding_interval_nonce != state.latest_rewarding_interval_nonce { + Err(ContractError::InvalidRewardingIntervalNonce { + received: rewarding_interval_nonce, + expected: state.latest_rewarding_interval_nonce, + }) + } else { + Ok(()) + } +} + +pub(crate) fn try_reward_next_mixnode_delegators_v2( + deps: DepsMut, + info: MessageInfo, + mix_identity: IdentityKey, + rewarding_interval_nonce: u32, +) -> Result { + verify_rewarding_state(deps.storage, info, rewarding_interval_nonce)?; + + match rewarded_mixnodes_read(deps.storage, rewarding_interval_nonce) + .may_load(mix_identity.as_bytes())? + { + None => { + // we haven't called 'regular' try_reward_mixnode, i.e. the operator itself + // was not rewarded yet + Err(ContractError::MixnodeOperatorNotRewarded { + identity: mix_identity, + }) + } + Some(RewardingStatus::Complete(_)) => { + // rewarding of this mixnode operator and all of its delegators has already been completed + Err(ContractError::MixnodeAlreadyRewarded { + identity: mix_identity, + }) + } + Some(RewardingStatus::PendingNextDelegatorPage(next_page_info)) => { + let delegation_rewarding_result = reward_mix_delegators_v2( + deps.storage, + &mix_identity, + Some(next_page_info.next_start), + next_page_info.rewarding_params, + )?; + + // read current bond to update the memoised total delegation field + let mut mixnodes = mixnodes(deps.storage); + if let Some(mut current_bond) = mixnodes.may_load(mix_identity.as_bytes())? { + // if the node unbonded, we don't have to worry about it. + current_bond.total_delegation.amount += delegation_rewarding_result.total_rewarded; + mixnodes.save(mix_identity.as_bytes(), ¤t_bond)?; + } + + decr_reward_pool(delegation_rewarding_result.total_rewarded, deps.storage)?; + + let mut rewarding_results = next_page_info.running_results; + rewarding_results.total_delegator_reward += delegation_rewarding_result.total_rewarded; + + let mut attributes = vec![( + "current round delegation increase", + delegation_rewarding_result.total_rewarded.to_string(), + )]; + + if let Some(next_start) = delegation_rewarding_result.start_next { + attributes.push(("more delegators to reward", "true".to_owned())); + + rewarded_mixnodes(deps.storage, rewarding_interval_nonce).save( + mix_identity.as_bytes(), + &RewardingStatus::PendingNextDelegatorPage(PendingDelegatorRewarding { + running_results: rewarding_results, + next_start, + rewarding_params: next_page_info.rewarding_params, + }), + )?; + } else { + attributes.push(("more delegators to reward", "false".to_owned())); + + rewarded_mixnodes(deps.storage, rewarding_interval_nonce).save( + mix_identity.as_bytes(), + &RewardingStatus::Complete(rewarding_results), + )?; + } + + let mut response = Response::new(); + // it looks kinda ugly now, but the API for this is vastly improved in cosmwasm 1.0 + for attribute in attributes { + response.add_attribute(attribute.0, attribute.1) + } + + 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} +// must be updated appropriately. +pub(crate) fn try_reward_mixnode_v2( + deps: DepsMut, + env: Env, + info: MessageInfo, + mix_identity: IdentityKey, + params: NodeRewardParams, + rewarding_interval_nonce: u32, +) -> Result { + verify_rewarding_state(deps.storage, info, rewarding_interval_nonce)?; + + // check if the mixnode hasn't been rewarded in this rewarding interval already + match rewarded_mixnodes_read(deps.storage, rewarding_interval_nonce) + .may_load(mix_identity.as_bytes())? + { + None => (), + Some(RewardingStatus::Complete(_)) => { + return Err(ContractError::MixnodeAlreadyRewarded { + identity: mix_identity, + }) + } + Some(RewardingStatus::PendingNextDelegatorPage(_)) => { + return Err(ContractError::DelegatorsPendingReward { + 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, + Err(_) => { + return Ok(Response { + attributes: vec![attr("result", "bond not found")], + ..Default::default() + }); + } + }; + + // in cosmwasm 1.0 all attributes have to be T: Into anyway + let mut node_reward = "0".to_string(); + let mut operator_reward = Uint128::zero(); + let mut total_delegation_increase = Uint128::zero(); + let mut more_delegators = false; + + // check if node is old enough for rewarding and if its uptime is non-zero + if current_bond.block_height + MINIMUM_BLOCK_AGE_FOR_REWARDING <= env.block.height + && params.uptime() > 0 + { + let mut node_reward_params = params; + node_reward_params.set_reward_blockstamp(env.block.height); + + let operator_reward_result = current_bond.reward(&node_reward_params); + node_reward = operator_reward_result.reward().to_string(); + + // Omitting the price per packet function now, it follows that base operator reward is the node_reward + operator_reward = Uint128(current_bond.operator_reward(&node_reward_params)); + + let delegator_params = DelegatorRewardParams::new(¤t_bond, node_reward_params); + + let delegation_rewarding_result = + reward_mix_delegators_v2(deps.storage, &mix_identity, None, delegator_params)?; + + current_bond.bond_amount.amount += operator_reward; + current_bond.total_delegation.amount += delegation_rewarding_result.total_rewarded; + mixnodes(deps.storage).save(mix_identity.as_bytes(), ¤t_bond)?; + decr_reward_pool( + operator_reward + delegation_rewarding_result.total_rewarded, + deps.storage, + )?; + + let rewarding_results = RewardingResult { + operator_reward, + total_delegator_reward: delegation_rewarding_result.total_rewarded, + }; + + total_delegation_increase = rewarding_results.total_delegator_reward; + + if let Some(next_start) = delegation_rewarding_result.start_next { + more_delegators = true; + + rewarded_mixnodes(deps.storage, rewarding_interval_nonce).save( + mix_identity.as_bytes(), + &RewardingStatus::PendingNextDelegatorPage(PendingDelegatorRewarding { + running_results: rewarding_results, + next_start, + rewarding_params: delegator_params, + }), + )?; + } else { + rewarded_mixnodes(deps.storage, rewarding_interval_nonce).save( + mix_identity.as_bytes(), + &RewardingStatus::Complete(rewarding_results), + )?; + } + } else { + // node is not eligible for rewarding, so we're done immediately + rewarded_mixnodes(deps.storage, rewarding_interval_nonce).save( + mix_identity.as_bytes(), + &RewardingStatus::Complete(Default::default()), + )?; + } + + Ok(Response { + submessages: vec![], + messages: vec![], + attributes: vec![ + attr("node reward", node_reward), + attr("operator reward", operator_reward), + attr("total delegation increase", total_delegation_increase), + attr("more delegators to reward", more_delegators), + ], + 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()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::contract::DEFAULT_SYBIL_RESISTANCE_PERCENT; + use crate::storage::{ + circulating_supply, mix_delegations_read, read_mixnode_bond, read_mixnode_delegation, + reward_pool_value, + }; + use crate::support::tests::helpers; + use crate::support::tests::helpers::{ + good_mixnode_bond, mix_node_fixture, node_rewarding_params_fixture, + }; + use crate::transactions::{try_add_mixnode, try_delegate_to_mixnode}; + use config::defaults::DENOM; + use cosmwasm_std::testing::{mock_env, mock_info}; + use cosmwasm_std::{coin, Addr, Coin, Order}; + use mixnet_contract::{Layer, MixNode, MixNodeBond, RawDelegationData}; + + #[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_delay() { + 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_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_v2( + deps.as_mut(), + env.clone(), + info.clone(), + node_identity.clone(), + node_rewarding_params_fixture(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_v2( + deps.as_mut(), + env.clone(), + info, + node_identity.clone(), + node_rewarding_params_fixture(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_v2( + deps.as_mut(), + env.clone(), + info.clone(), + node_identity.clone(), + node_rewarding_params_fixture(100), + 0, + ); + assert_eq!( + Err(ContractError::InvalidRewardingIntervalNonce { + received: 0, + expected: 1 + }), + res + ); + + let res = try_reward_mixnode_v2( + deps.as_mut(), + env.clone(), + info.clone(), + node_identity.clone(), + node_rewarding_params_fixture(100), + 2, + ); + assert_eq!( + Err(ContractError::InvalidRewardingIntervalNonce { + received: 2, + expected: 1 + }), + res + ); + + let res = try_reward_mixnode_v2( + deps.as_mut(), + env.clone(), + info, + node_identity.clone(), + node_rewarding_params_fixture(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_v2( + deps.as_mut(), + env.clone(), + info.clone(), + node_identity.clone(), + node_rewarding_params_fixture(100), + 1, + ); + assert!(res.is_ok()); + + // but the other one fails + let res = try_reward_mixnode_v2( + deps.as_mut(), + env.clone(), + info.clone(), + node_identity.clone(), + node_rewarding_params_fixture(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_v2( + deps.as_mut(), + env, + info, + node_identity.clone(), + node_rewarding_params_fixture(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_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(); + + let initial_bond = 10000_000000; + let initial_delegation = 20000_000000; + let mixnode_bond = MixNodeBond { + bond_amount: coin(initial_bond, DENOM), + total_delegation: coin(initial_delegation, DENOM), + owner: node_owner.clone(), + layer: Layer::One, + block_height: env.block.height, + mix_node: MixNode { + identity_key: node_identity.clone(), + ..mix_node_fixture() + }, + profit_margin_percent: Some(10), + }; + + mixnodes(deps.as_mut().storage) + .save(node_identity.as_bytes(), &mixnode_bond) + .unwrap(); + + // delegation happens later, but not later enough + env.block.height += MINIMUM_BLOCK_AGE_FOR_REWARDING - 1; + + mix_delegations(&mut deps.storage, &node_identity) + .save( + b"delegator", + &RawDelegationData::new(initial_delegation.into(), env.block.height), + ) + .unwrap(); + + // no reward is due + 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_v2( + deps.as_mut(), + env.clone(), + info.clone(), + node_identity.clone(), + node_rewarding_params_fixture(100), + 1, + ) + .unwrap(); + try_finish_mixnode_rewarding(deps.as_mut(), info, 1).unwrap(); + + assert_eq!( + initial_bond, + read_mixnode_bond(deps.as_ref().storage, node_identity.as_bytes()) + .unwrap() + .u128() + ); + assert_eq!( + initial_delegation, + read_mixnode_delegation(deps.as_ref().storage, node_identity.as_bytes()) + .unwrap() + .u128() + ); + + assert_eq!(res.attributes[0], attr("node reward", "0")); + assert_eq!(res.attributes[1], attr("operator reward", "0")); + assert_eq!(res.attributes[2], attr("total delegation increase", "0")); + assert_eq!(res.attributes[3], attr("more delegators to reward", false)); + + // reward can happen now, but only for bonded node + env.block.height += 1; + + 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_v2( + deps.as_mut(), + env.clone(), + info.clone(), + node_identity.clone(), + node_rewarding_params_fixture(100), + 2, + ) + .unwrap(); + try_finish_mixnode_rewarding(deps.as_mut(), info, 2).unwrap(); + + assert!( + read_mixnode_bond(deps.as_ref().storage, node_identity.as_bytes()) + .unwrap() + .u128() + > initial_bond + ); + assert_eq!( + initial_delegation, + read_mixnode_delegation(deps.as_ref().storage, node_identity.as_bytes()) + .unwrap() + .u128() + ); + + assert_ne!(res.attributes[0], attr("node reward", "0")); + assert_ne!(res.attributes[1], attr("operator reward", "0")); + assert_eq!(res.attributes[2], attr("total delegation increase", "0")); + assert_eq!(res.attributes[3], attr("more delegators to reward", false)); + + // reward happens now, both for node owner and delegators + env.block.height += MINIMUM_BLOCK_AGE_FOR_REWARDING - 1; + + let bond_before_rewarding = + read_mixnode_bond(deps.as_ref().storage, node_identity.as_bytes()) + .unwrap() + .u128(); + + 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_v2( + deps.as_mut(), + env.clone(), + info.clone(), + node_identity.clone(), + node_rewarding_params_fixture(100), + 3, + ) + .unwrap(); + try_finish_mixnode_rewarding(deps.as_mut(), info, 3).unwrap(); + + assert!( + read_mixnode_bond(deps.as_ref().storage, node_identity.as_bytes()) + .unwrap() + .u128() + > bond_before_rewarding + ); + assert!( + read_mixnode_delegation(deps.as_ref().storage, node_identity.as_bytes()) + .unwrap() + .u128() + > initial_delegation + ); + + assert_ne!(res.attributes[0], attr("node reward", "0")); + assert_ne!(res.attributes[1], attr("operator reward", "0")); + assert_ne!(res.attributes[2], attr("total delegation increase", "0")); + assert_eq!(res.attributes[3], attr("more delegators to reward", false)); + } + + #[test] + fn test_tokenomics_rewarding() { + use crate::contract::{EPOCH_REWARD_PERCENT, INITIAL_REWARD_POOL}; + + type U128 = fixed::types::U75F53; + + let mut deps = helpers::init_contract(); + let mut env = mock_env(); + let current_state = config(deps.as_mut().storage).load().unwrap(); + 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 + let circulating_supply = circulating_supply(&deps.storage).u128(); + assert_eq!(circulating_supply, 750_000_000_000_000u128); + // mut_reward_pool(deps.as_mut().storage) + // .save(&Uint128(period_reward_pool)) + // .unwrap(); + + try_add_mixnode( + deps.as_mut(), + mock_env(), + mock_info( + "alice", + &vec![Coin { + denom: DENOM.to_string(), + amount: Uint128(10000_000_000), + }], + ), + MixNode { + identity_key: "alice".to_string(), + ..helpers::mix_node_fixture() + }, + ) + .unwrap(); + + try_delegate_to_mixnode( + deps.as_mut(), + mock_env(), + mock_info("d1", &vec![coin(8000_000000, DENOM)]), + "alice".to_string(), + ) + .unwrap(); + + try_delegate_to_mixnode( + deps.as_mut(), + mock_env(), + mock_info("d2", &vec![coin(2000_000000, DENOM)]), + "alice".to_string(), + ) + .unwrap(); + + 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; + + let mix_1 = mixnodes_read(&deps.storage).load(b"alice").unwrap(); + let mix_1_uptime = 100; + + let mut params = NodeRewardParams::new( + period_reward_pool, + k, + 0, + circulating_supply, + mix_1_uptime, + DEFAULT_SYBIL_RESISTANCE_PERCENT, + ); + + params.set_reward_blockstamp(env.block.height); + + assert_eq!(params.performance(), 1); + + let mix_1_reward_result = mix_1.reward(¶ms); + + assert_eq!( + mix_1_reward_result.sigma(), + U128::from_num(0.0000266666666666) + ); + assert_eq!( + mix_1_reward_result.lambda(), + U128::from_num(0.0000133333333333) + ); + assert_eq!(mix_1_reward_result.reward().int(), 102646153); + + let mix1_operator_profit = mix_1.operator_reward(¶ms); + + let mix1_delegator1_reward = mix_1.reward_delegation(Uint128(8000_000000), ¶ms); + + let mix1_delegator2_reward = mix_1.reward_delegation(Uint128(2000_000000), ¶ms); + + assert_eq!(mix1_operator_profit, U128::from_num(74455384)); + assert_eq!(mix1_delegator1_reward, U128::from_num(22552615)); + assert_eq!(mix1_delegator2_reward, U128::from_num(5638153)); + + let pre_reward_bond = read_mixnode_bond(&deps.storage, b"alice").unwrap().u128(); + assert_eq!(pre_reward_bond, 10000_000_000); + + let pre_reward_delegation = read_mixnode_delegation(&deps.storage, b"alice") + .unwrap() + .u128(); + assert_eq!(pre_reward_delegation, 10000_000_000); + + 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(), + U128::from_num(pre_reward_bond) + U128::from_num(mix1_operator_profit) + ); + assert_eq!( + read_mixnode_delegation(&deps.storage, b"alice") + .unwrap() + .u128(), + pre_reward_delegation + mix1_delegator1_reward + mix1_delegator2_reward + ); + + assert_eq!( + reward_pool_value(&deps.storage).u128(), + U128::from_num(INITIAL_REWARD_POOL) + - (U128::from_num(mix1_operator_profit) + + U128::from_num(mix1_delegator1_reward) + + U128::from_num(mix1_delegator2_reward)) + ); + + // it's all correctly saved + match rewarded_mixnodes_read(&deps.storage, 1) + .load(b"alice") + .unwrap() + { + RewardingStatus::Complete(result) => assert_eq!( + RewardingResult { + operator_reward: Uint128::new(mix1_operator_profit), + total_delegator_reward: Uint128::new( + mix1_delegator1_reward + mix1_delegator2_reward + ) + }, + result + ), + _ => unreachable!(), + } + } + + #[cfg(test)] + mod mixnode_rewarding_distributes_rewards_to_up_to_one_page_of_delegators { + + #[test] + fn with_10_delegations() { + use super::*; + + let mut deps = helpers::init_contract(); + let mut env = mock_env(); + let current_state = config(deps.as_mut().storage).load().unwrap(); + let rewarding_validator_address = current_state.rewarding_validator_address; + + let mix_bond = Uint128(10000_000_000); + let delegation_value = 2000_000000; + try_add_mixnode( + deps.as_mut(), + env.clone(), + mock_info( + "10delegators", + &vec![Coin { + denom: DENOM.to_string(), + amount: mix_bond, + }], + ), + MixNode { + identity_key: "10delegators".to_string(), + ..helpers::mix_node_fixture() + }, + ) + .unwrap(); + + for i in 0..10 { + try_delegate_to_mixnode( + deps.as_mut(), + env.clone(), + mock_info( + &*format!("delegator{}", i), + &vec![coin(delegation_value, DENOM)], + ), + "10delegators".to_string(), + ) + .unwrap(); + } + + env.block.height += MINIMUM_BLOCK_AGE_FOR_REWARDING; + + 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(); + + let res = try_reward_mixnode_v2( + deps.as_mut(), + env, + info, + "10delegators".to_string(), + node_rewarding_params_fixture(100), + 1, + ) + .unwrap(); + assert_eq!(res.attributes[3], attr("more delegators to reward", false)); + + try_finish_mixnode_rewarding( + deps.as_mut(), + mock_info(rewarding_validator_address.as_ref(), &[]), + 1, + ) + .unwrap(); + + for i in 0..10 { + let delegation = mix_delegations_read(deps.as_ref().storage, "10delegators") + .load(format!("delegator{}", i).as_bytes()) + .unwrap(); + assert!(delegation.amount > Uint128(delegation_value)); + } + } + + #[test] + fn with_full_page_limit() { + use super::*; + + let mut deps = helpers::init_contract(); + let mut env = mock_env(); + let current_state = config(deps.as_mut().storage).load().unwrap(); + let rewarding_validator_address = current_state.rewarding_validator_address; + + let mix_bond = Uint128(10000_000_000); + let delegation_value = 2000_000000; + try_add_mixnode( + deps.as_mut(), + env.clone(), + mock_info( + "MIXNODE_DELEGATORS_PAGE_LIMIT_delegators", + &vec![Coin { + denom: DENOM.to_string(), + amount: mix_bond, + }], + ), + MixNode { + identity_key: "MIXNODE_DELEGATORS_PAGE_LIMIT_delegators".to_string(), + ..helpers::mix_node_fixture() + }, + ) + .unwrap(); + + for i in 0..MIXNODE_DELEGATORS_PAGE_LIMIT { + try_delegate_to_mixnode( + deps.as_mut(), + env.clone(), + mock_info( + &*format!("delegator{}", i), + &vec![coin(delegation_value, DENOM)], + ), + "MIXNODE_DELEGATORS_PAGE_LIMIT_delegators".to_string(), + ) + .unwrap(); + } + + env.block.height += MINIMUM_BLOCK_AGE_FOR_REWARDING; + + 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(); + + let res = try_reward_mixnode_v2( + deps.as_mut(), + env, + info, + "MIXNODE_DELEGATORS_PAGE_LIMIT_delegators".to_string(), + node_rewarding_params_fixture(100), + 1, + ) + .unwrap(); + assert_eq!(res.attributes[3], attr("more delegators to reward", false)); + + try_finish_mixnode_rewarding( + deps.as_mut(), + mock_info(rewarding_validator_address.as_ref(), &[]), + 1, + ) + .unwrap(); + + for i in 0..MIXNODE_DELEGATORS_PAGE_LIMIT { + let delegation = mix_delegations_read( + deps.as_ref().storage, + "MIXNODE_DELEGATORS_PAGE_LIMIT_delegators", + ) + .load(format!("delegator{}", i).as_bytes()) + .unwrap(); + assert!(delegation.amount > Uint128(delegation_value)); + } + } + + #[test] + fn with_more_than_full_page_limit() { + use super::*; + + let mut deps = helpers::init_contract(); + let mut env = mock_env(); + let current_state = config(deps.as_mut().storage).load().unwrap(); + let rewarding_validator_address = current_state.rewarding_validator_address; + + let mix_bond = Uint128(10000_000_000); + let delegation_value = 2000_000000; + try_add_mixnode( + deps.as_mut(), + env.clone(), + mock_info( + "MIXNODE_DELEGATORS_PAGE_LIMIT+1_delegators", + &vec![Coin { + denom: DENOM.to_string(), + amount: mix_bond, + }], + ), + MixNode { + identity_key: "MIXNODE_DELEGATORS_PAGE_LIMIT+1_delegators".to_string(), + ..helpers::mix_node_fixture() + }, + ) + .unwrap(); + + for i in 0..MIXNODE_DELEGATORS_PAGE_LIMIT + 1 { + try_delegate_to_mixnode( + deps.as_mut(), + env.clone(), + mock_info( + &*format!("delegator{:04}", i), + &vec![coin(delegation_value, DENOM)], + ), + "MIXNODE_DELEGATORS_PAGE_LIMIT+1_delegators".to_string(), + ) + .unwrap(); + } + + env.block.height += MINIMUM_BLOCK_AGE_FOR_REWARDING; + + 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(); + + let res = try_reward_mixnode_v2( + deps.as_mut(), + env, + info, + "MIXNODE_DELEGATORS_PAGE_LIMIT+1_delegators".to_string(), + node_rewarding_params_fixture(100), + 1, + ) + .unwrap(); + assert_eq!(res.attributes[3], attr("more delegators to reward", true)); + + try_finish_mixnode_rewarding( + deps.as_mut(), + mock_info(rewarding_validator_address.as_ref(), &[]), + 1, + ) + .unwrap(); + + for i in 0..MIXNODE_DELEGATORS_PAGE_LIMIT { + let delegation = mix_delegations_read( + deps.as_ref().storage, + "MIXNODE_DELEGATORS_PAGE_LIMIT+1_delegators", + ) + .load(format!("delegator{:04}", i).as_bytes()) + .unwrap(); + assert!(delegation.amount > Uint128(delegation_value)); + } + + // and the one on the next page should have been unrewarded + let delegation = mix_delegations_read( + deps.as_ref().storage, + "MIXNODE_DELEGATORS_PAGE_LIMIT+1_delegators", + ) + .load(format!("delegator{:04}", MIXNODE_DELEGATORS_PAGE_LIMIT).as_bytes()) + .unwrap(); + assert_eq!(delegation.amount, Uint128(delegation_value)); + } + } + + #[test] + fn rewarding_mix_delegators_return_consistent_results() { + // with single page + let mut deps = helpers::init_contract(); + let mut env = mock_env(); + + let node_identity = "bobsnode".to_string(); + try_add_mixnode( + deps.as_mut(), + env.clone(), + mock_info("bob", &[coin(10000_000_000, DENOM)]), + MixNode { + identity_key: node_identity.clone(), + ..helpers::mix_node_fixture() + }, + ) + .unwrap(); + let bond = mixnodes_read(deps.as_ref().storage) + .load(node_identity.as_bytes()) + .unwrap(); + + let base_delegation = 200_000000; + let delegations = 123; + + for i in 0..delegations { + try_delegate_to_mixnode( + deps.as_mut(), + env.clone(), + mock_info( + &*format!("delegator{:04}", i), + &vec![coin(base_delegation, DENOM)], + ), + node_identity.clone(), + ) + .unwrap(); + } + + env.block.height += MINIMUM_BLOCK_AGE_FOR_REWARDING + 1; + let mut node_rewarding_params = node_rewarding_params_fixture(100); + node_rewarding_params.set_reward_blockstamp(env.block.height); + + let params = DelegatorRewardParams::new(&bond, node_rewarding_params); + let res = + reward_mix_delegators_v2(deps.as_mut().storage, &node_identity, None, params).unwrap(); + + let mut actual_reward = Uint128::new(0); + for delegation in mix_delegations_read(deps.as_ref().storage, &node_identity).range( + None, + None, + Order::Ascending, + ) { + actual_reward += Uint128(delegation.unwrap().1.amount.u128() - base_delegation); + } + + // sanity check to make sure we actually gave out any rewards + assert_ne!(actual_reward, Uint128::zero()); + + assert_eq!(actual_reward, res.total_rewarded); + assert!(res.start_next.is_none()); + + // with paging + let node_identity = "alicesnode".to_string(); + try_add_mixnode( + deps.as_mut(), + env.clone(), + mock_info("alice", &[coin(10000_000_000, DENOM)]), + MixNode { + identity_key: node_identity.clone(), + ..helpers::mix_node_fixture() + }, + ) + .unwrap(); + let bond = mixnodes_read(deps.as_ref().storage) + .load(node_identity.as_bytes()) + .unwrap(); + + let base_delegation = 200_000000; + let delegations = MIXNODE_DELEGATORS_PAGE_LIMIT + 123; + + for i in 0..delegations { + try_delegate_to_mixnode( + deps.as_mut(), + env.clone(), + mock_info( + &*format!("delegator{:04}", i), + &vec![coin(base_delegation, DENOM)], + ), + node_identity.clone(), + ) + .unwrap(); + } + + env.block.height += MINIMUM_BLOCK_AGE_FOR_REWARDING + 1; + let mut node_rewarding_params = node_rewarding_params_fixture(100); + node_rewarding_params.set_reward_blockstamp(env.block.height); + + let params = DelegatorRewardParams::new(&bond, node_rewarding_params); + let res = + reward_mix_delegators_v2(deps.as_mut().storage, &node_identity, None, params).unwrap(); + + let mut actual_reward = Uint128::new(0); + for delegation in mix_delegations_read(deps.as_ref().storage, &node_identity).range( + None, + None, + Order::Ascending, + ) { + let (delegator, delegation) = delegation.unwrap(); + let delegator_reward = Uint128(delegation.amount.u128() - base_delegation); + actual_reward += delegator_reward; + + let delegator = String::from_utf8(delegator).unwrap(); + let delegator_id: usize = delegator + .strip_prefix("delegator") + .unwrap() + .parse() + .unwrap(); + if delegator_id >= MIXNODE_DELEGATORS_PAGE_LIMIT { + // if they were in next page, they shouldn't have gotten anything! + assert_eq!(Uint128::zero(), delegator_reward) + } else { + assert_ne!(Uint128::zero(), delegator_reward); + } + } + + assert_eq!(actual_reward, res.total_rewarded); + let expected_next_page_start = format!("delegator{:04}", MIXNODE_DELEGATORS_PAGE_LIMIT); + assert_eq!(expected_next_page_start, res.start_next.clone().unwrap()); + + let res2 = reward_mix_delegators_v2( + deps.as_mut().storage, + &node_identity, + res.start_next.clone(), + params, + ) + .unwrap(); + + let start = res.start_next.unwrap(); + let start_bytes = start.as_bytes(); + let mut actual_reward = Uint128::new(0); + for delegation in mix_delegations_read(deps.as_ref().storage, &node_identity).range( + Some(start_bytes), + None, + Order::Ascending, + ) { + actual_reward += Uint128(delegation.unwrap().1.amount.u128() - base_delegation); + } + + assert_eq!(actual_reward, res2.total_rewarded); + assert!(res2.start_next.is_none()); + } + + #[cfg(test)] + mod delegator_rewarding_tx { + use super::*; + use crate::storage::mix_delegations_read; + + #[test] + fn can_only_be_called_by_specified_validator_address() { + let mut deps = helpers::init_contract(); + + let res = try_reward_next_mixnode_delegators_v2( + deps.as_mut(), + mock_info("not-the-approved-validator", &[]), + "alice's mixnode".to_string(), + 1, + ); + assert_eq!(Err(ContractError::Unauthorized), res); + } + + #[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_reward_next_mixnode_delegators_v2( + deps.as_mut(), + mock_info(rewarding_validator_address.as_ref(), &[]), + "alice's mixnode".to_string(), + 1, + ); + + assert_eq!(Err(ContractError::RewardingNotInProgress), res); + } + + #[test] + fn cannot_be_called_if_mixnodes_operator_wasnt_rewarded() { + 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_reward_next_mixnode_delegators_v2( + deps.as_mut(), + mock_info(rewarding_validator_address.as_ref(), &[]), + "alice's mixnode".to_string(), + 1, + ); + + assert_eq!( + Err(ContractError::MixnodeOperatorNotRewarded { + identity: "alice's mixnode".to_string() + }), + res + ) + } + + #[test] + fn cannot_be_called_if_mixnode_is_fully_rewarded() { + // everything was done in a single reward call + let mut deps = helpers::init_contract(); + let mut env = mock_env(); + let current_state = config_read(deps.as_mut().storage).load().unwrap(); + let rewarding_validator_address = current_state.rewarding_validator_address; + + try_add_mixnode( + deps.as_mut(), + mock_env(), + mock_info( + "alice", + &vec![Coin { + denom: DENOM.to_string(), + amount: Uint128(10000_000_000), + }], + ), + MixNode { + identity_key: "alice".to_string(), + ..helpers::mix_node_fixture() + }, + ) + .unwrap(); + + env.block.height += MINIMUM_BLOCK_AGE_FOR_REWARDING; + + try_begin_mixnode_rewarding( + deps.as_mut(), + env.clone(), + mock_info(rewarding_validator_address.as_ref(), &[]), + 1, + ) + .unwrap(); + + try_reward_mixnode_v2( + deps.as_mut(), + env.clone(), + mock_info(rewarding_validator_address.as_ref(), &[]), + "alice".to_string(), + node_rewarding_params_fixture(100), + 1, + ) + .unwrap(); + + let res = try_reward_next_mixnode_delegators_v2( + deps.as_mut(), + mock_info(rewarding_validator_address.as_ref(), &[]), + "alice".to_string(), + 1, + ); + + assert_eq!( + Err(ContractError::MixnodeAlreadyRewarded { + identity: "alice".to_string() + }), + res + ); + + try_finish_mixnode_rewarding( + deps.as_mut(), + mock_info(rewarding_validator_address.as_ref(), &[]), + 1, + ) + .unwrap(); + + // there was another page of delegators, but they were already dealt with + try_add_mixnode( + deps.as_mut(), + env.clone(), + mock_info( + "bob", + &vec![Coin { + denom: DENOM.to_string(), + amount: Uint128(10000_000_000), + }], + ), + MixNode { + identity_key: "bob".to_string(), + ..helpers::mix_node_fixture() + }, + ) + .unwrap(); + + for i in 0..MIXNODE_DELEGATORS_PAGE_LIMIT + 1 { + try_delegate_to_mixnode( + deps.as_mut(), + env.clone(), + mock_info( + &*format!("delegator{:04}", i), + &vec![coin(2000_000000, DENOM)], + ), + "bob".to_string(), + ) + .unwrap(); + } + + env.block.height += MINIMUM_BLOCK_AGE_FOR_REWARDING; + + 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(), &[]), + 2, + ) + .unwrap(); + + try_reward_mixnode_v2( + deps.as_mut(), + env.clone(), + info, + "bob".to_string(), + node_rewarding_params_fixture(100), + 2, + ) + .unwrap(); + + // rewards all pending + try_reward_next_mixnode_delegators_v2( + deps.as_mut(), + mock_info(rewarding_validator_address.as_ref(), &[]), + "bob".to_string(), + 2, + ) + .unwrap(); + + let res = try_reward_next_mixnode_delegators_v2( + deps.as_mut(), + mock_info(rewarding_validator_address.as_ref(), &[]), + "bob".to_string(), + 2, + ); + + assert_eq!( + Err(ContractError::MixnodeAlreadyRewarded { + identity: "bob".to_string() + }), + res + ); + + try_finish_mixnode_rewarding( + deps.as_mut(), + mock_info(rewarding_validator_address.as_ref(), &[]), + 2, + ) + .unwrap(); + } + + #[test] + fn rewards_all_delegators_on_the_next_page() { + // setup: bond > page limit delegators, reward operator + first batch + let mut deps = helpers::init_contract(); + let mut env = mock_env(); + let current_state = config_read(deps.as_mut().storage).load().unwrap(); + let rewarding_validator_address = current_state.rewarding_validator_address; + + let mix_bond = Uint128(10000_000_000); + let delegation_value = 2000_000000; + + let total_delegators = 2 * MIXNODE_DELEGATORS_PAGE_LIMIT + 123; + + try_add_mixnode( + deps.as_mut(), + env.clone(), + mock_info( + "alice", + &vec![Coin { + denom: DENOM.to_string(), + amount: mix_bond, + }], + ), + MixNode { + identity_key: "alice".to_string(), + ..helpers::mix_node_fixture() + }, + ) + .unwrap(); + + for i in 0..total_delegators { + try_delegate_to_mixnode( + deps.as_mut(), + env.clone(), + mock_info( + &*format!("delegator{:04}", i), + &vec![coin(delegation_value, DENOM)], + ), + "alice".to_string(), + ) + .unwrap(); + } + + env.block.height += MINIMUM_BLOCK_AGE_FOR_REWARDING; + + 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(); + + try_reward_mixnode_v2( + deps.as_mut(), + env.clone(), + info, + "alice".to_string(), + node_rewarding_params_fixture(100), + 1, + ) + .unwrap(); + + // we have 3 pages in total, so we have to call this twice + try_reward_next_mixnode_delegators_v2( + deps.as_mut(), + mock_info(rewarding_validator_address.as_ref(), &[]), + "alice".to_string(), + 1, + ) + .unwrap(); + try_reward_next_mixnode_delegators_v2( + deps.as_mut(), + mock_info(rewarding_validator_address.as_ref(), &[]), + "alice".to_string(), + 1, + ) + .unwrap(); + + let expected = mix_delegations_read(deps.as_ref().storage, "alice") + .load("delegator0001".as_bytes()) + .unwrap() + .amount; + + for i in 0..total_delegators { + // everyone was rewarded (and the same amount, because they all delegated the same amount) + let delegation = mix_delegations_read(deps.as_ref().storage, "alice") + .load(format!("delegator{:04}", i).as_bytes()) + .unwrap(); + assert!(delegation.amount > Uint128(delegation_value)); + assert_eq!(expected, delegation.amount) + } + } + + #[test] + fn ignores_delegators_that_updated_their_pledge_in_the_meantime() { + // setup: bond > page limit delegators, reward operator + first batch + let mut deps = helpers::init_contract(); + let mut env = mock_env(); + let current_state = config_read(deps.as_mut().storage).load().unwrap(); + let rewarding_validator_address = current_state.rewarding_validator_address; + + let mix_bond = Uint128(10000_000_000); + let delegation_value = 2000_000000; + + let total_delegators = MIXNODE_DELEGATORS_PAGE_LIMIT + 123; + + try_add_mixnode( + deps.as_mut(), + env.clone(), + mock_info( + "alice", + &vec![Coin { + denom: DENOM.to_string(), + amount: mix_bond, + }], + ), + MixNode { + identity_key: "alice".to_string(), + ..helpers::mix_node_fixture() + }, + ) + .unwrap(); + + for i in 0..total_delegators { + try_delegate_to_mixnode( + deps.as_mut(), + env.clone(), + mock_info( + &*format!("delegator{:04}", i), + &vec![coin(delegation_value, DENOM)], + ), + "alice".to_string(), + ) + .unwrap(); + } + + env.block.height += MINIMUM_BLOCK_AGE_FOR_REWARDING; + + // update some delegations (on 'main' page and the secondary call) + try_delegate_to_mixnode( + deps.as_mut(), + env.clone(), + mock_info("delegator0123", &vec![coin(delegation_value, DENOM)]), + "alice".to_string(), + ) + .unwrap(); + + try_delegate_to_mixnode( + deps.as_mut(), + env.clone(), + mock_info( + &*format!("delegator{:04}", 123 + MIXNODE_DELEGATORS_PAGE_LIMIT), + &vec![coin(delegation_value, DENOM)], + ), + "alice".to_string(), + ) + .unwrap(); + + env.block.height += 123; + + 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(); + + try_reward_mixnode_v2( + deps.as_mut(), + env.clone(), + info, + "alice".to_string(), + node_rewarding_params_fixture(100), + 1, + ) + .unwrap(); + + // we have 3 pages in total, so we have to call this twice + try_reward_next_mixnode_delegators_v2( + deps.as_mut(), + mock_info(rewarding_validator_address.as_ref(), &[]), + "alice".to_string(), + 1, + ) + .unwrap(); + + let expected = mix_delegations_read(deps.as_ref().storage, "alice") + .load("delegator0001".as_bytes()) + .unwrap() + .amount; + + for i in 0..total_delegators { + // everyone was rewarded (and the same amount, because they all delegated the same amount) + let delegation = mix_delegations_read(deps.as_ref().storage, "alice") + .load(format!("delegator{:04}", i).as_bytes()) + .unwrap(); + + if i == 123 || i == 123 + MIXNODE_DELEGATORS_PAGE_LIMIT { + assert_eq!(delegation.amount, Uint128(2 * delegation_value)) + } else { + assert!(delegation.amount > Uint128(delegation_value)); + assert_eq!(expected, delegation.amount) + } + } + } + } +} diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index 4c9e061a43..4afc9daff1 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -1298,6 +1298,7 @@ dependencies = [ "az", "bytemuck", "half", + "serde", "typenum", ] diff --git a/nym-wallet/src-tauri/src/operations/admin.rs b/nym-wallet/src-tauri/src/operations/admin.rs index a98e180437..4c958505c0 100644 --- a/nym-wallet/src-tauri/src/operations/admin.rs +++ b/nym-wallet/src-tauri/src/operations/admin.rs @@ -1,22 +1,17 @@ use crate::format_err; use crate::state::State; -use cosmwasm_std::Decimal; use cosmwasm_std::Uint128; use mixnet_contract::StateParams; use serde::{Deserialize, Serialize}; use std::convert::{TryFrom, TryInto}; -use std::str::FromStr; use std::sync::Arc; use tokio::sync::RwLock; #[cfg_attr(test, derive(ts_rs::TS))] #[derive(Serialize, Deserialize)] pub struct TauriStateParams { - epoch_length: u32, minimum_mixnode_bond: String, minimum_gateway_bond: String, - mixnode_bond_reward_rate: String, - mixnode_delegation_reward_rate: String, mixnode_rewarded_set_size: u32, mixnode_active_set_size: u32, } @@ -24,11 +19,8 @@ pub struct TauriStateParams { impl From for TauriStateParams { fn from(p: StateParams) -> TauriStateParams { TauriStateParams { - epoch_length: p.epoch_length, minimum_mixnode_bond: p.minimum_mixnode_bond.to_string(), minimum_gateway_bond: p.minimum_gateway_bond.to_string(), - mixnode_bond_reward_rate: p.mixnode_bond_reward_rate.to_string(), - mixnode_delegation_reward_rate: p.mixnode_delegation_reward_rate.to_string(), mixnode_rewarded_set_size: p.mixnode_rewarded_set_size, mixnode_active_set_size: p.mixnode_active_set_size, } @@ -40,11 +32,8 @@ impl TryFrom for StateParams { fn try_from(p: TauriStateParams) -> Result { Ok(StateParams { - epoch_length: p.epoch_length, minimum_mixnode_bond: Uint128::try_from(p.minimum_mixnode_bond.as_str())?, minimum_gateway_bond: Uint128::try_from(p.minimum_gateway_bond.as_str())?, - mixnode_bond_reward_rate: Decimal::from_str(p.mixnode_bond_reward_rate.as_str())?, - mixnode_delegation_reward_rate: Decimal::from_str(p.mixnode_delegation_reward_rate.as_str())?, mixnode_rewarded_set_size: p.mixnode_rewarded_set_size, mixnode_active_set_size: p.mixnode_active_set_size, }) diff --git a/nym-wallet/src/types/rust/stateparams.ts b/nym-wallet/src/types/rust/stateparams.ts index ebe12a6bf1..84da086dfb 100644 --- a/nym-wallet/src/types/rust/stateparams.ts +++ b/nym-wallet/src/types/rust/stateparams.ts @@ -1,9 +1,6 @@ export interface TauriStateParams { - epoch_length: number; minimum_mixnode_bond: string; minimum_gateway_bond: string; - mixnode_bond_reward_rate: string; - mixnode_delegation_reward_rate: string; mixnode_rewarded_set_size: number; mixnode_active_set_size: number; } \ No newline at end of file diff --git a/validator-api/Cargo.toml b/validator-api/Cargo.toml index f55a71a6bc..27be478871 100644 --- a/validator-api/Cargo.toml +++ b/validator-api/Cargo.toml @@ -57,8 +57,6 @@ credentials = { path = "../common/credentials", optional = true } [features] coconut = ["coconut-interface", "credentials", "gateway-client/coconut"] -default = ["tokenomics"] -tokenomics = [] [build-dependencies] tokio = { version = "1.4", features = ["rt-multi-thread", "macros"] } diff --git a/validator-api/src/config/template.rs b/validator-api/src/config/template.rs index 7526d23ad1..0f07b943a3 100644 --- a/validator-api/src/config/template.rs +++ b/validator-api/src/config/template.rs @@ -16,9 +16,6 @@ local_validator = '{{ base.local_validator }}' # Address of the validator contract managing the network. mixnet_contract_address = '{{ base.mixnet_contract_address }}' -# Mnemonic (currently of the network monitor) used for rewarding -mnemonic = '{{ base.mnemonic }}' - ##### network monitor config options ##### [network_monitor] diff --git a/validator-api/src/node_status_api/models.rs b/validator-api/src/node_status_api/models.rs index a4ff5f54e2..a3fbe6ee63 100644 --- a/validator-api/src/node_status_api/models.rs +++ b/validator-api/src/node_status_api/models.rs @@ -17,7 +17,7 @@ use time::OffsetDateTime; pub struct InvalidUptime; // value in range 0-100 -#[derive(Clone, Copy, Serialize, Deserialize, Debug)] +#[derive(Clone, Copy, Serialize, Deserialize, Debug, Default)] pub struct Uptime(u8); impl Uptime { diff --git a/validator-api/src/nymd_client.rs b/validator-api/src/nymd_client.rs index 3ada44491a..d8dc0df87e 100644 --- a/validator-api/src/nymd_client.rs +++ b/validator-api/src/nymd_client.rs @@ -8,16 +8,18 @@ use crate::rewarding::{ }; use config::defaults::DEFAULT_VALIDATOR_API_PORT; use mixnet_contract::{ - Delegation, ExecuteMsg, GatewayBond, IdentityKey, MixNodeBond, RewardingIntervalResponse, - StateParams, + Delegation, ExecuteMsg, GatewayBond, IdentityKey, MixNodeBond, MixnodeRewardingStatusResponse, + RewardingIntervalResponse, StateParams, MIXNODE_DELEGATORS_PAGE_LIMIT, }; +use serde::Serialize; use std::sync::Arc; use std::time::Duration; use tokio::sync::RwLock; use tokio::time::sleep; use validator_client::nymd::{ hash::{Hash, SHA256_HASH_SIZE}, - CosmWasmClient, Fee, QueryNymdClient, SigningCosmWasmClient, SigningNymdClient, TendermintTime, + CosmWasmClient, CosmosCoin, Fee, QueryNymdClient, SigningCosmWasmClient, SigningNymdClient, + TendermintTime, }; use validator_client::ValidatorClientError; @@ -155,6 +157,21 @@ impl Client { self.0.read().await.get_current_rewarding_interval().await } + pub(crate) async fn get_rewarding_status( + &self, + mix_identity: mixnet_contract::IdentityKey, + rewarding_interval_nonce: u32, + ) -> Result + where + C: CosmWasmClient + Sync, + { + self.0 + .read() + .await + .get_rewarding_status(mix_identity, rewarding_interval_nonce) + .await + } + /// Obtains the hash of a block specified by the provided height. /// If the resulting digest is empty, a `None` is returned instead. /// @@ -235,7 +252,77 @@ impl Client { Ok(()) } - pub(crate) async fn reward_mixnodes( + pub(crate) async fn reward_mixnode_and_all_delegators( + &self, + node: &MixnodeToReward, + rewarding_interval_nonce: u32, + ) -> Result<(), RewardingError> + where + C: SigningCosmWasmClient + Sync, + { + // determine how many times we are going to have to call the delegator rewarding, + // note that it doesn't include the "base" call to `RewardMixnode` that rewards one page + let further_calls = node.total_delegations / MIXNODE_DELEGATORS_PAGE_LIMIT; + + // start with the base call to reward operator and first page of delegators + let fee = self + .estimate_mixnode_reward_fees(1, MIXNODE_DELEGATORS_PAGE_LIMIT) + .await; + let msgs = vec![( + node.to_reward_execute_msg_v2(rewarding_interval_nonce), + vec![], + )]; + let memo = format!( + "operator + {} delegators rewarding", + MIXNODE_DELEGATORS_PAGE_LIMIT + ); + self.execute_multiple_with_retry(msgs, fee, memo).await?; + + // reward rest of delegators + let mut remaining_delegators = node.total_delegations - MIXNODE_DELEGATORS_PAGE_LIMIT; + let delegator_rewarding_msg = ( + node.to_next_delegator_reward_execute_msg_v2(rewarding_interval_nonce), + vec![], + ); + for _ in 0..further_calls { + let delegators_in_call = remaining_delegators.min(MIXNODE_DELEGATORS_PAGE_LIMIT); + let fee = self + .estimate_mixnode_reward_fees(1, delegators_in_call) + .await; + let msgs = vec![delegator_rewarding_msg.clone()]; + let memo = format!("rewarding another {} delegators", delegators_in_call); + self.execute_multiple_with_retry(msgs, fee, memo).await?; + + remaining_delegators -= MIXNODE_DELEGATORS_PAGE_LIMIT; + } + + Ok(()) + } + + pub(crate) async fn reward_mix_delegators( + &self, + node: &MixnodeToReward, + rewarding_interval_nonce: u32, + ) -> Result<(), RewardingError> + where + C: SigningCosmWasmClient + Sync, + { + // the fee is a tricky subject here because we don't know exactly how many delegators we missed, + // let's aim for the worst case scenario and assume it was the entire page + let fee = self + .estimate_mixnode_reward_fees(1, MIXNODE_DELEGATORS_PAGE_LIMIT) + .await; + let delegator_rewarding_msg = ( + node.to_next_delegator_reward_execute_msg_v2(rewarding_interval_nonce), + vec![], + ); + + let memo = "rewarding delegators".to_string(); + self.execute_multiple_with_retry(vec![delegator_rewarding_msg], fee, memo) + .await + } + + pub(crate) async fn reward_mixnodes_with_single_page_of_delegators( &self, nodes: &[MixnodeToReward], rewarding_interval_nonce: u32, @@ -249,12 +336,25 @@ impl Client { .await; let msgs: Vec<(ExecuteMsg, _)> = nodes .iter() - .map(|node| node.to_execute_msg(rewarding_interval_nonce)) + .map(|node| node.to_reward_execute_msg_v2(rewarding_interval_nonce)) .zip(std::iter::repeat(Vec::new())) .collect(); let memo = format!("rewarding {} mixnodes", msgs.len()); + self.execute_multiple_with_retry(msgs, fee, memo).await + } + + async fn execute_multiple_with_retry( + &self, + msgs: Vec<(M, Vec)>, + fee: Fee, + memo: String, + ) -> Result<(), RewardingError> + where + C: SigningCosmWasmClient + Sync, + M: Serialize + Clone + Send, + { let contract = self .0 .read() diff --git a/validator-api/src/rewarding/mod.rs b/validator-api/src/rewarding/mod.rs index f75bbb8ac5..48ad245acf 100644 --- a/validator-api/src/rewarding/mod.rs +++ b/validator-api/src/rewarding/mod.rs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 use crate::cache::ValidatorCache; -use crate::node_status_api::models::{MixnodeStatusReport, Uptime}; use crate::node_status_api::ONE_DAY; use crate::nymd_client::Client; use crate::rewarding::epoch::Epoch; @@ -13,8 +12,7 @@ use crate::storage::models::{ use crate::storage::ValidatorApiStorage; use log::{error, info}; use mixnet_contract::mixnode::NodeRewardParams; -use mixnet_contract::{ExecuteMsg, IdentityKey}; -use std::collections::HashMap; +use mixnet_contract::{ExecuteMsg, IdentityKey, RewardingStatus, MIXNODE_DELEGATORS_PAGE_LIMIT}; use std::convert::TryInto; use std::time::Duration; use time::OffsetDateTime; @@ -40,47 +38,39 @@ pub(crate) const PER_MIXNODE_DELEGATION_GAS_INCREASE: u64 = 2750; // the calculated total gas limit is going to get multiplied by that value. pub(crate) const REWARDING_GAS_LIMIT_MULTIPLIER: f64 = 1.05; -pub(crate) const MAX_TO_REWARD_AT_ONCE: usize = 50; - #[derive(Debug, Clone)] pub(crate) struct MixnodeToReward { pub(crate) identity: IdentityKey, - pub(crate) uptime: Uptime, /// Total number of individual addresses that have delegated to this particular node pub(crate) total_delegations: usize, + /// Node absolute uptime over total active set uptime - params: Option, + pub(crate) params: NodeRewardParams, } 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 - } else { - None - } + /// Somewhat clumsy way of feature gatting tokenomics payments. In a tokenomics scenario this will never be None at reward time. We leverage that to Into a different ExecuteMsg variant + fn params(&self) -> NodeRewardParams { + self.params } } impl MixnodeToReward { - pub(crate) fn to_execute_msg(&self, rewarding_interval_nonce: u32) -> ExecuteMsg { - ExecuteMsg::RewardMixnode { + pub(crate) fn to_reward_execute_msg_v2(&self, rewarding_interval_nonce: u32) -> ExecuteMsg { + ExecuteMsg::RewardMixnodeV2 { identity: self.identity.clone(), - uptime: self.uptime.u8() as u32, + params: self.params(), 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(), + pub(crate) fn to_next_delegator_reward_execute_msg_v2( + &self, + rewarding_interval_nonce: u32, + ) -> ExecuteMsg { + ExecuteMsg::RewardNextMixDelegators { + mix_identity: self.identity.clone(), rewarding_interval_nonce, } } @@ -149,56 +139,18 @@ impl Rewarder { .len()) } - /// Queries the smart contract in order to obtain the current list of bonded mixnodes and then - /// for each mixnode determines how many delegators it has. - async fn produce_active_mixnode_delegators_map( - &self, - ) -> Result, RewardingError> { - // Technically we could optimise it by creating a concurrent stream and executing multiple - // queries concurrently. - // - // I've actually tested that approach and for 5300 nodes running it all sequentially was taking around 19s - // while running it with 20 concurrent queries was taking around 4.5s. - // Note that the results were a bit biased as I was testing it against remote validator - // while in real world this would be making only local requests. - // During the test my average ping times to the machine were around 2.6ms. - // So I guess the network latency was 2.6ms * 5300 = 13.78s in total in the sequential case. - // - // HOWEVER, even if the method was taking that long in real world, - // in the grand scheme of things it makes absolutely no difference. If the rewards - // distribution is delayed by 15s, it changes nothing as the process itself is not - // instantaneous. - let mut map = HashMap::new(); - - let active_bonded_mixnodes = self.validator_cache.active_mixnodes().await.into_inner(); - for mix in active_bonded_mixnodes.into_iter() { - let delegator_count = self - .get_mixnode_delegators_count(mix.mix_node.identity_key.clone()) - .await?; - map.insert(mix.mix_node.identity_key, delegator_count); - } - - Ok(map) - } - - /// Given the list of mixnodes that were tested in the last epoch, tries to determine the - /// subset that are eligible for any rewards. - /// - /// As of right now, it is a rather straightforward process. It is checked whether the node - /// is currently bonded, has uptime > 0 and is part of the "active" set. - /// Unlike the typescript rewards script, it currently does not look at the verloc data nor - /// whether the non-mixing ports are open. + /// Obtain the list of current 'rewarded' set, determine their uptime in the provided epoch + /// and attach information required for rewarding. /// /// The method also obtains the number of delegators towards the node in order to more accurately /// approximate the required gas fees when distributing the rewards. /// /// # Arguments /// - /// * `active_mixnodes`: list of the nodes that were tested at least once by the network monitor - /// in the last epoch. + /// * `epoch`: current rewarding epoch async fn determine_eligible_mixnodes( &self, - active_mixnodes: &[MixnodeStatusReport], + epoch: Epoch, ) -> Result, RewardingError> { // Currently we don't have as many 'features' as in the typescript reward script, // such as we don't check ports or verloc data anymore. However, that's fine as @@ -206,75 +158,103 @@ impl Rewarder { // and the lack of port data / verloc data will eventually be balanced out anyway // by people hesitating to delegate to nodes without them and thus those nodes disappearing // from the active set (once introduced) - let mixnode_delegators = self.produce_active_mixnode_delegators_map().await?; let state = self.nymd_client.get_state_params().await?; - // 1. go through all active mixnodes - // 2. filter out nodes that are currently not in the active set (as `mixnode_delegators` was obtained by - // querying the validator) - // 3. determine uptime and attach delegators count - let mut eligible_nodes: Vec = active_mixnodes - .iter() - .filter_map(|mix| { - mixnode_delegators - .get(&mix.identity) - .map(|&total_delegations| MixnodeToReward { - identity: mix.identity.clone(), - uptime: mix.last_day, - total_delegations, - params: None, - }) - }) - .filter(|node| node.uptime.u8() > 0) - .collect(); + let reward_pool = self.nymd_client.get_reward_pool().await?; + let circulating_supply = self.nymd_client.get_circulating_supply().await?; + let sybil_resistance_percent = self.nymd_client.get_sybil_resistance_percent().await?; + let epoch_reward_percent = self.nymd_client.get_epoch_reward_percent().await?; - if cfg!(feature = "tokenomics") { - let reward_pool = self.nymd_client.get_reward_pool().await?; - let circulating_supply = self.nymd_client.get_circulating_supply().await?; - let sybil_resistance_percent = self.nymd_client.get_sybil_resistance_percent().await?; - let epoch_reward_percent = self.nymd_client.get_epoch_reward_percent().await?; - let k = state.mixnode_active_set_size; - let period_reward_pool = (reward_pool / 100) * epoch_reward_percent as u128; + // TODO: question to @durch: is k active set or 'rewarded' set? + let k = state.mixnode_active_set_size; + let period_reward_pool = (reward_pool / 100) * epoch_reward_percent as u128; - info!("Rewarding pool stats"); - info!("-- Reward pool: {} unym", reward_pool); - info!("---- Epoch reward pool: {} unym", period_reward_pool); - info!("-- Circulating supply: {} unym", circulating_supply); + info!("Rewarding pool stats"); + info!("-- Reward pool: {} unym", reward_pool); + info!("---- Epoch reward pool: {} unym", period_reward_pool); + info!("-- Circulating supply: {} unym", circulating_supply); - for mix in eligible_nodes.iter_mut() { - mix.params = Some(NodeRewardParams::new( + // 1. get list of 'rewarded' nodes + // 2. for each of them determine their delegator count + // 3. for each of them determine their uptime for the epoch + let rewarded_nodes = self.validator_cache.rewarded_mixnodes().await.into_inner(); + let mut nodes_with_delegations = Vec::with_capacity(rewarded_nodes.len()); + for rewarded_node in rewarded_nodes { + let delegator_count = self + .get_mixnode_delegators_count(rewarded_node.mix_node.identity_key.clone()) + .await?; + nodes_with_delegations.push((rewarded_node, delegator_count)); + } + + let mut eligible_nodes = Vec::with_capacity(nodes_with_delegations.len()); + for (rewarded_node, total_delegations) in nodes_with_delegations { + let uptime = self + .storage + .get_average_mixnode_uptime_in_interval( + rewarded_node.identity(), + epoch.start_unix_timestamp(), + epoch.end_unix_timestamp(), + ) + .await?; + + eligible_nodes.push(MixnodeToReward { + identity: rewarded_node.mix_node.identity_key, + total_delegations, + params: NodeRewardParams::new( period_reward_pool, k.into(), 0, circulating_supply, - mix.uptime.u8().into(), + uptime.u8().into(), sybil_resistance_percent, - )); - } - } else { - info!("Tokenomics feature is OFF"); + ), + }) } Ok(eligible_nodes) } - /// Obtains the lists of all mixnodes that were tested at least a single time - /// by the network monitor in the specified epoch. + /// Check whether every node, and their delegators, on the provided list were fully rewarded + /// in the specified interval. /// - /// # Arguments + /// It is used to deal with edge cases such that mixnode had exactly full page of delegations and + /// somebody created a new delegation thus causing the "last" delegator to possibly be pushed + /// onto the next page that the validator API was not aware of. /// - /// * `epoch`: the specified epoch. - async fn get_active_monitor_mixnodes( + /// * `eligible_mixnodes`: list of the nodes that were eligible to receive rewards. + /// * `rewarding_interval_nonce`: nonce associated with the current rewarding interval + async fn verify_rewarding_completion( &self, - epoch: Epoch, - ) -> Result, RewardingError> { - Ok(self - .storage - .get_all_active_mixnode_reports_in_interval( - epoch.start_unix_timestamp(), - epoch.end_unix_timestamp(), - ) - .await?) + eligible_mixnodes: &[MixnodeToReward], + current_rewarding_nonce: u32, + ) -> (Vec, Vec) { + let mut unrewarded = Vec::new(); + let mut further_delegators_present = Vec::new(); + for mix in eligible_mixnodes { + match self + .nymd_client + .get_rewarding_status(mix.identity.clone(), current_rewarding_nonce) + .await + { + Ok(rewarding_status) => match rewarding_status.status { + // that case is super weird, it implies the node hasn't been rewarded at all! + // maybe the transaction timed out twice or something? In any case, we should attempt + // the reward for the final time! + None => unrewarded.push(mix.clone()), + Some(RewardingStatus::PendingNextDelegatorPage(_)) => { + further_delegators_present.push(mix.clone()) + } + Some(RewardingStatus::Complete(_)) => {} + }, + Err(err) => { + error!( + "failed to query rewarding status of {} - {}", + mix.identity, err + ) + } + } + } + (unrewarded, further_delegators_present) } /// Using the list of mixnodes eligible for rewards, chunks it into pre-defined sized-chunks @@ -290,7 +270,7 @@ impl Rewarder { /// /// # Arguments /// - /// * `eligible_mixnodes`: list of the nodes that are eligible to receive non-zero rewards. + /// * `eligible_mixnodes`: list of the nodes that are eligible to receive rewards. /// * `rewarding_interval_nonce`: nonce associated with the current rewarding interval async fn distribute_rewards_to_mixnodes( &self, @@ -299,10 +279,80 @@ impl Rewarder { ) -> Option> { let mut failed_chunks = Vec::new(); - for (i, mix_chunk) in eligible_mixnodes.chunks(MAX_TO_REWARD_AT_ONCE).enumerate() { + // construct chunks such that we reward at most MIXNODE_DELEGATORS_PAGE_LIMIT delegators per block + + // nodes with > MIXNODE_DELEGATORS_PAGE_LIMIT delegators that have to be treated in a special way, + // because we cannot batch them together + let mut individually_rewarded = Vec::new(); + + // sets of nodes that together they have < MIXNODE_DELEGATORS_PAGE_LIMIT delegators + let mut batch_rewarded = vec![vec![]]; + let mut current_batch_i = 0; + let mut current_batch_total = 0; + + // right now put mixes into batches super naively, if it doesn't fit into the current one, + // create a new one. + for mix in eligible_mixnodes { + // if mixnode has uptime of 0, no rewarding will actually happen regardless of number of delegators, + // so we can just batch it with the current batch + if mix.params.uptime() == 0 { + batch_rewarded[current_batch_i].push(mix.clone()); + continue; + } + + if mix.total_delegations > MIXNODE_DELEGATORS_PAGE_LIMIT { + individually_rewarded.push(mix) + } else if current_batch_total + mix.total_delegations < MIXNODE_DELEGATORS_PAGE_LIMIT { + batch_rewarded[current_batch_i].push(mix.clone()); + current_batch_total += mix.total_delegations; + } else { + batch_rewarded.push(vec![mix.clone()]); + current_batch_i += 1; + current_batch_total = 0; + } + } + + let mut total_rewarded = 0; + + // start rewarding, first the nodes that are dealt with individually, i.e. nodes that + // need to have their own special blocks due to number of delegators + for mix in individually_rewarded { if let Err(err) = self .nymd_client - .reward_mixnodes(mix_chunk, rewarding_interval_nonce) + .reward_mixnode_and_all_delegators(mix, 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 + if !err.is_tendermint_duplicate() { + error!("failed to reward mixnode with all delegators... - {}", err); + failed_chunks.push(FailedMixnodeRewardChunkDetails { + possibly_unrewarded: vec![mix.clone()], + error_message: err.to_string(), + }); + } + sleep(Duration::from_secs(11)).await; + } + + total_rewarded += 1; + let percentage = total_rewarded as f32 * 100.0 / eligible_mixnodes.len() as f32; + info!( + "Rewarded {} / {} mixnodes\t{:.2}%", + total_rewarded, + eligible_mixnodes.len(), + percentage + ); + } + + // then we move onto the chunks + for mix_chunk in batch_rewarded { + if let Err(err) = self + .nymd_client + .reward_mixnodes_with_single_page_of_delegators( + &mix_chunk, + rewarding_interval_nonce, + ) .await { // this is a super weird edge case that we didn't catch change to sequence and @@ -317,11 +367,12 @@ impl Rewarder { } sleep(Duration::from_secs(11)).await; } - let rewarded = i * MAX_TO_REWARD_AT_ONCE + mix_chunk.len(); - let percentage = rewarded as f32 * 100.0 / eligible_mixnodes.len() as f32; + + total_rewarded += mix_chunk.len(); + let percentage = total_rewarded as f32 * 100.0 / eligible_mixnodes.len() as f32; info!( "Rewarded {} / {} mixnodes\t{:.2}%", - rewarded, + total_rewarded, eligible_mixnodes.len(), percentage ); @@ -334,28 +385,53 @@ impl Rewarder { } } + /// For each mixnode on the list, try to "continue" rewarding its delegators. + /// Note: due to the checks inside the smart contract, it's impossible to accidentally + /// reward the same mixnode (or delegator) twice during particular rewarding interval. + /// + /// Realistically if this method is ever called, it will be only done once per node, so there's + /// no need to determine the exact number of missed delegators. + /// + /// * `nodes`: mixnodes which delegators did not receive all rewards in this epoch. + /// * `rewarding_interval_nonce`: nonce associated with the current rewarding interval. + async fn reward_missed_delegators( + &self, + nodes: &[MixnodeToReward], + rewarding_interval_nonce: u32, + ) { + for missed_node in nodes { + if let Err(err) = self + .nymd_client + .reward_mix_delegators(missed_node, rewarding_interval_nonce) + .await + { + warn!( + "failed to attempt to reward missed delegators of node {} - {}", + missed_node.identity, err + ) + } + } + } + /// Using the list of active mixnode and gateways, determine which of them are eligible for /// rewarding and distribute the rewards. /// /// # Arguments /// - /// * `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. + /// * `epoch_rewarding_id`: id of the current epoch rewarding as stored in the database. + /// * `epoch`: current rewarding epoch async fn distribute_rewards( &self, epoch_rewarding_database_id: i64, - active_monitor_mixnodes: &[MixnodeStatusReport], + epoch: Epoch, ) -> Result<(RewardingReport, Option), RewardingError> { let mut failure_data = FailureData::default(); - let eligible_mixnodes = self - .determine_eligible_mixnodes(active_monitor_mixnodes) - .await?; + let eligible_mixnodes = self.determine_eligible_mixnodes(epoch).await?; if eligible_mixnodes.is_empty() { return Err(RewardingError::NoMixnodesToReward); } + let total_eligible = eligible_mixnodes.len(); let current_rewarding_nonce = self .nymd_client @@ -369,9 +445,43 @@ impl Rewarder { .distribute_rewards_to_mixnodes(&eligible_mixnodes, current_rewarding_nonce + 1) .await; + let mut nodes_to_verify = eligible_mixnodes; + + // if there's some underlying networking error or something, don't keep retrying forever + let mut retries_allowed = 5; + loop { + if retries_allowed <= 0 { + break; + } + let (unrewarded, mut pending_delegators) = self + .verify_rewarding_completion(&nodes_to_verify, current_rewarding_nonce + 1) + .await; + if unrewarded.is_empty() && pending_delegators.is_empty() { + // we're all good - everyone got their rewards + break; + } + + if !unrewarded.is_empty() { + // no need to save failure data as we already know about those from the very first run + self.distribute_rewards_to_mixnodes(&unrewarded, current_rewarding_nonce + 1) + .await; + } + + if !pending_delegators.is_empty() { + self.reward_missed_delegators(&pending_delegators, current_rewarding_nonce + 1) + .await; + } + + // no point in verifying EVERYTHING again, just check the nodes that went through retries + nodes_to_verify = unrewarded; + nodes_to_verify.append(&mut pending_delegators); + + retries_allowed -= 1; + } + let report = RewardingReport { epoch_rewarding_id: epoch_rewarding_database_id, - eligible_mixnodes: eligible_mixnodes.len() as i64, + eligible_mixnodes: total_eligible as i64, possibly_unrewarded_mixnodes: failure_data .mixnodes .as_ref() @@ -426,7 +536,7 @@ impl Rewarder { .insert_possibly_unrewarded_mixnode(PossiblyUnrewardedMixnode { chunk_id, identity: node.identity, - uptime: node.uptime.u8(), + uptime: node.params.uptime() as u8, }) .await?; } @@ -553,9 +663,6 @@ impl Rewarder { epoch ); - // get nodes that were active during the epoch - let active_monitor_mixnodes = self.get_active_monitor_mixnodes(epoch).await?; - // insert information about beginning the procedure (so that if we crash during it, // we wouldn't attempt to possibly double reward operators) let epoch_rewarding_id = self @@ -563,9 +670,7 @@ impl Rewarder { .insert_started_epoch_rewarding(epoch.start_unix_timestamp()) .await?; - let (report, failure_data) = self - .distribute_rewards(epoch_rewarding_id, &active_monitor_mixnodes) - .await?; + let (report, failure_data) = self.distribute_rewards(epoch_rewarding_id, epoch).await?; self.storage .finish_rewarding_epoch_and_insert_report(report) diff --git a/validator-api/src/storage/mod.rs b/validator-api/src/storage/mod.rs index 44c466ed02..449c205e3d 100644 --- a/validator-api/src/storage/mod.rs +++ b/validator-api/src/storage/mod.rs @@ -4,7 +4,7 @@ use crate::network_monitor::monitor::summary_producer::NodeResult; use crate::network_monitor::test_route::TestRoute; use crate::node_status_api::models::{ - GatewayStatusReport, GatewayUptimeHistory, MixnodeStatusReport, MixnodeUptimeHistory, + GatewayStatusReport, GatewayUptimeHistory, MixnodeStatusReport, MixnodeUptimeHistory, Uptime, ValidatorApiStorageError, }; use crate::node_status_api::{ONE_DAY, ONE_HOUR}; @@ -264,6 +264,54 @@ impl ValidatorApiStorage { )) } + /// Based on the data available in the validator API, determines the average uptime of particular + /// mixnode during the specified time interval. + /// + /// # Arguments + /// + /// * `identity`: base58-encoded identity of the mixnode. + /// * `since`: unix timestamp indicating the lower bound interval of the selection. + /// * `end`: unix timestamp indicating the upper bound interval of the selection. + pub(crate) async fn get_average_mixnode_uptime_in_interval( + &self, + identity: &str, + start: UnixTimestamp, + end: UnixTimestamp, + ) -> Result { + let mixnode_database_id = match self + .manager + .get_mixnode_id(identity) + .await + .map_err(|_| ValidatorApiStorageError::InternalDatabaseError)? + { + Some(id) => id, + None => return Ok(Uptime::zero()), + }; + + let monitor_runs = self.get_monitor_runs_count(start, end).await?; + let mixnode_statuses = self + .manager + .get_mixnode_statuses_by_id(mixnode_database_id, start, end) + .await + .map_err(|_| ValidatorApiStorageError::InternalDatabaseError)?; + + let mut total: f32 = 0.0; + for mixnode_status in mixnode_statuses { + total += mixnode_status.reliability as f32; + } + + let uptime = match Uptime::from_uptime_sum(total, monitor_runs) { + Ok(uptime) => uptime, + Err(_) => { + // this should really ever happen... + error!("mixnode {} has uptime > 100!", identity); + Uptime::default() + } + }; + + Ok(uptime) + } + /// Obtain status reports of mixnodes that were active in the specified time interval. /// /// # Arguments @@ -633,7 +681,7 @@ impl ValidatorApiStorage { } //////////////////////////////////////////////////////////////////////// - // TODO: Should all of the below really return a "NodeStatusApi" Errors? + // TODO: Should all of the below really return a "ValidatorApiStorageError" Errors? //////////////////////////////////////////////////////////////////////// /// Inserts information about starting new epoch rewarding into the database.