diff --git a/common/mixnet-contract/src/error.rs b/common/mixnet-contract/src/error.rs index 78caf1a709..89416950df 100644 --- a/common/mixnet-contract/src/error.rs +++ b/common/mixnet-contract/src/error.rs @@ -3,5 +3,5 @@ use thiserror::Error; #[derive(Error, Debug, PartialEq)] pub enum MixnetContractError { #[error("Could not convert ration to f64: {0} / {1}")] - InvalidRatio(u128, u128) -} \ No newline at end of file + InvalidRatio(u128, u128), +} diff --git a/common/mixnet-contract/src/lib.rs b/common/mixnet-contract/src/lib.rs index d6ec018a0b..feb08f41f6 100644 --- a/common/mixnet-contract/src/lib.rs +++ b/common/mixnet-contract/src/lib.rs @@ -2,11 +2,11 @@ // SPDX-License-Identifier: Apache-2.0 mod delegation; +pub mod error; mod gateway; mod mixnode; mod msg; mod types; -pub mod error; pub use cosmwasm_std::{Addr, Coin}; pub use delegation::{ diff --git a/common/mixnet-contract/src/mixnode.rs b/common/mixnet-contract/src/mixnode.rs index 17e6dda03c..ac79bced83 100644 --- a/common/mixnet-contract/src/mixnode.rs +++ b/common/mixnet-contract/src/mixnode.rs @@ -15,6 +15,8 @@ use ts_rs::TS; use crate::current_block_height; +const DEFAULT_PROFIT_MARGIN: f64 = 0.1; + #[derive(Clone, Debug, Deserialize, PartialEq, PartialOrd, Serialize, JsonSchema, TS)] pub struct MixNode { pub host: String, @@ -47,6 +49,7 @@ pub struct MixNodeBond { #[serde(default = "current_block_height")] pub block_height: u64, pub mix_node: MixNode, + pub profit_margin: Option, } impl MixNodeBond { @@ -56,6 +59,7 @@ impl MixNodeBond { layer: Layer, block_height: u64, mix_node: MixNode, + profit_margin: Option, ) -> Self { MixNodeBond { total_delegation: coin(0, &bond_amount.denom), @@ -64,6 +68,15 @@ impl MixNodeBond { layer, block_height, mix_node, + profit_margin, + } + } + + pub fn profit_margin(&self) -> f64 { + if let Some(margin) = self.profit_margin { + margin + } else { + DEFAULT_PROFIT_MARGIN } } @@ -88,18 +101,21 @@ impl MixNodeBond { } pub fn bond_to_total_stake_ratio(&self, total_stake: Uint128) -> Ratio { - Ratio::new( - self.bond_amount().amount.u128(), - total_stake.u128(), - ) + Ratio::new(self.bond_amount().amount.u128(), total_stake.u128()) } - pub fn bond_to_total_stake_f64(&self, total_stake: Uint128) -> Result { + pub fn bond_to_total_stake_f64( + &self, + total_stake: Uint128, + ) -> Result { let ratio = self.bond_to_total_stake_ratio(total_stake); if let Some(f) = ratio.to_f64() { Ok(f) } else { - Err(MixnetContractError::InvalidRatio(*ratio.numer(), *ratio.denom())) + Err(MixnetContractError::InvalidRatio( + *ratio.numer(), + *ratio.denom(), + )) } } @@ -110,12 +126,18 @@ impl MixNodeBond { ) } - pub fn stake_to_total_stake_f64(&self, total_stake: Uint128) -> Result { + pub fn stake_to_total_stake_f64( + &self, + total_stake: Uint128, + ) -> Result { let ratio = self.stake_to_total_stake_ratio(total_stake); if let Some(f) = ratio.to_f64() { Ok(f) } else { - Err(MixnetContractError::InvalidRatio(*ratio.numer(), *ratio.denom())) + Err(MixnetContractError::InvalidRatio( + *ratio.numer(), + *ratio.denom(), + )) } } } @@ -249,6 +271,7 @@ mod tests { layer: Layer::One, block_height: 100, mix_node: mixnode_fixture(), + profit_margin: None, }; let mix2 = MixNodeBond { @@ -258,6 +281,7 @@ mod tests { layer: Layer::One, block_height: 120, mix_node: mixnode_fixture(), + profit_margin: None, }; let mix3 = MixNodeBond { @@ -267,6 +291,7 @@ mod tests { layer: Layer::One, block_height: 120, mix_node: mixnode_fixture(), + profit_margin: None, }; let mix4 = MixNodeBond { @@ -276,6 +301,7 @@ mod tests { layer: Layer::One, block_height: 120, mix_node: mixnode_fixture(), + profit_margin: None, }; let mix5 = MixNodeBond { @@ -285,6 +311,7 @@ mod tests { layer: Layer::One, block_height: 120, mix_node: mixnode_fixture(), + profit_margin: None, }; // summary: diff --git a/common/mixnet-contract/src/msg.rs b/common/mixnet-contract/src/msg.rs index 1072545375..ed21a80dcf 100644 --- a/common/mixnet-contract/src/msg.rs +++ b/common/mixnet-contract/src/msg.rs @@ -49,7 +49,7 @@ pub enum ExecuteMsg { identity: IdentityKey, // percentage value in range 0-100 uptime: u32, - performance: f64 + performance: f64, }, RewardGateway { diff --git a/contracts/mixnet/src/contract.rs b/contracts/mixnet/src/contract.rs index 65784a60cb..e92ac8f906 100644 --- a/contracts/mixnet/src/contract.rs +++ b/contracts/mixnet/src/contract.rs @@ -30,11 +30,10 @@ pub const INITIAL_MIXNODE_ACTIVE_SET_SIZE: u32 = 100; pub const INITIAL_GATEWAY_ACTIVE_SET_SIZE: u32 = 20; // This is totally made up, lets set the pool to billion nyms, so million billion micro nyms -pub const INITIAL_INFLATION_POOL: u64 = 1_000_000_000_000_000; +pub const INITIAL_INFLATION_POOL: u128 = 1_000_000_000_000_000; // Sybil attack resistance parameter pub const ALPHA: f64 = 0.3; // 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_PROFIT_MARGIN: f64 = 0.1; // Assume operators want a 10 % profit pub const DEFAULT_COST_PER_EPOCH: u32 = 40_000_000; fn default_initial_state(owner: Addr) -> State { @@ -118,9 +117,11 @@ pub fn execute( ExecuteMsg::RewardMixnode { identity, uptime } => { transactions::try_reward_mixnode(deps, env, info, identity, uptime) } - ExecuteMsg::RewardMixnodeV2 { identity, uptime, performance } => { - transactions::try_reward_mixnode_v2(deps, env, info, identity, uptime, performance) - } + ExecuteMsg::RewardMixnodeV2 { + identity, + uptime, + performance, + } => transactions::try_reward_mixnode_v2(deps, env, info, identity, uptime, performance), ExecuteMsg::RewardGateway { identity, uptime } => { transactions::try_reward_gateway(deps, env, info, identity, uptime) } @@ -226,7 +227,7 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> Result Result { - todo!("Calculate initial total mix and gateway stake after initial deployment"); + todo!("Calculate initial total mix and gateway stake after initial deployment. Update mixnode model with profit sharing"); #[allow(unreachable_code)] Ok(Default::default()) diff --git a/contracts/mixnet/src/storage.rs b/contracts/mixnet/src/storage.rs index 92cab28d5a..954a195d7b 100644 --- a/contracts/mixnet/src/storage.rs +++ b/contracts/mixnet/src/storage.rs @@ -1,7 +1,9 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::contract::{DEFAULT_COST_PER_EPOCH, INITIAL_INFLATION_POOL}; +use crate::contract::{ + DEFAULT_COST_PER_EPOCH, INITIAL_INFLATION_POOL, INITIAL_MIXNODE_ACTIVE_SET_SIZE, +}; use crate::state::State; use crate::transactions::MINIMUM_BLOCK_AGE_FOR_REWARDING; use crate::{error::ContractError, queries}; @@ -14,6 +16,8 @@ use mixnet_contract::{ Addr, GatewayBond, IdentityKey, IdentityKeyRef, Layer, LayerDistribution, MixNodeBond, RawDelegationData, StateParams, }; +use num::rational::Ratio; +use num::ToPrimitive; use serde::de::DeserializeOwned; use serde::Serialize; @@ -61,11 +65,11 @@ pub fn mut_total_mix_stake(storage: &mut dyn Storage) -> Singleton { singleton(storage, TOTAL_MIX_STAKE_KEY) } -fn inflation_pool(storage: &dyn Storage) -> ReadonlySingleton { +fn inflation_pool(storage: &dyn Storage) -> ReadonlySingleton { singleton_read(storage, INFLATION_POOL_PREFIX) } -pub fn mut_inflation_pool(storage: &mut dyn Storage) -> Singleton { +pub fn mut_inflation_pool(storage: &mut dyn Storage) -> Singleton { singleton(storage, INFLATION_POOL_PREFIX) } @@ -91,7 +95,7 @@ pub fn total_gateway_stake_value(storage: &dyn Storage) -> Uint128 { } } -pub fn inflation_pool_value(storage: &dyn Storage) -> u64 { +pub fn inflation_pool_value(storage: &dyn Storage) -> u128 { match inflation_pool(storage).load() { Ok(value) => value, Err(_e) => INITIAL_INFLATION_POOL, @@ -134,13 +138,14 @@ pub fn decr_total_gateway_stake( Ok(stake) } -pub fn incr_inflation_pool(amount: u64, storage: &mut dyn Storage) -> Result { +#[allow(dead_code)] +pub fn incr_inflation_pool(amount: u128, storage: &mut dyn Storage) -> Result { let stake = inflation_pool_value(storage) + amount; mut_inflation_pool(storage).save(&stake)?; Ok(stake) } -pub fn decr_inflation_pool(amount: u64, storage: &mut dyn Storage) -> Result { +pub fn decr_inflation_pool(amount: u128, storage: &mut dyn Storage) -> Result { // TODO: This could got to < 0 let stake = inflation_pool_value(storage) - amount; mut_inflation_pool(storage).save(&stake)?; @@ -319,20 +324,20 @@ pub(crate) fn price_for_uptime(uptime: u32) -> f64 { pub(crate) fn increase_mix_delegated_stakes_v2( storage: &mut dyn Storage, - mix_identity: IdentityKeyRef, - sigma: f64, + bond: &MixNodeBond, node_reward: f64, uptime: u32, - profit_margin: f64, reward_blockstamp: u64, -) -> StdResult { +) -> Result { let chunk_size = queries::DELEGATION_PAGE_MAX_LIMIT as usize; + let total_mix_stake = total_gateway_stake_value(storage); + let one_over_k = 1. / INITIAL_MIXNODE_ACTIVE_SET_SIZE as f64; 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) + let delegations_chunk = mix_delegations_read(storage, bond.identity()) .range(chunk_start.as_deref(), None, Order::Ascending) .take(chunk_size) .collect::>>()?; @@ -352,20 +357,40 @@ pub(crate) fn increase_mix_delegated_stakes_v2( .chain(std::iter::once(0u8)) .collect(), ); + let sigma_ratio = if bond.stake_to_total_stake_f64(total_mix_stake)? < one_over_k { + bond.stake_to_total_stake_ratio(total_mix_stake) + } else { + Ratio::new(1, INITIAL_MIXNODE_ACTIVE_SET_SIZE as u128) + }; // 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 = ((1. - profit_margin) + // Getting amount / over sigma by reciprocal fraction multiplication' + // Delegation rewards almost certanly need to be scaled by total_stake + let delegation_over_sigma_ratio = Ratio::new( + delegation.amount.u128() * sigma_ratio.denom(), + total_mix_stake.u128() * sigma_ratio.numer(), + ); + // If we can't resolve the ration we move on, and the delegation does not get rewarded + let delegation_over_sigma_f64 = + if let Some(f) = delegation_over_sigma_ratio.to_f64() { + f + } else { + continue; + }; + let reward = Uint128( + ((1. - bond.profit_margin()) // Need to convert this to ratio multiplication - * (delegation.amount / sigma) + * delegation_over_sigma_f64 * (node_reward * price_for_uptime(uptime))) - .max(0.); + .max(0.) as u128, + ); delegation.amount += reward; total_rewarded += reward; - mix_delegations(storage, mix_identity).save(&delegator_address, &delegation)?; + mix_delegations(storage, bond.identity()).save(&delegator_address, &delegation)?; } } } @@ -630,6 +655,7 @@ mod tests { identity_key: node_identity.clone(), ..mix_node_fixture() }, + profit_margin: None, }; 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 40aefcd3e6..2e3f3bc717 100644 --- a/contracts/mixnet/src/transactions.rs +++ b/contracts/mixnet/src/transactions.rs @@ -1,9 +1,7 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::contract::{ - ALPHA, DEFAULT_PROFIT_MARGIN, INITIAL_MIXNODE_ACTIVE_SET_SIZE, -}; +use crate::contract::{ALPHA, INITIAL_MIXNODE_ACTIVE_SET_SIZE}; use crate::error::ContractError; use crate::helpers::{calculate_epoch_reward_rate, scale_reward_by_uptime, Delegations}; use crate::queries; @@ -16,8 +14,6 @@ use cosmwasm_storage::ReadonlyBucket; use mixnet_contract::{ Gateway, GatewayBond, IdentityKey, Layer, MixNode, MixNodeBond, RawDelegationData, StateParams, }; -use num::rational::Ratio; -use num::ToPrimitive; pub(crate) const OLD_DELEGATIONS_CHUNK_SIZE: usize = 500; pub(crate) const MINIMUM_BLOCK_AGE_FOR_REWARDING: u64 = 17280; @@ -104,6 +100,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 @@ -459,7 +456,6 @@ pub(crate) fn try_reward_mixnode( }) } - pub(crate) fn try_reward_mixnode_v2( deps: DepsMut, env: Env, @@ -503,32 +499,32 @@ pub(crate) fn try_reward_mixnode_v2( / (1. + ALPHA); // Omitting the price per packet function now, it follows that base operator reward is the node_reward - let operator_profit = ((DEFAULT_PROFIT_MARGIN - + (1. - DEFAULT_PROFIT_MARGIN) * (lambda / sigma)) + let operator_profit = ((current_bond.profit_margin() + + (1. - current_bond.profit_margin()) * (lambda / sigma)) * (node_reward - price_for_uptime(uptime))) .max(0.); let operator_base_reward = node_reward.min(price_for_uptime(uptime)); - let total_operator_reward = (operator_base_reward + operator_profit) as u128; + let total_operator_reward = Uint128((operator_base_reward + operator_profit) as u128); let total_delegation_reward = increase_mix_delegated_stakes_v2( deps.storage, - &mix_identity, - sigma, + ¤t_bond, node_reward, uptime, - DEFAULT_PROFIT_MARGIN, - env.block.height + env.block.height, )?; // 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 { - node_reward = current_bond.bond_amount.amount * bond_scaled_reward_rate; - current_bond.bond_amount.amount += node_reward; + current_bond.bond_amount.amount += total_operator_reward; current_bond.total_delegation.amount += total_delegation_reward; mixnodes(deps.storage).save(mix_identity.as_bytes(), ¤t_bond)?; } + decr_inflation_pool(total_operator_reward.u128(), deps.storage)?; + decr_inflation_pool(total_delegation_reward.u128(), deps.storage)?; + Ok(Response { submessages: vec![], messages: vec![], @@ -1918,6 +1914,7 @@ pub mod tests { identity_key: node_identity.clone(), ..mix_node_fixture() }, + profit_margin: None, }; mixnodes(deps.as_mut().storage) @@ -2017,6 +2014,7 @@ pub mod tests { identity_key: node_identity.clone(), ..mix_node_fixture() }, + profit_margin: None, }; mixnodes(deps.as_mut().storage) diff --git a/tokenomics-py/src/lib.rs b/tokenomics-py/src/lib.rs index c8e466b2a8..05f23d7c85 100644 --- a/tokenomics-py/src/lib.rs +++ b/tokenomics-py/src/lib.rs @@ -38,7 +38,6 @@ fn compute_mix_rewards(mix: &MixNodeBond) { let work_share = one_over_k; let omega_k = work_share * k; // TODO: Use Coin struct from the Tauri wallet, this must be in the Minor denom it will be much easier then - } // TODO: diff --git a/validator-api/src/rewarding/mod.rs b/validator-api/src/rewarding/mod.rs index 502b9a35d5..a9b86eab26 100644 --- a/validator-api/src/rewarding/mod.rs +++ b/validator-api/src/rewarding/mod.rs @@ -61,7 +61,7 @@ pub(crate) struct MixnodeToReward { impl MixnodeToReward { /// Somewhat clumsy way of feature gatting tokenomics payments. In a tokenomics scenario this will never be None at reward time. We levarage that to Into a different ExecuteMsg variant fn performance(&self) -> Option { - if cfg!(featrure = "tokenomics"){ + if cfg!(featrure = "tokenomics") { self.performance } else { None @@ -100,7 +100,7 @@ impl<'a> From<&'a MixnodeToReward> for ExecuteMsg { ExecuteMsg::RewardMixnodeV2 { identity: node.identity.clone(), uptime: node.uptime.u8() as u32, - performance: perf + performance: perf, } } else { ExecuteMsg::RewardMixnode { @@ -108,7 +108,6 @@ impl<'a> From<&'a MixnodeToReward> for ExecuteMsg { uptime: node.uptime.u8() as u32, } } - } } @@ -321,7 +320,6 @@ impl Rewarder { for mix in eligible_nodes.iter_mut() { mix.performance = Some(mix.uptime.u8() as f64 / total_epoch_uptime); } - } Ok(eligible_nodes)