From 22a4b03ce6d0c4ffa35162cf77857cb7788593d6 Mon Sep 17 00:00:00 2001 From: Drazen Urch Date: Thu, 23 Sep 2021 15:58:42 +0200 Subject: [PATCH] Initial analysis --- Cargo.lock | 105 +++++++++++++ Cargo.toml | 1 + .../validator-client/src/client.rs | 14 ++ .../validator-client/src/nymd/mod.rs | 21 +++ common/mixnet-contract/src/gateway.rs | 4 + common/mixnet-contract/src/mixnode.rs | 4 + common/mixnet-contract/src/msg.rs | 2 + contracts/mixnet/src/contract.rs | 6 +- contracts/mixnet/src/error.rs | 3 + contracts/mixnet/src/queries.rs | 12 +- contracts/mixnet/src/storage.rs | 71 ++++++++- contracts/mixnet/src/transactions.rs | 147 +++++++++++++++++- tokenomics-py/Cargo.toml | 20 +++ tokenomics-py/src/lib.rs | 49 ++++++ validator-api/src/cache/mod.rs | 36 ++++- validator-api/src/nymd_client.rs | 14 ++ validator-api/src/rewarding/mod.rs | 3 + 17 files changed, 499 insertions(+), 13 deletions(-) create mode 100644 tokenomics-py/Cargo.toml create mode 100644 tokenomics-py/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 22217f3cb9..6f4440999f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2628,6 +2628,29 @@ dependencies = [ "serde", ] +[[package]] +name = "indoc" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47741a8bc60fb26eb8d6e0238bbb26d8575ff623fdc97b1a2c00c050b9684ed8" +dependencies = [ + "indoc-impl", + "proc-macro-hack", +] + +[[package]] +name = "indoc-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce046d161f000fffde5f432a0d034d0341dc152643b2598ed5bfce44c4f3a8f0" +dependencies = [ + "proc-macro-hack", + "proc-macro2", + "quote", + "syn", + "unindent", +] + [[package]] name = "infer" version = "0.4.0" @@ -3790,6 +3813,25 @@ dependencies = [ "winapi", ] +[[package]] +name = "paste" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45ca20c77d80be666aef2b45486da86238fabe33e38306bd3118fe4af33fa880" +dependencies = [ + "paste-impl", + "proc-macro-hack", +] + +[[package]] +name = "paste-impl" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d95a7db200b97ef370c8e6de0088252f7e0dfff7d047a28528e47456c0fc98b6" +dependencies = [ + "proc-macro-hack", +] + [[package]] name = "pbkdf2" version = "0.8.0" @@ -4230,6 +4272,54 @@ dependencies = [ "url", ] +[[package]] +name = "pyo3" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35100f9347670a566a67aa623369293703322bb9db77d99d7df7313b575ae0c8" +dependencies = [ + "cfg-if 1.0.0", + "indoc", + "libc", + "parking_lot", + "paste", + "pyo3-build-config", + "pyo3-macros", + "unindent", +] + +[[package]] +name = "pyo3-build-config" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d12961738cacbd7f91b7c43bc25cfeeaa2698ad07a04b3be0aa88b950865738f" +dependencies = [ + "once_cell", +] + +[[package]] +name = "pyo3-macros" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0bc5215d704824dfddddc03f93cb572e1155c68b6761c37005e1c288808ea8" +dependencies = [ + "pyo3-macros-backend", + "quote", + "syn", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71623fc593224afaab918aa3afcaf86ed2f43d34f6afde7f3922608f253240df" +dependencies = [ + "proc-macro2", + "pyo3-build-config", + "quote", + "syn", +] + [[package]] name = "quick-error" version = "1.2.3" @@ -6289,6 +6379,15 @@ dependencies = [ "syn", ] +[[package]] +name = "tokenomics-py" +version = "0.1.0" +dependencies = [ + "cosmwasm-std", + "mixnet-contract", + "pyo3", +] + [[package]] name = "tokio" version = "1.12.0" @@ -6619,6 +6718,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" +[[package]] +name = "unindent" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f14ee04d9415b52b3aeab06258a3f07093182b88ba0f9b8d203f211a7a7d41c7" + [[package]] name = "untrusted" version = "0.7.1" diff --git a/Cargo.toml b/Cargo.toml index 02ceec5e95..7ce59518ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,6 +50,7 @@ members = [ "mixnode", "service-providers/network-requester", "validator-api", + "tokenomics-py" ] default-members = [ diff --git a/common/client-libs/validator-client/src/client.rs b/common/client-libs/validator-client/src/client.rs index 48535ba7d8..ee99f0fa9b 100644 --- a/common/client-libs/validator-client/src/client.rs +++ b/common/client-libs/validator-client/src/client.rs @@ -182,6 +182,20 @@ impl Client { Ok(self.nymd.get_state_params().await?) } + pub async fn get_total_mix_stake(&self) -> Result + where + C: CosmWasmClient + Sync, + { + Ok(self.nymd.get_total_mix_stake().await?.u128()) + } + + pub async fn get_total_gateway_stake(&self) -> Result + where + C: CosmWasmClient + Sync, + { + Ok(self.nymd.get_total_gateway_stake().await?.u128()) + } + // 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 2ec670fb6e..7ce478a5d7 100644 --- a/common/client-libs/validator-client/src/nymd/mod.rs +++ b/common/client-libs/validator-client/src/nymd/mod.rs @@ -12,6 +12,7 @@ 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, @@ -212,6 +213,26 @@ impl NymdClient { .await } + pub async fn get_total_mix_stake(&self) -> Result + where + C: CosmWasmClient + Sync, + { + let request = QueryMsg::GetTotalMixStake {}; + self.client + .query_contract_smart(self.contract_address()?, &request) + .await + } + + pub async fn get_total_gateway_stake(&self) -> Result + where + C: CosmWasmClient + Sync, + { + let request = QueryMsg::GetTotalGatewayStake {}; + 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/src/gateway.rs b/common/mixnet-contract/src/gateway.rs index 6318452ecb..19a0a193f7 100644 --- a/common/mixnet-contract/src/gateway.rs +++ b/common/mixnet-contract/src/gateway.rs @@ -59,6 +59,10 @@ impl GatewayBond { pub fn gateway(&self) -> &Gateway { &self.gateway } + + pub fn total_delegation(&self) -> Coin { + self.total_delegation.clone() + } } impl PartialOrd for GatewayBond { diff --git a/common/mixnet-contract/src/mixnode.rs b/common/mixnet-contract/src/mixnode.rs index 53610ab36e..e719137714 100644 --- a/common/mixnet-contract/src/mixnode.rs +++ b/common/mixnet-contract/src/mixnode.rs @@ -79,6 +79,10 @@ impl MixNodeBond { pub fn mix_node(&self) -> &MixNode { &self.mix_node } + + pub fn total_delegation(&self) -> Coin { + self.total_delegation.clone() + } } impl PartialOrd for MixNodeBond { diff --git a/common/mixnet-contract/src/msg.rs b/common/mixnet-contract/src/msg.rs index 15b4193369..2785e32182 100644 --- a/common/mixnet-contract/src/msg.rs +++ b/common/mixnet-contract/src/msg.rs @@ -107,6 +107,8 @@ pub enum QueryMsg { address: Addr, }, LayerDistribution {}, + GetTotalMixStake {}, + GetTotalGatewayStake {}, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] diff --git a/contracts/mixnet/src/contract.rs b/contracts/mixnet/src/contract.rs index 8321e1fc3a..79f739c953 100644 --- a/contracts/mixnet/src/contract.rs +++ b/contracts/mixnet/src/contract.rs @@ -207,13 +207,17 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> Result to_binary(&queries::query_total_mix_stake(deps)), + QueryMsg::GetTotalGatewayStake {} => to_binary(&queries::query_total_gt_stake(deps)), }; Ok(query_res?) } - #[entry_point] pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result { + todo!("Calculate initial total mix and gateway stake after initial deployment"); + + #[allow(unreachable_code)] Ok(Default::default()) } diff --git a/contracts/mixnet/src/error.rs b/contracts/mixnet/src/error.rs index 652533d6d0..344a73e60f 100644 --- a/contracts/mixnet/src/error.rs +++ b/contracts/mixnet/src/error.rs @@ -89,4 +89,7 @@ pub enum ContractError { identity: IdentityKey, address: Addr, }, + + #[error("Overflow error!")] + Overflow(#[from] cosmwasm_std::OverflowError), } diff --git a/contracts/mixnet/src/queries.rs b/contracts/mixnet/src/queries.rs index 0d2c3826aa..bb069a3525 100644 --- a/contracts/mixnet/src/queries.rs +++ b/contracts/mixnet/src/queries.rs @@ -7,10 +7,10 @@ use crate::storage::{ all_gateway_delegations_read, all_mix_delegations_read, gateway_delegations_read, gateways_owners_read, gateways_read, mix_delegations_read, mixnodes_owners_read, mixnodes_read, read_layer_distribution, read_state_params, reverse_gateway_delegations_read, - reverse_mix_delegations_read, + reverse_mix_delegations_read, total_gateway_stake_value, total_mix_stake_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, PagedGatewayDelegationsResponse, @@ -93,6 +93,14 @@ pub(crate) fn query_layer_distribution(deps: Deps) -> LayerDistribution { read_layer_distribution(deps.storage) } +pub(crate) fn query_total_mix_stake(deps: Deps) -> Uint128 { + total_mix_stake_value(deps.storage) +} + +pub(crate) fn query_total_gt_stake(deps: Deps) -> Uint128 { + total_gateway_stake_value(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 21cd2c41da..143927ec16 100644 --- a/contracts/mixnet/src/storage.rs +++ b/contracts/mixnet/src/storage.rs @@ -1,9 +1,9 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::queries; use crate::state::State; use crate::transactions::MINIMUM_BLOCK_AGE_FOR_REWARDING; +use crate::{error::ContractError, queries}; use cosmwasm_std::{Decimal, Order, StdResult, Storage, Uint128}; use cosmwasm_storage::{ bucket, bucket_read, singleton, singleton_read, Bucket, ReadonlyBucket, ReadonlySingleton, @@ -24,6 +24,9 @@ use serde::Serialize; // singletons const CONFIG_KEY: &[u8] = b"config"; const LAYER_DISTRIBUTION_KEY: &[u8] = b"layers"; +// Keeps total amount of stake towards mixnodes and gateways. Removing a bond removes all its delegations from the total, the reverse is true for adding a bond. +const TOTAL_MIX_STAKE_KEY: &[u8] = b"total_mn"; +const TOTAL_GATEWAY_STAKE_KEY: &[u8] = b"total_gt"; // buckets const PREFIX_MIXNODES: &[u8] = b"mn"; @@ -48,6 +51,72 @@ pub fn config_read(storage: &dyn Storage) -> ReadonlySingleton { singleton_read(storage, CONFIG_KEY) } +fn total_mix_stake(storage: &dyn Storage) -> ReadonlySingleton { + singleton_read(storage, TOTAL_MIX_STAKE_KEY) +} + +pub fn mut_total_mix_stake(storage: &mut dyn Storage) -> Singleton { + singleton(storage, TOTAL_MIX_STAKE_KEY) +} + +fn total_gateway_stake(storage: &dyn Storage) -> ReadonlySingleton { + singleton_read(storage, TOTAL_GATEWAY_STAKE_KEY) +} + +pub fn mut_total_gateway_stake(storage: &mut dyn Storage) -> Singleton { + singleton(storage, TOTAL_GATEWAY_STAKE_KEY) +} + +pub fn total_mix_stake_value(storage: &dyn Storage) -> Uint128 { + match total_mix_stake(storage).load() { + Ok(value) => value, + Err(_e) => Uint128(0), + } +} + +pub fn total_gateway_stake_value(storage: &dyn Storage) -> Uint128 { + match total_gateway_stake(storage).load() { + Ok(value) => value, + Err(_e) => Uint128(0), + } +} + +pub fn incr_total_mix_stake( + amount: Uint128, + storage: &mut dyn Storage, +) -> Result { + let stake = total_mix_stake_value(storage).checked_add(amount)?; + mut_total_mix_stake(storage).save(&stake)?; + Ok(stake) +} + +pub fn decr_total_mix_stake( + amount: Uint128, + storage: &mut dyn Storage, +) -> Result { + let stake = total_mix_stake_value(storage).checked_sub(amount)?; + mut_total_mix_stake(storage).save(&stake)?; + Ok(stake) +} + +pub fn incr_total_gateway_stake( + amount: Uint128, + storage: &mut dyn Storage, +) -> Result { + let stake = total_gateway_stake_value(storage).checked_add(amount)?; + mut_total_gateway_stake(storage).save(&stake)?; + Ok(stake) +} + +pub fn decr_total_gateway_stake( + amount: Uint128, + storage: &mut dyn Storage, +) -> Result { + let stake = total_gateway_stake_value(storage).checked_sub(amount)?; + mut_total_gateway_stake(storage).save(&stake)?; + Ok(stake) +} + 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, diff --git a/contracts/mixnet/src/transactions.rs b/contracts/mixnet/src/transactions.rs index 7b2ec5c14c..dfca2d508e 100644 --- a/contracts/mixnet/src/transactions.rs +++ b/contracts/mixnet/src/transactions.rs @@ -111,6 +111,8 @@ pub(crate) fn try_add_mixnode( mixnodes(deps.storage).save(identity.as_bytes(), &bond)?; mixnodes_owners(deps.storage).save(sender_bytes, identity)?; increment_layer_count(deps.storage, bond.layer)?; + incr_total_mix_stake(bond.bond_amount().amount, deps.storage)?; + incr_total_mix_stake(bond.total_delegation().amount, deps.storage)?; let attributes = vec![attr("overwritten", was_present)]; Ok(Response { @@ -150,6 +152,9 @@ pub(crate) fn try_remove_mixnode( // decrement layer count decrement_layer_count(deps.storage, mixnode_bond.layer)?; + decr_total_mix_stake(mixnode_bond.bond_amount().amount, deps.storage)?; + decr_total_mix_stake(mixnode_bond.total_delegation().amount, deps.storage)?; + // log our actions let attributes = vec![attr("action", "unbond"), attr("mixnode_bond", mixnode_bond)]; @@ -243,6 +248,9 @@ pub(crate) fn try_add_gateway( gateways_owners(deps.storage).save(sender_bytes, identity)?; increment_layer_count(deps.storage, Layer::Gateway)?; + incr_total_gateway_stake(bond.bond_amount().amount, deps.storage)?; + incr_total_gateway_stake(bond.total_delegation().amount, deps.storage)?; + let attributes = vec![attr("overwritten", was_present)]; Ok(Response { submessages: Vec::new(), @@ -281,6 +289,9 @@ pub(crate) fn try_remove_gateway( // decrement layer count decrement_layer_count(deps.storage, Layer::Gateway)?; + decr_total_gateway_stake(gateway_bond.bond_amount().amount, deps.storage)?; + decr_total_gateway_stake(gateway_bond.total_delegation().amount, deps.storage)?; + // log our actions let attributes = vec![ attr("action", "unbond"), @@ -561,8 +572,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); @@ -570,8 +583,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); @@ -579,6 +592,8 @@ pub(crate) fn try_delegate_to_mixnode( reverse_mix_delegations(deps.storage, &info.sender).save(mix_identity.as_bytes(), &())?; + incr_total_mix_stake(amount, deps.storage)?; + Ok(Response::default()) } @@ -614,6 +629,8 @@ pub(crate) fn try_remove_delegation_from_mixnode( .checked_sub(delegation.amount) .unwrap(); mixnodes_bucket.save(mix_identity.as_bytes(), &existing_bond)?; + // We should only decr total if a node is bonded, node unbonding remove all delegations from the total + decr_total_mix_stake(delegation.amount, deps.storage)?; } Ok(Response { @@ -669,6 +686,8 @@ pub(crate) fn try_delegate_to_gateway( reverse_gateway_delegations(deps.storage, &info.sender) .save(gateway_identity.as_bytes(), &())?; + incr_total_gateway_stake(info.funds[0].amount, deps.storage)?; + Ok(Response::default()) } @@ -707,6 +726,8 @@ pub(crate) fn try_remove_delegation_from_gateway( .checked_sub(delegation.amount) .unwrap(); gateways_bucket.save(gateway_identity.as_bytes(), &existing_bond)?; + + decr_total_gateway_stake(delegation.amount, deps.storage)?; } Ok(Response { @@ -3494,7 +3515,7 @@ pub mod tests { } #[test] - fn delegation_is_not_removed_if_node_unbonded() { + fn delegation_is_not_removed_if_gateway_unbonded() { let mut deps = helpers::init_contract(); let gateway_owner = "bob"; @@ -4001,4 +4022,122 @@ pub mod tests { assert_eq!(bob_node.mix_node.identity_key, "bob"); assert_eq!(bob_node.layer, Layer::Two); } + + #[test] + fn mix_stake_accounting() { + let mut deps = helpers::init_contract(); + let mut test_bond = INITIAL_MIXNODE_BOND; + try_add_mixnode( + deps.as_mut(), + mock_env(), + mock_info("alice", &good_mixnode_bond()), + MixNode { + identity_key: "alice".to_string(), + ..helpers::mix_node_fixture() + }, + ) + .unwrap(); + + assert_eq!(total_mix_stake_value(&deps.storage), test_bond); + + let delegation = coin(100, DENOM); + try_delegate_to_mixnode( + deps.as_mut(), + mock_env(), + mock_info("bob", &vec![delegation.clone()]), + "alice".to_string(), + ) + .unwrap(); + + test_bond += Uint128(100); + assert_eq!(total_mix_stake_value(&deps.storage), test_bond); + + try_remove_mixnode(deps.as_mut(), mock_info("alice", &[])).unwrap(); + + test_bond = Uint128(0); + assert_eq!(total_mix_stake_value(&deps.storage), test_bond); + + try_add_mixnode( + deps.as_mut(), + mock_env(), + mock_info("alice", &good_mixnode_bond()), + MixNode { + identity_key: "alice".to_string(), + ..helpers::mix_node_fixture() + }, + ) + .unwrap(); + + test_bond = Uint128(100) + INITIAL_MIXNODE_BOND; + assert_eq!(total_mix_stake_value(&deps.storage), test_bond); + + try_remove_delegation_from_mixnode( + deps.as_mut(), + mock_info("bob", &[]), + "alice".to_string(), + ) + .unwrap(); + + test_bond = test_bond.checked_sub(Uint128(100)).unwrap(); + assert_eq!(total_mix_stake_value(&deps.storage), test_bond); + } + + #[test] + fn gateway_stake_accounting() { + let mut deps = helpers::init_contract(); + let mut test_bond = INITIAL_GATEWAY_BOND; + try_add_gateway( + deps.as_mut(), + mock_env(), + mock_info("alice", &good_gateway_bond()), + Gateway { + identity_key: "alice".to_string(), + ..helpers::gateway_fixture() + }, + ) + .unwrap(); + + assert_eq!(total_gateway_stake_value(&deps.storage), test_bond); + + let delegation = coin(100, DENOM); + try_delegate_to_gateway( + deps.as_mut(), + mock_env(), + mock_info("bob", &vec![delegation.clone()]), + "alice".to_string(), + ) + .unwrap(); + + test_bond += Uint128(100); + assert_eq!(total_gateway_stake_value(&deps.storage), test_bond); + + try_remove_gateway(deps.as_mut(), mock_info("alice", &[])).unwrap(); + + test_bond = Uint128(0); + assert_eq!(total_gateway_stake_value(&deps.storage), test_bond); + + try_add_gateway( + deps.as_mut(), + mock_env(), + mock_info("alice", &good_gateway_bond()), + Gateway { + identity_key: "alice".to_string(), + ..helpers::gateway_fixture() + }, + ) + .unwrap(); + + test_bond = Uint128(100) + INITIAL_GATEWAY_BOND; + assert_eq!(total_gateway_stake_value(&deps.storage), test_bond); + + try_remove_delegation_from_gateway( + deps.as_mut(), + mock_info("bob", &[]), + "alice".to_string(), + ) + .unwrap(); + + test_bond = test_bond.checked_sub(Uint128(100)).unwrap(); + assert_eq!(total_gateway_stake_value(&deps.storage), test_bond); + } } diff --git a/tokenomics-py/Cargo.toml b/tokenomics-py/Cargo.toml new file mode 100644 index 0000000000..1d864910b8 --- /dev/null +++ b/tokenomics-py/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "tokenomics-py" +version = "0.1.0" +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "tokenomics_py" +crate-type = ["cdylib"] + +[dependencies] +mixnet-contract = {path = "../common/mixnet-contract"} +cosmwasm-std = { git = "https://github.com/jstuczyn/cosmwasm", branch="0.14.1-updatedk256" } + +[dependencies.pyo3] +version = "0.14.5" +features = ["extension-module"] + + diff --git a/tokenomics-py/src/lib.rs b/tokenomics-py/src/lib.rs new file mode 100644 index 0000000000..c8e466b2a8 --- /dev/null +++ b/tokenomics-py/src/lib.rs @@ -0,0 +1,49 @@ +use cosmwasm_std::Uint128; +use mixnet_contract::MixNodeBond; +use pyo3::prelude::*; + +const ACTIVE_MIX_SET_SIZE: u32 = 1000; + +#[pymodule] +fn tokenomics_py(_py: Python, m: &PyModule) -> PyResult<()> { + m.add_function(wrap_pyfunction!(compute_mix_rewards_py, m)?)?; + + Ok(()) +} + +// assign_rewards_to_nodes, Econ_results:344 +#[pyfunction] +fn compute_mix_rewards_py( + pledged: f64, + delegated: f64, + total_stake: f64, // total stake delegated across the network, ideally persisted in the blockchain + performance: f64, // uptime ? + income_global_mix: f64, // inflation pool + omega_k: f64, // work_share * number of noes, where workshare is considered uniform 1/k, so it follows this is k in the uniform case + alpha: f64, // sybil resistance param - externally defined constant + k: f64, // number of desired mixnodes - externally defined constant +) -> f64 { + // set_lambda_sigma_mixnet, Network_econ:308 + let lambda = (pledged / total_stake).min(1. / k); + let sigma = ((pledged + delegated) / total_stake).min(1. / k); + + performance * income_global_mix * (sigma * omega_k + alpha * lambda) / (1. + alpha) +} + +fn compute_mix_rewards(mix: &MixNodeBond) { + let k = ACTIVE_MIX_SET_SIZE as f64; + let one_over_k = 1. / k; + let one_over_k_uint = (one_over_k * 1_000_000.) as u128; + // Assume uniform node work distribution for simplicity + 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: +// Compute total stake across the entire network, open question is how to handle orphaned delegations +// Compute performance as uptime, uptime can be obtained from the validator API, might be useful to introduce some sugar functions. Validator API is also not available from the contract most likely +// Compute workshare, as either 1/k or node uptime / total uptime, probably best to assume 1/k for now +// k is the active set size +// Where should the inflation pool size be set diff --git a/validator-api/src/cache/mod.rs b/validator-api/src/cache/mod.rs index dfa4424659..017e66ffed 100644 --- a/validator-api/src/cache/mod.rs +++ b/validator-api/src/cache/mod.rs @@ -37,6 +37,9 @@ struct ValidatorCacheInner { current_mixnode_active_set_size: AtomicUsize, current_gateway_active_set_size: AtomicUsize, + + total_mix_stake: RwLock>, + total_gateway_stake: RwLock>, } #[derive(Default, Serialize, Clone)] @@ -76,21 +79,30 @@ impl ValidatorCacheRefresher { where C: CosmWasmClient + Sync, { - let (mixnodes, gateways) = tokio::try_join!( + let (mixnodes, gateways, total_mix_stake, total_gt_state) = tokio::try_join!( self.nymd_client.get_mixnodes(), - self.nymd_client.get_gateways() + self.nymd_client.get_gateways(), + self.nymd_client.get_total_mix_stake(), + self.nymd_client.get_total_gateway_stake() )?; let state_params = self.nymd_client.get_state_params().await?; info!( - "Updating validator cache. There are {} mixnodes and {} gateways", + "Updating validator cache. There are {} mixnodes and {} gateways, total_mix_stake is {}", mixnodes.len(), - gateways.len() + gateways.len(), + total_mix_stake ); self.cache - .update_cache(mixnodes, gateways, state_params) + .update_cache( + mixnodes, + gateways, + total_mix_stake, + total_gt_state, + state_params, + ) .await; Ok(()) @@ -177,6 +189,8 @@ impl ValidatorCache { &self, mut mixnodes: Vec, mut gateways: Vec, + total_mix_stake: u128, + total_gt_stake: u128, state: StateParams, ) { // if our data is valid, it means the active sets are available, @@ -218,6 +232,16 @@ impl ValidatorCache { self.inner.mixnodes.write().await.set(mixnodes); self.inner.gateways.write().await.set(gateways); + self.inner + .total_mix_stake + .write() + .await + .set(total_mix_stake); + self.inner + .total_gateway_stake + .write() + .await + .set(total_gt_stake); } pub async fn mixnodes(&self) -> Cache> { @@ -303,6 +327,8 @@ impl ValidatorCacheInner { active_gateways_available: AtomicBool::new(false), current_mixnode_active_set_size: Default::default(), current_gateway_active_set_size: Default::default(), + total_mix_stake: Default::default(), + total_gateway_stake: Default::default(), } } } diff --git a/validator-api/src/nymd_client.rs b/validator-api/src/nymd_client.rs index 1a449abb8d..c26632367e 100644 --- a/validator-api/src/nymd_client.rs +++ b/validator-api/src/nymd_client.rs @@ -94,6 +94,20 @@ impl Client { Ok(time) } + pub(crate) async fn get_total_mix_stake(&self) -> Result + where + C: CosmWasmClient + Sync, + { + Ok(self.0.read().await.get_total_mix_stake().await?) + } + + pub(crate) async fn get_total_gateway_stake(&self) -> Result + where + C: CosmWasmClient + Sync, + { + Ok(self.0.read().await.get_total_gateway_stake().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 09d56eada3..ed3de7a4d1 100644 --- a/validator-api/src/rewarding/mod.rs +++ b/validator-api/src/rewarding/mod.rs @@ -54,6 +54,8 @@ 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 + pub(crate) performance: Option } #[derive(Debug, Clone)] @@ -285,6 +287,7 @@ impl Rewarder { uptime: self .calculate_absolute_uptime(mix.last_day_ipv4, mix.last_day_ipv6), total_delegations, + performance: None }) }) .filter(|node| node.uptime.u8() > 0)