diff --git a/.gitignore b/.gitignore index ac7542786c..32512e37ec 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,8 @@ validator-api/v4.json validator-api/v6.json **/node_modules validator-api/keypair +contracts/mixnet/code_id +contracts/mixnet/Justfile +contracts/mixnet/Makefile +validator-config +*.patch \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 034a979e0f..ee1f369724 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -228,6 +228,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +[[package]] +name = "az" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d6dff4a1892b54d70af377bf7a17064192e822865791d812957f21e3108c325" + [[package]] name = "base-x" version = "0.2.8" @@ -411,6 +417,12 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" +[[package]] +name = "bytemuck" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72957246c41db82b8ef88a5486143830adeb8227ef9837740bdec67724cf2c5b" + [[package]] name = "byteorder" version = "1.4.3" @@ -1642,6 +1654,18 @@ dependencies = [ "winapi", ] +[[package]] +name = "fixed" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d333a26ec13a023c6dff4b7584de4d323cfee2e508f5dd2bbee6669e4f7efdf" +dependencies = [ + "az", + "bytemuck", + "half", + "typenum", +] + [[package]] name = "flate2" version = "1.0.22" @@ -2271,6 +2295,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "half" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" + [[package]] name = "handlebars" version = "3.5.5" @@ -2986,10 +3016,15 @@ dependencies = [ name = "mixnet-contract" version = "0.1.0" dependencies = [ + "az", "cosmwasm-std", + "fixed", + "log", + "network-defaults", "schemars", "serde", "serde_repr", + "thiserror", "ts-rs", ] diff --git a/Cargo.toml b/Cargo.toml index 02ceec5e95..8d959736a0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,4 +62,4 @@ default-members = [ "validator-api", ] -exclude = ["explorer", "contracts"] +exclude = ["explorer", "contracts", "tokenomics-py"] diff --git a/README.md b/README.md index 155ebc01ea..cd149437b8 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,6 @@ SPDX-License-Identifier: Apache-2.0 ## The Nym Privacy Platform -This repository contains the Nym mixnet. - The platform is composed of multiple Rust crates. Top-level executable binary crates include: * nym-mixnode - shuffles [Sphinx](https://github.com/nymtech/sphinx) packets together to provide privacy against network-level attackers. @@ -33,6 +31,45 @@ There's a `.env.sample-dev` file provided which you can rename to `.env` if you You can chat to us in [Keybase](https://keybase.io). Download their chat app, then click **Teams -> Join a team**. Type **nymtech.friends** into the team name and hit **continue**. For general chat, hang out in the **#general** channel. Our development takes places in the **#dev** channel. Node operators should be in the **#node-operators** channel. +### Rewards + +Node, node operator and delegator rewards are determined according to the principles laid out in the section 6 of [Nym Whitepaper](https://nymtech.net/nym-whitepaper.pdf). Below is a TLDR of the variables and formulas involved in calculating the epoch rewards. Initial reward pool is set to 250 million Nym, making the circulating supply 750 million Nym. + +|Symbol|Definition| +|---|---| +||global share of rewards available, starts at 2% of the reward pool. +||node reward for mixnode `i`. +||ratio of total node stake (node bond + all delegations) to the token circulating supply. +||ratio of stake operator has plaged to their node to the token circulating supply. +||fraction of total effort undertaken by node `i`, set to `1/k` in testnet Milhon. +||number of nodes stakeholders are incentivised to create, set by the validators, a matter of governance. Currently determined by the `active set` size, and set to 5000 in testnet Milhon. +||Sybil attack resistance parameter - the higher this parameter is set the stronger the reduction in competitivness gets for a Sybil attacker. +||declared profit margin of operator `i`, defaults to 10% in testnet Milhon. +||uptime of node `i`, scaled to 0 - 1, for the rewarding epoch +||cost of operating node `i` for the duration of the rewarding eopoch, set to 40 Nym for testnet Milhon. + +Node reward for node `i` is determined as: + + + +where: + + + +and + + + +Operator of node `i` is credited with the following amount: + + + +Delegate with stake `s` recieves: + + + +where `s'` is stake `s` scaled over total token circulating supply. + ### Licensing and copyright information This program is available as open source under the terms of the Apache 2.0 license. However, some elements are being licensed under CC0-1.0 and MIT. For accurate information, please check individual files. diff --git a/common/client-libs/gateway-client/src/client.rs b/common/client-libs/gateway-client/src/client.rs index da9fee7bd2..487280157e 100644 --- a/common/client-libs/gateway-client/src/client.rs +++ b/common/client-libs/gateway-client/src/client.rs @@ -523,8 +523,11 @@ impl GatewayClient { return Err(GatewayClientError::NotAuthenticated); } #[cfg(feature = "coconut")] - if self.estimate_required_bandwidth(&packets) < self.bandwidth_remaining { - return Err(GatewayClientError::NotEnoughBandwidth); + if self.bandwidth_remaining <= self.estimate_required_bandwidth(&packets) { + return Err(GatewayClientError::NotEnoughBandwidth(( + self.estimate_required_bandwidth(&packets), + self.bandwidth_remaining, + ))); } if !self.connection.is_established() { return Err(GatewayClientError::ConnectionNotEstablished); @@ -592,7 +595,10 @@ impl GatewayClient { } #[cfg(feature = "coconut")] if (mix_packet.sphinx_packet().len() as i64) > self.bandwidth_remaining { - return Err(GatewayClientError::NotEnoughBandwidth); + return Err(GatewayClientError::NotEnoughBandwidth(( + (mix_packet.sphinx_packet().len() as i64), + self.bandwidth_remaining, + ))); } if !self.connection.is_established() { return Err(GatewayClientError::ConnectionNotEstablished); @@ -631,7 +637,10 @@ impl GatewayClient { } #[cfg(feature = "coconut")] if self.bandwidth_remaining <= 0 { - return Err(GatewayClientError::NotEnoughBandwidth); + return Err(GatewayClientError::NotEnoughBandwidth(( + 0, + self.bandwidth_remaining, + ))); } if self.connection.is_partially_delegated() { return Ok(()); diff --git a/common/client-libs/gateway-client/src/error.rs b/common/client-libs/gateway-client/src/error.rs index 170314e484..90cd77300d 100644 --- a/common/client-libs/gateway-client/src/error.rs +++ b/common/client-libs/gateway-client/src/error.rs @@ -23,7 +23,7 @@ pub enum GatewayClientError { MalformedResponse, SerializeCredential, NotAuthenticated, - NotEnoughBandwidth, + NotEnoughBandwidth((i64, i64)), UnexpectedResponse, ConnectionInInvalidState, RegistrationFailure(HandshakeError), @@ -102,8 +102,12 @@ impl fmt::Display for GatewayClientError { write!(f, "gateway returned an error response - {}", err) } GatewayClientError::UnexpectedResponse => write!(f, "received an unexpected response"), - GatewayClientError::NotEnoughBandwidth => { - write!(f, "client does not have enough bandwidth") + GatewayClientError::NotEnoughBandwidth((estimated, remaining)) => { + write!( + f, + "Client does not have enough bandwidth: estimated {}, remaining: {}", + estimated, remaining + ) } GatewayClientError::SerializeCredential => { write!(f, "credential could not be serialized") diff --git a/common/client-libs/gateway-client/src/socket_state.rs b/common/client-libs/gateway-client/src/socket_state.rs index b82e9907b3..9fba57208f 100644 --- a/common/client-libs/gateway-client/src/socket_state.rs +++ b/common/client-libs/gateway-client/src/socket_state.rs @@ -119,7 +119,7 @@ impl PartiallyDelegated { } .is_err() { - panic!("failed to send back `mixnet_receiver_future` result on the oneshot channel") + warn!("failed to send back `mixnet_receiver_future` result on the oneshot channel") } }; diff --git a/common/client-libs/validator-client/src/client.rs b/common/client-libs/validator-client/src/client.rs index 0e2533b37d..5312e248c0 100644 --- a/common/client-libs/validator-client/src/client.rs +++ b/common/client-libs/validator-client/src/client.rs @@ -172,6 +172,34 @@ impl Client { Ok(self.nymd.get_state_params().await?) } + pub async fn get_reward_pool(&self) -> Result + where + C: CosmWasmClient + Sync, + { + Ok(self.nymd.get_reward_pool().await?.u128()) + } + + pub async fn get_circulating_supply(&self) -> Result + where + C: CosmWasmClient + Sync, + { + Ok(self.nymd.get_circulating_supply().await?.u128()) + } + + pub async fn get_sybil_resistance_percent(&self) -> Result + where + C: CosmWasmClient + Sync, + { + Ok(self.nymd.get_sybil_resistance_percent().await?) + } + + pub async fn get_epoch_reward_percent(&self) -> Result + where + C: CosmWasmClient + Sync, + { + Ok(self.nymd.get_epoch_reward_percent().await?) + } + // basically handles paging for us pub async fn get_all_nymd_mixnodes(&self) -> Result, ValidatorClientError> where diff --git a/common/client-libs/validator-client/src/nymd/mod.rs b/common/client-libs/validator-client/src/nymd/mod.rs index 54477ca013..7fc1685849 100644 --- a/common/client-libs/validator-client/src/nymd/mod.rs +++ b/common/client-libs/validator-client/src/nymd/mod.rs @@ -11,7 +11,7 @@ use crate::nymd::fee_helpers::Operation; use crate::nymd::wallet::DirectSecp256k1HdWallet; use cosmrs::rpc::endpoint::broadcast; use cosmrs::rpc::{Error as TendermintRpcError, HttpClientUrl}; -use cosmwasm_std::Coin; +use cosmwasm_std::{Coin, Uint128}; use mixnet_contract::{ Addr, Delegation, ExecuteMsg, Gateway, GatewayOwnershipResponse, IdentityKey, LayerDistribution, MixNode, MixOwnershipResponse, PagedAllDelegationsResponse, @@ -211,6 +211,46 @@ impl NymdClient { .await } + pub async fn get_reward_pool(&self) -> Result + where + C: CosmWasmClient + Sync, + { + let request = QueryMsg::GetRewardPool {}; + self.client + .query_contract_smart(self.contract_address()?, &request) + .await + } + + pub async fn get_circulating_supply(&self) -> Result + where + C: CosmWasmClient + Sync, + { + let request = QueryMsg::GetCirculatingSupply {}; + self.client + .query_contract_smart(self.contract_address()?, &request) + .await + } + + pub async fn get_sybil_resistance_percent(&self) -> Result + where + C: CosmWasmClient + Sync, + { + let request = QueryMsg::GetSybilResistancePercent {}; + self.client + .query_contract_smart(self.contract_address()?, &request) + .await + } + + pub async fn get_epoch_reward_percent(&self) -> Result + where + C: CosmWasmClient + Sync, + { + let request = QueryMsg::GetEpochRewardPercent {}; + self.client + .query_contract_smart(self.contract_address()?, &request) + .await + } + /// Checks whether there is a bonded mixnode associated with the provided client's address pub async fn owns_mixnode(&self, address: &AccountId) -> Result where diff --git a/common/mixnet-contract/Cargo.toml b/common/mixnet-contract/Cargo.toml index 67c94e3355..fba7fecfd1 100644 --- a/common/mixnet-contract/Cargo.toml +++ b/common/mixnet-contract/Cargo.toml @@ -8,10 +8,18 @@ edition = "2018" [dependencies] # this branch is identical to 0.14.1 with addition of updated k256 dependency required to help poor cargo choose correct version -cosmwasm-std = { git = "https://github.com/jstuczyn/cosmwasm", branch="0.14.1-updatedk256" } +cosmwasm-std = { git = "https://github.com/jstuczyn/cosmwasm", branch = "0.14.1-updatedk256" } #cosmwasm-std = { version = "0.14.1" } serde = { version = "1.0", features = ["derive"] } serde_repr = "0.1" schemars = "0.8" -ts-rs = "3.0" +ts-rs = { version = "3.0", optional = true } +thiserror = "1.0" +network-defaults = { path = "../network-defaults" } +fixed = "1.1" +az = "1.1" +log = "0.4.14" + +[features] +default = ["ts-rs"] diff --git a/common/mixnet-contract/src/error.rs b/common/mixnet-contract/src/error.rs new file mode 100644 index 0000000000..b338d12125 --- /dev/null +++ b/common/mixnet-contract/src/error.rs @@ -0,0 +1,9 @@ +use thiserror::Error; + +#[derive(Error, Debug, PartialEq)] +pub enum MixnetContractError { + #[error("Overflow Error")] + OverflowError(#[from] cosmwasm_std::OverflowError), + #[error("reward_blockstamp field not set, set_reward_blockstamp must be called before attempting to issue rewards")] + BlockstampNotSet, +} diff --git a/common/mixnet-contract/src/gateway.rs b/common/mixnet-contract/src/gateway.rs index 25c9fe0bab..23ab18b11d 100644 --- a/common/mixnet-contract/src/gateway.rs +++ b/common/mixnet-contract/src/gateway.rs @@ -7,9 +7,9 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::cmp::Ordering; use std::fmt::Display; -use ts_rs::TS; -#[derive(Clone, Debug, Deserialize, PartialEq, PartialOrd, Serialize, JsonSchema, TS)] +#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))] +#[derive(Clone, Debug, Deserialize, PartialEq, PartialOrd, Serialize, JsonSchema)] pub struct Gateway { pub host: String, pub mix_port: u16, diff --git a/common/mixnet-contract/src/lib.rs b/common/mixnet-contract/src/lib.rs index ffecdea57c..04c758ec29 100644 --- a/common/mixnet-contract/src/lib.rs +++ b/common/mixnet-contract/src/lib.rs @@ -2,8 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 mod delegation; +pub mod error; mod gateway; -mod mixnode; +pub mod mixnode; mod msg; mod types; diff --git a/common/mixnet-contract/src/mixnode.rs b/common/mixnet-contract/src/mixnode.rs index f6b3281580..82d8b4c33c 100644 --- a/common/mixnet-contract/src/mixnode.rs +++ b/common/mixnet-contract/src/mixnode.rs @@ -2,15 +2,20 @@ #![allow(clippy::field_reassign_with_default)] use crate::{IdentityKey, SphinxKey}; -use cosmwasm_std::{coin, Addr, Coin}; +use az::CheckedCast; +use cosmwasm_std::{coin, Addr, Coin, Uint128}; +use log::error; +use network_defaults::{DEFAULT_OPERATOR_EPOCH_COST, DEFAULT_PROFIT_MARGIN}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; use std::cmp::Ordering; use std::fmt::Display; -use ts_rs::TS; -#[derive(Clone, Debug, Deserialize, PartialEq, PartialOrd, Serialize, JsonSchema, TS)] +type U128 = fixed::types::U75F53; // u128 with 18 significant digits + +#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))] +#[derive(Clone, Debug, Deserialize, PartialEq, PartialOrd, Serialize, JsonSchema)] pub struct MixNode { pub host: String, pub mix_port: u16, @@ -43,6 +48,97 @@ pub enum Layer { Three = 3, } +#[derive(Debug, Clone, JsonSchema, PartialEq, Serialize, Deserialize, Copy)] +pub struct NodeRewardParams { + period_reward_pool: Uint128, + k: Uint128, + reward_blockstamp: u64, + circulating_supply: Uint128, + uptime: Uint128, + sybil_resistance_percent: u8, +} + +impl NodeRewardParams { + pub fn new( + period_reward_pool: u128, + k: u128, + reward_blockstamp: u64, + circulating_supply: u128, + uptime: u128, + sybil_resistance_percent: u8, + ) -> NodeRewardParams { + NodeRewardParams { + period_reward_pool: Uint128(period_reward_pool), + k: Uint128(k), + reward_blockstamp, + circulating_supply: Uint128(circulating_supply), + uptime: Uint128(uptime), + sybil_resistance_percent, + } + } + + pub fn performance(&self) -> U128 { + U128::from_num(self.uptime.u128()) / U128::from_num(100) + } + + pub fn operator_cost(&self) -> U128 { + U128::from_num(self.uptime.u128() / 100u128 * DEFAULT_OPERATOR_EPOCH_COST as u128) + } + + pub fn set_reward_blockstamp(&mut self, blockstamp: u64) { + self.reward_blockstamp = blockstamp; + } + + pub fn period_reward_pool(&self) -> u128 { + self.period_reward_pool.u128() + } + + pub fn k(&self) -> u128 { + self.k.u128() + } + + pub fn circulating_supply(&self) -> u128 { + self.circulating_supply.u128() + } + + pub fn reward_blockstamp(&self) -> u64 { + self.reward_blockstamp + } + + pub fn uptime(&self) -> u128 { + self.uptime.u128() + } + + pub fn one_over_k(&self) -> U128 { + U128::from_num(1) / U128::from_num(self.k.u128()) + } + + pub fn alpha(&self) -> U128 { + U128::from_num(self.sybil_resistance_percent) / U128::from_num(100) + } +} + +#[derive(Debug)] +pub struct NodeRewardResult { + reward: U128, + lambda: U128, + sigma: U128, +} + +impl NodeRewardResult { + pub fn reward(&self) -> U128 { + self.reward + } + + pub fn lambda(&self) -> U128 { + self.lambda + } + + pub fn sigma(&self) -> U128 { + self.sigma + } +} + #[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] pub struct MixNodeBond { pub bond_amount: Coin, @@ -51,6 +147,7 @@ pub struct MixNodeBond { pub layer: Layer, pub block_height: u64, pub mix_node: MixNode, + pub profit_margin_percent: Option, } impl MixNodeBond { @@ -60,6 +157,7 @@ impl MixNodeBond { layer: Layer, block_height: u64, mix_node: MixNode, + profit_margin_percent: Option, ) -> Self { MixNodeBond { total_delegation: coin(0, &bond_amount.denom), @@ -68,9 +166,15 @@ impl MixNodeBond { layer, block_height, mix_node, + profit_margin_percent, } } + pub fn profit_margin(&self) -> U128 { + U128::from_num(self.profit_margin_percent.unwrap_or(DEFAULT_PROFIT_MARGIN)) + / U128::from_num(100) + } + pub fn identity(&self) -> &String { &self.mix_node.identity_key } @@ -86,6 +190,116 @@ impl MixNodeBond { pub fn mix_node(&self) -> &MixNode { &self.mix_node } + + pub fn total_delegation(&self) -> Coin { + self.total_delegation.clone() + } + + pub fn bond_to_circulating_supply(&self, circulating_supply: u128) -> U128 { + U128::from_num(self.bond_amount().amount.u128()) / U128::from_num(circulating_supply) + } + + pub fn total_stake_to_circulating_supply(&self, circulating_supply: u128) -> U128 { + U128::from_num(self.bond_amount().amount.u128() + self.total_delegation().amount.u128()) + / U128::from_num(circulating_supply) + } + + pub fn lambda(&self, params: &NodeRewardParams) -> U128 { + // Ratio of a bond to the token circulating supply + let bond_to_circulating_supply_ratio = + self.bond_to_circulating_supply(params.circulating_supply()); + bond_to_circulating_supply_ratio.min(params.one_over_k()) + } + + pub fn sigma(&self, params: &NodeRewardParams) -> U128 { + // Ratio of a delegation to the the token circulating supply + let total_stake_to_circulating_supply_ratio = + self.total_stake_to_circulating_supply(params.circulating_supply()); + total_stake_to_circulating_supply_ratio.min(params.one_over_k()) + } + + 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 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()); + + NodeRewardResult { + reward, + lambda, + sigma, + } + } + + pub fn node_profit(&self, params: &NodeRewardParams) -> U128 { + if self.reward(params).reward() < params.operator_cost() { + U128::from_num(0) + } else { + self.reward(params).reward() - params.operator_cost() + } + } + + pub fn operator_reward(&self, params: &NodeRewardParams) -> u128 { + let reward = self.reward(params); + let profit = if reward.reward < params.operator_cost() { + U128::from_num(0) + } else { + reward.reward - params.operator_cost() + }; + 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) + * profit; + + let reward = (operator_reward + operator_base_reward).max(U128::from_num(0)); + + if let Some(int_reward) = reward.checked_cast() { + int_reward + } else { + error!( + "Could not cast reward ({}) to u128, returning 0 - mixnode {}", + reward, + self.identity() + ); + 0u128 + } + } + + pub fn sigma_ratio(&self, params: &NodeRewardParams) -> U128 { + if self.total_stake_to_circulating_supply(params.circulating_supply()) < params.one_over_k() + { + self.total_stake_to_circulating_supply(params.circulating_supply()) + } else { + params.one_over_k() + } + } + + 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 + } + } } impl PartialOrd for MixNodeBond { @@ -217,6 +431,7 @@ mod tests { layer: Layer::One, block_height: 100, mix_node: mixnode_fixture(), + profit_margin_percent: Some(10), }; let mix2 = MixNodeBond { @@ -226,6 +441,7 @@ mod tests { layer: Layer::One, block_height: 120, mix_node: mixnode_fixture(), + profit_margin_percent: Some(10), }; let mix3 = MixNodeBond { @@ -235,6 +451,7 @@ mod tests { layer: Layer::One, block_height: 120, mix_node: mixnode_fixture(), + profit_margin_percent: Some(10), }; let mix4 = MixNodeBond { @@ -244,6 +461,7 @@ mod tests { layer: Layer::One, block_height: 120, mix_node: mixnode_fixture(), + profit_margin_percent: Some(10), }; let mix5 = MixNodeBond { @@ -253,6 +471,7 @@ mod tests { layer: Layer::One, block_height: 120, mix_node: mixnode_fixture(), + profit_margin_percent: Some(10), }; // summary: diff --git a/common/mixnet-contract/src/msg.rs b/common/mixnet-contract/src/msg.rs index dfda40befa..84f872fdc0 100644 --- a/common/mixnet-contract/src/msg.rs +++ b/common/mixnet-contract/src/msg.rs @@ -1,6 +1,7 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::mixnode::NodeRewardParams; use crate::StateParams; use crate::{Gateway, IdentityKey, MixNode}; use cosmwasm_std::Addr; @@ -36,6 +37,12 @@ pub enum ExecuteMsg { // percentage value in range 0-100 uptime: u32, }, + + RewardMixnodeV2 { + identity: IdentityKey, + // percentage value in range 0-100 + params: NodeRewardParams, + }, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] @@ -75,6 +82,10 @@ pub enum QueryMsg { address: Addr, }, LayerDistribution {}, + GetRewardPool {}, + GetCirculatingSupply {}, + GetEpochRewardPercent {}, + GetSybilResistancePercent {}, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] diff --git a/common/network-defaults/src/lib.rs b/common/network-defaults/src/lib.rs index 1542c8a3ad..e02b6e738e 100644 --- a/common/network-defaults/src/lib.rs +++ b/common/network-defaults/src/lib.rs @@ -93,4 +93,11 @@ pub const VALIDATOR_API_VERSION: &str = "v1"; // REWARDING pub const DEFAULT_FIRST_EPOCH_START: OffsetDateTime = time::macros::datetime!(2021-08-23 12:00 UTC); -pub const DEFAULT_EPOCH_LENGTH: Duration = Duration::from_secs(24 * 60 * 60); // 24h +pub const DEFAULT_EPOCH_LENGTH: Duration = Duration::from_secs(24 * 60 * 60 * 30); // 30 days +/// 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 +pub const DEFAULT_OPERATOR_EPOCH_COST: u64 = 40_000_000; // 40$/(30 days) at 1 Nym == 1$ + +// TODO: is there a way to get this from the chain +pub const TOTAL_SUPPLY: u128 = 1_000_000_000_000_000; + +pub const DEFAULT_PROFIT_MARGIN: u8 = 10; diff --git a/contracts/mixnet/.gitignore b/contracts/mixnet/.gitignore new file mode 100644 index 0000000000..1761c01d05 --- /dev/null +++ b/contracts/mixnet/.gitignore @@ -0,0 +1 @@ +.envrc \ No newline at end of file diff --git a/contracts/mixnet/Cargo.lock b/contracts/mixnet/Cargo.lock index 90c5f59a0f..0f7f85618a 100644 --- a/contracts/mixnet/Cargo.lock +++ b/contracts/mixnet/Cargo.lock @@ -23,9 +23,9 @@ dependencies = [ [[package]] name = "ast_node" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93f52ce8fac3d0e6720a92b0576d737c01b1b5db4dd786e962e5925f00bf755" +checksum = "e96d5444b02f3080edac8a144f6baf29b2fb6ff589ad4311559731a7c7529381" dependencies = [ "darling", "pmutil", @@ -41,12 +41,24 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +[[package]] +name = "az" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d6dff4a1892b54d70af377bf7a17064192e822865791d812957f21e3108c325" + [[package]] name = "base64" 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" @@ -79,9 +91,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.7.1" +version = "3.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9df67f7bf9ef8498769f994239c45613ef0c5899415fb58e9add412d2c1a538" +checksum = "8f1e260c3a9040a7c19a12468758f4c16f31a81a1fe087482be9570ec864bb6c" [[package]] name = "byte-tools" @@ -89,6 +101,12 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" +[[package]] +name = "bytemuck" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72957246c41db82b8ef88a5486143830adeb8227ef9837740bdec67724cf2c5b" + [[package]] name = "byteorder" version = "1.4.3" @@ -121,9 +139,9 @@ dependencies = [ [[package]] name = "const-oid" -version = "0.6.0" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c32f031ea41b4291d695026c023b95d59db2d8a2c7640800ed56bc8f510f22" +checksum = "9d6f2aa4d0537bcc1c74df8755072bd31c1ef1a3a1b85a68e8404a8c353b7b8b" [[package]] name = "cosmwasm-crypto" @@ -147,9 +165,9 @@ dependencies = [ [[package]] name = "cosmwasm-schema" -version = "0.14.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6a9a6a4ca3b4d7b56f943312a65857cdfc84424f9c2773889f4cd17f36fba61" +checksum = "04159eec9b583671db7923ff2b979736dfb8f0152347cab9fd02373c22e1a870" dependencies = [ "schemars", "serde_json", @@ -181,9 +199,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.1.4" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed00c67cb5d0a7d64a44f6ad2668db7e7530311dd53ea79bcd4fb022c64911c8" +checksum = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469" dependencies = [ "libc", ] @@ -196,9 +214,9 @@ checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" [[package]] name = "crypto-bigint" -version = "0.2.2" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b32a398eb1ccfbe7e4f452bc749c44d38dd732e9a253f19da224c416f00ee7f4" +checksum = "f83bd3bb4314701c568e340cd8cf78c975aa0ca79e03d3f6d1677d5b0c9c0c03" dependencies = [ "generic-array 0.14.4", "rand_core 0.6.3", @@ -218,9 +236,9 @@ dependencies = [ [[package]] name = "curve25519-dalek" -version = "3.1.0" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "639891fde0dbea823fc3d798a0fdf9d2f9440a42d64a78ab3488b0ca025117b3" +checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61" dependencies = [ "byteorder", "digest 0.9.0", @@ -266,9 +284,9 @@ dependencies = [ [[package]] name = "der" -version = "0.4.0" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f215f706081a44cb702c71c39a52c05da637822e9c1645a50b7202689e982d" +checksum = "28e98c534e9c8a0483aa01d6f6913bc063de254311bd267c9cf535e9b70e15b2" dependencies = [ "const-oid", ] @@ -338,9 +356,9 @@ checksum = "ee2626afccd7561a06cf1367e2950c4718ea04565e20fb5029b6c7d8ad09abcf" [[package]] name = "ecdsa" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "713c32426287891008edb98f8b5c6abb2130aa043c93a818728fcda78606f274" +checksum = "43ee23aa5b4f68c7a092b5c3beb25f50c406adc75e2363634f242f28ab255372" dependencies = [ "der", "elliptic-curve", @@ -370,9 +388,9 @@ checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" [[package]] name = "elliptic-curve" -version = "0.10.4" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83e5c176479da93a0983f0a6fdc3c1b8e7d5be0d7fe3fe05a99f15b96582b9a8" +checksum = "beca177dcb8eb540133e7680baff45e7cc4d93bf22002676cec549f82343721b" dependencies = [ "crypto-bigint", "ff", @@ -404,14 +422,26 @@ checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" [[package]] name = "ff" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63eec06c61e487eecf0f7e6e6372e596a81922c28d33e645d6983ca6493a1af0" +checksum = "d0f40b2dcd8bc322217a5f6559ae5f9e9d1de202a2ecee2e9eafcbece7562a4f" dependencies = [ "rand_core 0.6.3", "subtle", ] +[[package]] +name = "fixed" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d333a26ec13a023c6dff4b7584de4d323cfee2e508f5dd2bbee6669e4f7efdf" +dependencies = [ + "az", + "bytemuck", + "half", + "typenum", +] + [[package]] name = "fnv" version = "1.0.7" @@ -501,6 +531,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "half" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" + [[package]] name = "handlebars" version = "3.5.5" @@ -564,6 +600,15 @@ 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" @@ -579,15 +624,15 @@ dependencies = [ [[package]] name = "itoa" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" [[package]] name = "k256" -version = "0.9.5" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008b0281ca8032567c9711cd48631781c15228301860a39b32deb28d63125e46" +checksum = "903ae2481bcdfdb7b68e0a9baa4b7c9aff600b9ae2e8e5bb5833b8c91ab851ea" dependencies = [ "cfg-if 1.0.0", "ecdsa", @@ -603,9 +648,18 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.100" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1fa8cddc8fbbee11227ef194b5317ed014b8acbf15139bd716a18ad3fe99ec5" +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" @@ -624,9 +678,9 @@ checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" [[package]] name = "matches" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" +checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" [[package]] name = "memchr" @@ -638,10 +692,15 @@ checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" name = "mixnet-contract" version = "0.1.0" dependencies = [ + "az", "cosmwasm-std", + "fixed", + "log", + "network-defaults", "schemars", "serde", "serde_repr", + "thiserror", "ts-rs", ] @@ -653,6 +712,7 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cosmwasm-storage", + "fixed", "mixnet-contract", "schemars", "serde", @@ -732,6 +792,31 @@ 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" @@ -802,9 +887,9 @@ dependencies = [ [[package]] name = "pkcs8" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbee84ed13e44dd82689fa18348a49934fa79cc774a344c42fc9b301c71b140a" +checksum = "ee3ef9b64d26bad0536099c816c6734379e45bbd5f14798def6809e5cc350447" dependencies = [ "der", "spki", @@ -823,9 +908,9 @@ dependencies = [ [[package]] name = "ppv-lite86" -version = "0.2.10" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" +checksum = "ed0cfbc8191465bed66e1718596ee0b0b35d5ee1f41c5df2189d0fe8bde535ba" [[package]] name = "precomputed-hash" @@ -835,9 +920,9 @@ checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" [[package]] name = "proc-macro2" -version = "1.0.24" +version = "1.0.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" +checksum = "ba508cc11742c0dc5c1659771673afbab7a0efab23aa17e854cbab0837ed0b43" dependencies = [ "unicode-xid", ] @@ -850,9 +935,9 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quote" -version = "1.0.8" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "991431c3519a3f36861882da93630ce66b52918dcf1b8e2fd66b397fc96f28df" +checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" dependencies = [ "proc-macro2", ] @@ -917,6 +1002,15 @@ 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" @@ -942,9 +1036,9 @@ checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" [[package]] name = "schemars" -version = "0.8.3" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc6ab463ae35acccb5cba66c0084c985257b797d288b6050cc2f6ac1b266cb78" +checksum = "d7a48d098c2a7fdf5740b19deb1181b4fb8a9e68e03ae517c14cde04b5725409" dependencies = [ "dyn-clone", "schemars_derive", @@ -954,9 +1048,9 @@ dependencies = [ [[package]] name = "schemars_derive" -version = "0.8.3" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "902fdfbcf871ae8f653bddf4b2c05905ddaabc08f69d32a915787e3be0d31356" +checksum = "4a9ea2a613fe4cd7118b2bb101a25d8ae6192e1975179b67b2f17afd11e70ac8" dependencies = [ "proc-macro2", "quote", @@ -971,10 +1065,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" [[package]] -name = "serde" -version = "1.0.122" +name = "scopeguard" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "974ef1bd2ad8a507599b336595454081ff68a9599b4890af7643c0c0ed73a62c" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "serde" +version = "1.0.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913" dependencies = [ "serde_derive", ] @@ -990,9 +1090,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.122" +version = "1.0.130" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8dee1f300f838c8ac340ecb0112b3ac472464fa67e87292bdb3dfc9c49128e17" +checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b" dependencies = [ "proc-macro2", "quote", @@ -1012,9 +1112,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.61" +version = "1.0.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fceb2595057b6891a4ee808f70054bd2d12f0e97f1cbb78689b59f676df325a" +checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8" dependencies = [ "itoa", "ryu", @@ -1046,9 +1146,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.9.5" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b362ae5752fd2137731f9fa25fd4d9058af34666ca1966fb969119cc35719f12" +checksum = "b69f9a4c9740d74c5baa3fd2e547f9525fa8088a8a958e0ca2409a514e33f5fa" dependencies = [ "block-buffer 0.9.0", "cfg-if 1.0.0", @@ -1059,9 +1159,9 @@ dependencies = [ [[package]] name = "signature" -version = "1.3.1" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c19772be3c4dd2ceaacf03cb41d5885f2a02c4d8804884918e3a258480803335" +checksum = "f2807892cfa58e081aa1f1111391c7a0649d4fa127a4ffbe34bcbfb35a1171a4" dependencies = [ "digest 0.9.0", "rand_core 0.6.3", @@ -1075,15 +1175,15 @@ checksum = "533494a8f9b724d33625ab53c6c4800f7cc445895924a8ef649222dcb76e938b" [[package]] name = "smallvec" -version = "1.6.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" +checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309" [[package]] name = "spki" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "987637c5ae6b3121aba9d513f869bd2bff11c4cc086c22473befd6649c0bd521" +checksum = "5c01a0c15da1b0b0e1494112e7af814a678fec9bd157881b49beac661e9b6f32" dependencies = [ "der", ] @@ -1102,12 +1202,13 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "string_cache" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ddb1139b5353f96e429e1a5e19fbaf663bddedaa06d1dbd49f82e352601209a" +checksum = "923f0f39b6267d37d23ce71ae7235602134b250ace715dd2c90421998ddac0c6" dependencies = [ "lazy_static", "new_debug_unreachable", + "parking_lot", "phf_shared", "precomputed-hash", "serde", @@ -1146,15 +1247,15 @@ checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" [[package]] name = "subtle" -version = "2.4.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e81da0851ada1f3e9d4312c704aa4f8806f0f9d69faaf8df2f3464b4a9437c2" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "swc_atoms" -version = "0.2.7" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "837a3ef86c2817228e733b6f173c821fd76f9eb21a0bc9001a826be48b00b4e7" +checksum = "9f5229fe227ff0060e13baa386d6e368797700eab909523f730008d191ee53ae" dependencies = [ "string_cache", "string_cache_codegen", @@ -1254,9 +1355,9 @@ dependencies = [ [[package]] name = "swc_macros_common" -version = "0.3.3" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08ed2e930f5a1a4071fe62c90fd3a296f6030e5d94bfe13993244423caf59a78" +checksum = "bf7c68e78ffbcba3d38abe6d0b76a0e1a37888b5c9301db3426537207090ada3" dependencies = [ "pmutil", "proc-macro2", @@ -1266,9 +1367,9 @@ dependencies = [ [[package]] name = "swc_visit" -version = "0.2.6" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a423caa0b4585118164dbad8f1ad52b592a9a9370b25decc4d84c6b4309132c0" +checksum = "f8511a4788ab29daf00bee23e425aac92c9be4eec74c98fec4a45d0e710be695" dependencies = [ "either", "swc_visit_macros", @@ -1290,9 +1391,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.65" +version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3a1d708c221c5a612956ef9f75b37e454e88d1f7b899fbd3a18d4252012d663" +checksum = "f2afee18b8beb5a596ecb4a2dce128c719b4ba399d34126b9e4396e3f9860966" dependencies = [ "proc-macro2", "quote", @@ -1301,18 +1402,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.23" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76cc616c6abf8c8928e2fdcc0dbfab37175edd8fb49a4641066ad1364fdab146" +checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.23" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9be73a2caec27583d0046ef3796c3794f868a5bc813db689eed00c7631275cd1" +checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" dependencies = [ "proc-macro2", "quote", @@ -1321,9 +1422,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.1" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a776787d9c5d455bec3db044586ccdd8a9c74d5da5dc319fb80f3db08808fe6" +checksum = "99beeb0daeac2bd1e86ac2c21caddecb244b39a093594da1a661ec2060c7aedd" dependencies = [ "libc", "time-macros", @@ -1331,15 +1432,15 @@ dependencies = [ [[package]] name = "time-macros" -version = "0.2.1" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04a153416002296880a3b51329a0e3df31c779c53ec827993e865ce427982843" +checksum = "25eb0ca3468fc0acc11828786797f6ef9aa1555e4a211a60d64cc8e4d1be47d6" [[package]] name = "tinyvec" -version = "1.3.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "848a1e1181b9f6753b5e96a092749e29b11d19ede67dfbbd6c7dc7e0f49b5338" +checksum = "f83b2a3d4d9091d0abd7eba4dc2710b1718583bd4d8992e2190720ea38f391f7" dependencies = [ "tinyvec_macros", ] @@ -1383,9 +1484,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.13.0" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06" +checksum = "b63708a265f51345575b27fe43f9500ad611579e764c79edbc2037b1121959ec" [[package]] name = "ucd-trie" @@ -1395,9 +1496,9 @@ checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" [[package]] name = "uint" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e11fe9a9348741cf134085ad57c249508345fe16411b3d7fb4ff2da2f1d6382e" +checksum = "6470ab50f482bde894a037a57064480a246dbfdd5960bd65a44824693f08da5f" dependencies = [ "byteorder", "crunchy", @@ -1407,12 +1508,9 @@ dependencies = [ [[package]] name = "unicode-bidi" -version = "0.3.5" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eeb8be209bb1c96b7c177c7420d26e04eccacb0eeae6b980e35fcb74678107e0" -dependencies = [ - "matches", -] +checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f" [[package]] name = "unicode-normalization" @@ -1431,9 +1529,9 @@ checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" [[package]] name = "unicode-xid" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" [[package]] name = "url" @@ -1466,7 +1564,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" [[package]] -name = "zeroize" -version = "1.3.0" +name = "winapi" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4756f7db3f7b5574938c3eb1c117038b8e07f95ee6718c0efad4ac21508f1efd" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf68b08513768deaa790264a7fac27a58cbf2705cfcdc9448362229217d7e970" diff --git a/contracts/mixnet/Cargo.toml b/contracts/mixnet/Cargo.toml index 13bd3ed584..0d803ecae1 100644 --- a/contracts/mixnet/Cargo.toml +++ b/contracts/mixnet/Cargo.toml @@ -49,3 +49,4 @@ thiserror = { version = "1.0.23" } [dev-dependencies] cosmwasm-schema = { version = "0.14.0" } +fixed = "1.1" diff --git a/contracts/mixnet/src/contract.rs b/contracts/mixnet/src/contract.rs index 4dc9dac8e4..eb360bd686 100644 --- a/contracts/mixnet/src/contract.rs +++ b/contracts/mixnet/src/contract.rs @@ -1,6 +1,8 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use std::u128; + use crate::helpers::calculate_epoch_reward_rate; use crate::state::State; use crate::storage::{config, layer_distribution}; @@ -26,6 +28,13 @@ pub const INITIAL_MIXNODE_DELEGATION_REWARD_RATE: u64 = 110; pub const INITIAL_MIXNODE_ACTIVE_SET_SIZE: u32 = 100; +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 +pub const DEFAULT_COST_PER_EPOCH: u32 = 40_000_000; + fn default_initial_state(owner: Addr) -> 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); @@ -94,6 +103,9 @@ pub fn execute( ExecuteMsg::RewardMixnode { identity, uptime } => { transactions::try_reward_mixnode(deps, env, info, identity, uptime) } + ExecuteMsg::RewardMixnodeV2 { identity, params } => { + transactions::try_reward_mixnode_v2(deps, env, info, identity, params) + } ExecuteMsg::DelegateToMixnode { mix_identity } => { transactions::try_delegate_to_mixnode(deps, env, info, mix_identity) } @@ -151,11 +163,14 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> Result to_binary(&queries::query_reward_pool(deps)), + QueryMsg::GetCirculatingSupply {} => to_binary(&queries::query_circulating_supply(deps)), + QueryMsg::GetEpochRewardPercent {} => to_binary(&EPOCH_REWARD_PERCENT), + QueryMsg::GetSybilResistancePercent {} => to_binary(&DEFAULT_SYBIL_RESISTANCE_PERCENT), }; Ok(query_res?) } - #[entry_point] pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result { Ok(Default::default()) diff --git a/contracts/mixnet/src/error.rs b/contracts/mixnet/src/error.rs index 3610edee30..6d3db45be5 100644 --- a/contracts/mixnet/src/error.rs +++ b/contracts/mixnet/src/error.rs @@ -77,4 +77,12 @@ 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), } diff --git a/contracts/mixnet/src/helpers.rs b/contracts/mixnet/src/helpers.rs index de5636a0ac..6c86b4034e 100644 --- a/contracts/mixnet/src/helpers.rs +++ b/contracts/mixnet/src/helpers.rs @@ -19,11 +19,11 @@ const DECIMAL_FRACTIONAL: Uint128 = Uint128(1_000_000_000_000_000_000u128); // cosmwasm bucket internal value const NAMESPACE_LENGTH: usize = 2; -fn decimal_to_uint128(value: Decimal) -> Uint128 { +pub fn decimal_to_uint128(value: Decimal) -> Uint128 { value * DECIMAL_FRACTIONAL } -fn uint128_to_decimal(value: Uint128) -> Decimal { +pub fn uint128_to_decimal(value: Uint128) -> Decimal { Decimal::from_ratio(value, DECIMAL_FRACTIONAL) } diff --git a/contracts/mixnet/src/queries.rs b/contracts/mixnet/src/queries.rs index e76d62a93c..7453b2e1f9 100644 --- a/contracts/mixnet/src/queries.rs +++ b/contracts/mixnet/src/queries.rs @@ -4,12 +4,12 @@ use crate::error::ContractError; use crate::helpers::get_all_delegations_paged; use crate::storage::{ - all_mix_delegations_read, gateways_owners_read, gateways_read, mix_delegations_read, - mixnodes_owners_read, mixnodes_read, read_layer_distribution, read_state_params, - reverse_mix_delegations_read, + all_mix_delegations_read, circulating_supply, 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, }; use config::defaults::DENOM; -use cosmwasm_std::{coin, Addr, Deps, Order, StdResult}; +use cosmwasm_std::{coin, Addr, Deps, Order, StdResult, Uint128}; use mixnet_contract::{ Delegation, GatewayBond, GatewayOwnershipResponse, IdentityKey, LayerDistribution, MixNodeBond, MixOwnershipResponse, PagedAllDelegationsResponse, PagedGatewayResponse, @@ -91,6 +91,14 @@ pub(crate) fn query_layer_distribution(deps: Deps) -> LayerDistribution { read_layer_distribution(deps.storage) } +pub(crate) fn query_reward_pool(deps: Deps) -> Uint128 { + reward_pool_value(deps.storage) +} + +pub(crate) fn query_circulating_supply(deps: Deps) -> Uint128 { + circulating_supply(deps.storage) +} + /// Adds a 0 byte to terminate the `start_after` value given. This allows CosmWasm /// to get the succeeding key as the start of the next page. // S works for both `String` and `Addr` and that's what we wanted diff --git a/contracts/mixnet/src/storage.rs b/contracts/mixnet/src/storage.rs index 80aaddecf3..c95e909a03 100644 --- a/contracts/mixnet/src/storage.rs +++ b/contracts/mixnet/src/storage.rs @@ -1,14 +1,16 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 - -use crate::queries; +use crate::contract::INITIAL_REWARD_POOL; 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_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, @@ -24,9 +26,10 @@ use serde::Serialize; // singletons const CONFIG_KEY: &[u8] = b"config"; const LAYER_DISTRIBUTION_KEY: &[u8] = b"layers"; +const REWARD_POOL_PREFIX: &[u8] = b"pool"; // buckets -const PREFIX_MIXNODES: &[u8] = b"mn"; +pub const PREFIX_MIXNODES: &[u8] = b"mn"; const PREFIX_MIXNODES_OWNERS: &[u8] = b"mo"; const PREFIX_GATEWAYS: &[u8] = b"gt"; const PREFIX_GATEWAYS_OWNERS: &[u8] = b"go"; @@ -46,6 +49,53 @@ pub fn config_read(storage: &dyn Storage) -> ReadonlySingleton { singleton_read(storage, CONFIG_KEY) } +fn reward_pool(storage: &dyn Storage) -> ReadonlySingleton { + singleton_read(storage, REWARD_POOL_PREFIX) +} + +pub fn mut_reward_pool(storage: &mut dyn Storage) -> Singleton { + singleton(storage, REWARD_POOL_PREFIX) +} + +pub fn reward_pool_value(storage: &dyn Storage) -> Uint128 { + match reward_pool(storage).load() { + Ok(value) => value, + Err(_e) => Uint128(INITIAL_REWARD_POOL), + } +} + +#[allow(dead_code)] +pub fn incr_reward_pool( + amount: Uint128, + storage: &mut dyn Storage, +) -> Result { + let stake = reward_pool_value(storage).saturating_add(amount); + mut_reward_pool(storage).save(&stake)?; + Ok(stake) +} + +pub fn decr_reward_pool( + amount: Uint128, + storage: &mut dyn Storage, +) -> Result { + let stake = match reward_pool_value(storage).checked_sub(amount) { + Ok(stake) => stake, + Err(_e) => { + return Err(ContractError::OutOfFunds { + to_remove: amount.u128(), + reward_pool: reward_pool_value(storage).u128(), + }) + } + }; + mut_reward_pool(storage).save(&stake)?; + Ok(stake) +} + +pub fn circulating_supply(storage: &dyn Storage) -> Uint128 { + let reward_pool = reward_pool_value(storage).u128(); + Uint128(TOTAL_SUPPLY - reward_pool) +} + pub(crate) fn read_state_params(storage: &dyn Storage) -> StateParams { // note: In any other case, I wouldn't have attempted to unwrap this result, but in here // if we fail to load the stored state we would already be in the undefined behaviour land, @@ -196,6 +246,55 @@ pub(crate) fn increase_mix_delegated_stakes( 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( @@ -345,6 +444,7 @@ mod tests { identity_key: node_identity.clone(), ..mix_node_fixture() }, + profit_margin_percent: Some(10), }; mixnodes(&mut storage) diff --git a/contracts/mixnet/src/support/tests.rs b/contracts/mixnet/src/support/tests.rs index 70b761e0fd..0b36814fb7 100644 --- a/contracts/mixnet/src/support/tests.rs +++ b/contracts/mixnet/src/support/tests.rs @@ -133,6 +133,7 @@ pub mod helpers { Layer::One, 12_345, mix_node, + None, ) } diff --git a/contracts/mixnet/src/transactions.rs b/contracts/mixnet/src/transactions.rs index 8173243bbc..48254473a6 100644 --- a/contracts/mixnet/src/transactions.rs +++ b/contracts/mixnet/src/transactions.rs @@ -1,6 +1,5 @@ // 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::queries; @@ -10,6 +9,7 @@ use cosmwasm_std::{ attr, coins, BankMsg, Coin, Decimal, 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, }; @@ -99,6 +99,7 @@ pub(crate) fn try_add_mixnode( layer, env.block.height, mix_node, + None, ); // this might potentially require more gas if a significant number of delegations was there @@ -416,6 +417,65 @@ pub(crate) fn try_reward_mixnode( }) } +pub(crate) fn try_reward_mixnode_v2( + deps: DepsMut, + env: Env, + info: MessageInfo, + mix_identity: IdentityKey, + params: NodeRewardParams, +) -> Result { + let state = config_read(deps.storage).load().unwrap(); + + // check if this is executed by the monitor, if not reject the transaction + if info.sender != state.network_monitor_address { + return Err(ContractError::Unauthorized); + } + + // 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)?; + } + + Ok(Response { + submessages: vec![], + messages: vec![], + attributes: vec![ + attr("bond increase", reward_result.reward()), + attr("total delegation increase", total_delegation_reward), + ], + data: None, + }) +} fn validate_delegation_stake(delegation: &[Coin]) -> Result<(), ContractError> { // check if anything was put as delegation if delegation.is_empty() { @@ -459,8 +519,10 @@ pub(crate) fn try_delegate_to_mixnode( } }; + let amount = info.funds[0].amount; + // update total_delegation of this node - current_bond.total_delegation.amount += info.funds[0].amount; + current_bond.total_delegation.amount += amount; mixnodes_bucket.save(mix_identity.as_bytes(), ¤t_bond)?; let mut delegation_bucket = mix_delegations(deps.storage, &mix_identity); @@ -468,8 +530,8 @@ pub(crate) fn try_delegate_to_mixnode( // write the delegation let new_amount = match delegation_bucket.may_load(sender_bytes)? { - Some(existing_delegation) => existing_delegation.amount + info.funds[0].amount, - None => info.funds[0].amount, + Some(existing_delegation) => existing_delegation.amount + amount, + None => amount, }; // the block height is reset, if it existed let new_delegation = RawDelegationData::new(new_amount, env.block.height); @@ -532,8 +594,9 @@ pub(crate) fn try_remove_delegation_from_mixnode( pub mod tests { use super::*; use crate::contract::{ - execute, query, INITIAL_DEFAULT_EPOCH_LENGTH, INITIAL_GATEWAY_BOND, INITIAL_MIXNODE_BOND, - INITIAL_MIXNODE_BOND_REWARD_RATE, INITIAL_MIXNODE_DELEGATION_REWARD_RATE, + 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; @@ -1506,6 +1569,7 @@ pub mod tests { identity_key: node_identity.clone(), ..mix_node_fixture() }, + profit_margin_percent: Some(10), }; mixnodes(deps.as_mut().storage) @@ -1605,6 +1669,7 @@ pub mod tests { identity_key: node_identity.clone(), ..mix_node_fixture() }, + profit_margin_percent: Some(10), }; mixnodes(deps.as_mut().storage) @@ -2654,4 +2719,128 @@ 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 network_monitor_address = current_state.network_monitor_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(network_monitor_address.as_ref(), &[]); + + 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).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/tauri-wallet/Cargo.lock b/tauri-wallet/Cargo.lock index 869006a638..af3408d876 100644 --- a/tauri-wallet/Cargo.lock +++ b/tauri-wallet/Cargo.lock @@ -130,6 +130,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +[[package]] +name = "az" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d6dff4a1892b54d70af377bf7a17064192e822865791d812957f21e3108c325" + [[package]] name = "base64" version = "0.13.0" @@ -288,6 +294,12 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" +[[package]] +name = "bytemuck" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72957246c41db82b8ef88a5486143830adeb8227ef9837740bdec67724cf2c5b" + [[package]] name = "byteorder" version = "1.4.3" @@ -1197,6 +1209,18 @@ dependencies = [ "winapi", ] +[[package]] +name = "fixed" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d333a26ec13a023c6dff4b7584de4d323cfee2e508f5dd2bbee6669e4f7efdf" +dependencies = [ + "az", + "bytemuck", + "half", + "typenum", +] + [[package]] name = "flate2" version = "1.0.21" @@ -1741,6 +1765,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "half" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" + [[package]] name = "handlebars" version = "3.5.5" @@ -2329,10 +2359,15 @@ dependencies = [ name = "mixnet-contract" version = "0.1.0" dependencies = [ + "az", "cosmwasm-std", + "fixed", + "log", + "network-defaults", "schemars", "serde", "serde_repr", + "thiserror", "ts-rs", ] diff --git a/validator-api/Cargo.toml b/validator-api/Cargo.toml index 089c8d2529..72150c1c6e 100644 --- a/validator-api/Cargo.toml +++ b/validator-api/Cargo.toml @@ -40,6 +40,7 @@ getset = "0.1.1" rocket_sync_db_pools = {version = "0.1.0-rc.1", default-features = false} sqlx = { version = "0.5", features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"]} + ## internal config = { path = "../common/config" } crypto = { path="../common/crypto" } @@ -54,6 +55,8 @@ 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/cache/mod.rs b/validator-api/src/cache/mod.rs index 7a1cf9864b..33de1177c2 100644 --- a/validator-api/src/cache/mod.rs +++ b/validator-api/src/cache/mod.rs @@ -75,7 +75,7 @@ impl ValidatorCacheRefresher { { let (mixnodes, gateways) = tokio::try_join!( self.nymd_client.get_mixnodes(), - self.nymd_client.get_gateways() + self.nymd_client.get_gateways(), )?; let state_params = self.nymd_client.get_state_params().await?; @@ -83,7 +83,7 @@ impl ValidatorCacheRefresher { info!( "Updating validator cache. There are {} mixnodes and {} gateways", mixnodes.len(), - gateways.len() + gateways.len(), ); self.cache diff --git a/validator-api/src/nymd_client.rs b/validator-api/src/nymd_client.rs index 38dbd1e488..f4f73a41e4 100644 --- a/validator-api/src/nymd_client.rs +++ b/validator-api/src/nymd_client.rs @@ -93,6 +93,34 @@ impl Client { Ok(time) } + pub(crate) async fn get_reward_pool(&self) -> Result + where + C: CosmWasmClient + Sync, + { + Ok(self.0.read().await.get_reward_pool().await?) + } + + pub(crate) async fn get_circulating_supply(&self) -> Result + where + C: CosmWasmClient + Sync, + { + Ok(self.0.read().await.get_circulating_supply().await?) + } + + pub(crate) async fn get_sybil_resistance_percent(&self) -> Result + where + C: CosmWasmClient + Sync, + { + Ok(self.0.read().await.get_sybil_resistance_percent().await?) + } + + pub(crate) async fn get_epoch_reward_percent(&self) -> Result + where + C: CosmWasmClient + Sync, + { + Ok(self.0.read().await.get_epoch_reward_percent().await?) + } + pub(crate) async fn get_mixnodes(&self) -> Result, ValidatorClientError> where C: CosmWasmClient + Sync, diff --git a/validator-api/src/rewarding/mod.rs b/validator-api/src/rewarding/mod.rs index e1bac6fe73..94dfc7ef3f 100644 --- a/validator-api/src/rewarding/mod.rs +++ b/validator-api/src/rewarding/mod.rs @@ -12,6 +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 std::convert::TryInto; @@ -48,6 +49,19 @@ pub(crate) struct MixnodeToReward { /// 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, +} + +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 + fn params(&self) -> Option { + if cfg!(feature = "tokenomics") { + self.params + } else { + None + } + } } pub(crate) struct FailedMixnodeRewardChunkDetails { @@ -62,9 +76,16 @@ pub(crate) struct FailureData { impl<'a> From<&'a MixnodeToReward> for ExecuteMsg { fn from(node: &MixnodeToReward) -> Self { - ExecuteMsg::RewardMixnode { - identity: node.identity.clone(), - uptime: node.uptime.u8() as u32, + if let Some(params) = node.params() { + ExecuteMsg::RewardMixnodeV2 { + identity: node.identity.clone(), + params, + } + } else { + ExecuteMsg::RewardMixnode { + identity: node.identity.clone(), + uptime: node.uptime.u8() as u32, + } } } } @@ -185,12 +206,13 @@ impl Rewarder { // 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 eligible_nodes = active_mixnodes + let mut eligible_nodes: Vec = active_mixnodes .iter() .filter_map(|mix| { mixnode_delegators @@ -199,11 +221,39 @@ impl Rewarder { identity: mix.identity.clone(), uptime: mix.last_day, total_delegations, + params: None, }) }) .filter(|node| node.uptime.u8() > 0) .collect(); + 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; + + 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( + period_reward_pool, + k.into(), + 0, + circulating_supply, + mix.uptime.u8().into(), + sybil_resistance_percent, + )); + } + } else { + info!("Tokenomics feature is OFF"); + } + Ok(eligible_nodes) }