From 668325a4ce693fc8410891c293c353073105bc72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Tue, 28 Sep 2021 09:38:22 +0100 Subject: [PATCH] Feature/active sets (#764) * Added separate gateway active set size * Grabbing contract state * Defined PartialOrd on MixnodeBond and GatewayBond * Some initial stub for active set * Unit tests for mixnode and gateway bond partialord implementation * Obtaining active sets * Active nodes routes * Additional methods on validator client * Added state migration * Feature locking unused import * Fixed State test fixture * Included block height for partial_ord * Missing post-merge imports * api on the client for active nodes * Native/socks5/wasm clients using active nodes * Rewarding only active nodes * Updated validator client StateParams definition * Gateway active set size * Contract migration update * Cargo fmt * Updated TauriStateParams * [ci skip] Generate TS types Co-authored-by: jstuczyn --- .../src/client/topology_control.rs | 90 ++- clients/native/src/client/mod.rs | 1 + clients/socks5/src/client/mod.rs | 1 + clients/validator/package.json | 2 +- clients/validator/src/caches/gateways.ts | 4 +- clients/validator/src/caches/mixnodes.ts | 6 +- clients/validator/src/index.ts | 106 +--- clients/validator/src/net-client.ts | 2 +- clients/validator/src/query-client.ts | 2 +- clients/validator/src/types.ts | 75 +++ clients/webassembly/src/client/mod.rs | 4 +- .../validator-client/src/client.rs | 22 + .../validator-client/src/validator_api/mod.rs | 10 + .../src/validator_api/routes.rs | 2 + common/mixnet-contract/src/gateway.rs | 148 ++++- common/mixnet-contract/src/mixnode.rs | 162 ++++- common/mixnet-contract/src/types.rs | 8 +- contracts/mixnet/Cargo.lock | 600 +++++++++++++++++- contracts/mixnet/src/contract.rs | 115 ++-- contracts/mixnet/src/queries.rs | 1 + contracts/mixnet/src/storage.rs | 18 - contracts/mixnet/src/transactions.rs | 1 + .../src-tauri/src/operations/admin.rs | 3 + tauri-wallet/src/types/rust/stateparams.ts | 1 + validator-api/src/cache/mod.rs | 155 ++++- validator-api/src/cache/routes.rs | 14 + validator-api/src/nymd_client.rs | 9 +- validator-api/src/rewarding/mod.rs | 73 ++- validator-api/src/storage/mod.rs | 6 +- wallet-web/components/admin/AdminForm.tsx | 14 +- wallet-web/next-env.d.ts | 4 + wallet-web/package.json | 2 +- wallet-web/pages/admin.tsx | 3 +- wallet-web/yarn.lock | 7 +- 34 files changed, 1436 insertions(+), 235 deletions(-) diff --git a/clients/client-core/src/client/topology_control.rs b/clients/client-core/src/client/topology_control.rs index ffcbcb0f50..0863e1e867 100644 --- a/clients/client-core/src/client/topology_control.rs +++ b/clients/client-core/src/client/topology_control.rs @@ -107,7 +107,7 @@ impl TopologyAccessor { self.inner.read().await.into() } - async fn update_global_topology(&mut self, new_topology: Option) { + async fn update_global_topology(&self, new_topology: Option) { self.inner.write().await.update(new_topology); } @@ -130,19 +130,26 @@ impl Default for TopologyAccessor { pub struct TopologyRefresherConfig { validator_api_urls: Vec, refresh_rate: time::Duration, + client_version: String, } impl TopologyRefresherConfig { - pub fn new(validator_api_urls: Vec, refresh_rate: time::Duration) -> Self { + pub fn new( + validator_api_urls: Vec, + refresh_rate: time::Duration, + client_version: String, + ) -> Self { TopologyRefresherConfig { validator_api_urls, refresh_rate, + client_version, } } } pub struct TopologyRefresher { validator_client: validator_client::ApiClient, + client_version: String, validator_api_urls: Vec, topology_accessor: TopologyAccessor, @@ -158,6 +165,7 @@ impl TopologyRefresher { TopologyRefresher { validator_client: validator_client::ApiClient::new(cfg.validator_api_urls[0].clone()), + client_version: cfg.client_version, validator_api_urls: cfg.validator_api_urls, topology_accessor, refresh_rate: cfg.refresh_rate, @@ -177,12 +185,71 @@ impl TopologyRefresher { .change_validator_api(self.validator_api_urls[self.currently_used_api].clone()) } - async fn get_current_compatible_topology(&mut self) -> Option { + /// Verifies whether nodes a reasonably distributed among all mix layers. + /// + /// In ideal world we would have 33% nodes on layer 1, 33% on layer 2 and 33% on layer 3. + /// However, this is a rather unrealistic expectation, instead we check whether there exists + /// a layer with more than 66% of nodes or with fewer than 15% and if so, we trigger a failure. + /// + /// # Arguments + /// + /// * `topology`: active topology constructed from validator api data + /// * `mixnodes_count`: total number of active mixnodes + fn check_layer_distribution( + &self, + active_topology: &NymTopology, + mixnodes_count: usize, + ) -> bool { + let mixes = active_topology.mixes(); + if active_topology.gateways().is_empty() { + return false; + } + + // trivial check to see if have at least a single node on each layer (regardless of active set size) + if mixes.get(&1).is_none() || mixes.get(&2).is_none() || mixes.get(&3).is_none() { + return false; + } + + let upper_bound = (mixnodes_count as f32 * 0.66) as usize; + let lower_bound = (mixnodes_count as f32 * 0.15) as usize; + + let layer1 = mixes.get(&1).unwrap().len(); + let layer2 = mixes.get(&2).unwrap().len(); + let layer3 = mixes.get(&3).unwrap().len(); + + if layer1 < lower_bound || layer1 > upper_bound { + warn!( + "nodes: {}, layer1: {}, layer2: {}, layer3: {}", + mixnodes_count, layer1, layer2, layer3 + ); + return false; + } + + if layer2 < lower_bound || layer2 > upper_bound { + warn!( + "nodes: {}, layer1: {}, layer2: {}, layer3: {}", + mixnodes_count, layer1, layer2, layer3 + ); + return false; + } + + if layer3 < lower_bound || layer3 > upper_bound { + warn!( + "nodes: {}, layer1: {}, layer2: {}, layer3: {}", + mixnodes_count, layer1, layer2, layer3 + ); + return false; + } + + true + } + + async fn get_current_compatible_topology(&self) -> Option { // TODO: optimization for the future: // only refresh mixnodes on timer and refresh gateways only when // we have to send to a new, unknown, gateway - let mixnodes = match self.validator_client.get_cached_mixnodes().await { + let mixnodes = match self.validator_client.get_cached_active_mixnodes().await { Err(err) => { error!("failed to get network mixnodes - {}", err); return None; @@ -190,7 +257,7 @@ impl TopologyRefresher { Ok(mixes) => mixes, }; - let gateways = match self.validator_client.get_cached_gateways().await { + let gateways = match self.validator_client.get_cached_active_gateways().await { Err(err) => { error!("failed to get network gateways - {}", err); return None; @@ -198,11 +265,16 @@ impl TopologyRefresher { Ok(gateways) => gateways, }; - let topology = nym_topology_from_bonds(mixnodes, gateways); + let mixnodes_count = mixnodes.len(); + let topology = + nym_topology_from_bonds(mixnodes, gateways).filter_system_version(&self.client_version); - // TODO: I didn't want to change it now, but the expected system version should rather be put in config - // rather than pulled from package version of `client_core` - Some(topology.filter_system_version(env!("CARGO_PKG_VERSION"))) + if !self.check_layer_distribution(&topology, mixnodes_count) { + warn!("The current filtered active topology has extremely skewed layer distribution. It cannot be used."); + None + } else { + Some(topology) + } } pub async fn refresh(&mut self) { diff --git a/clients/native/src/client/mod.rs b/clients/native/src/client/mod.rs index 2b1c28b8cf..a4e57edb65 100644 --- a/clients/native/src/client/mod.rs +++ b/clients/native/src/client/mod.rs @@ -236,6 +236,7 @@ impl NymClient { let topology_refresher_config = TopologyRefresherConfig::new( self.config.get_base().get_validator_api_endpoints(), self.config.get_base().get_topology_refresh_rate(), + env!("CARGO_PKG_VERSION").to_string(), ); let mut topology_refresher = TopologyRefresher::new(topology_refresher_config, topology_accessor); diff --git a/clients/socks5/src/client/mod.rs b/clients/socks5/src/client/mod.rs index 485866673a..212787dc43 100644 --- a/clients/socks5/src/client/mod.rs +++ b/clients/socks5/src/client/mod.rs @@ -224,6 +224,7 @@ impl NymClient { let topology_refresher_config = TopologyRefresherConfig::new( self.config.get_base().get_validator_api_endpoints(), self.config.get_base().get_topology_refresh_rate(), + env!("CARGO_PKG_VERSION").to_string(), ); let mut topology_refresher = TopologyRefresher::new(topology_refresher_config, topology_accessor); diff --git a/clients/validator/package.json b/clients/validator/package.json index 167c1cfff7..bdd09f3002 100644 --- a/clients/validator/package.json +++ b/clients/validator/package.json @@ -1,6 +1,6 @@ { "name": "@nymproject/nym-validator-client", - "version": "0.17.0", + "version": "0.18.0", "description": "A TypeScript client for interacting with smart contracts in Nym validators", "repository": "https://github.com/nymtech/nym", "main": "./dist/index.js", diff --git a/clients/validator/src/caches/gateways.ts b/clients/validator/src/caches/gateways.ts index d221d77af7..f797c3ba98 100644 --- a/clients/validator/src/caches/gateways.ts +++ b/clients/validator/src/caches/gateways.ts @@ -1,7 +1,7 @@ -import { GatewayBond } from "../types"; +import {GatewayBond, PagedGatewayResponse} from "../types"; import {INetClient} from "../net-client" import {IQueryClient} from "../query-client"; -import {PagedGatewayResponse, VALIDATOR_API_GATEWAYS, VALIDATOR_API_PORT} from "../index"; +import {VALIDATOR_API_GATEWAYS, VALIDATOR_API_PORT} from "../index"; import axios from "axios"; diff --git a/clients/validator/src/caches/mixnodes.ts b/clients/validator/src/caches/mixnodes.ts index 094bd6dce8..9b1cfb100a 100644 --- a/clients/validator/src/caches/mixnodes.ts +++ b/clients/validator/src/caches/mixnodes.ts @@ -1,13 +1,13 @@ -import { MixNodeBond } from "../types"; +import {MixNodeBond, PagedMixnodeResponse} from "../types"; import { INetClient } from "../net-client" import {IQueryClient} from "../query-client"; -import {PagedMixnodeResponse, VALIDATOR_API_MIXNODES, VALIDATOR_API_PORT} from "../index"; +import {VALIDATOR_API_MIXNODES, VALIDATOR_API_PORT} from "../index"; import axios from "axios"; export { MixnodesCache }; /** - * There are serious limits in smart contract systems, but we need to keep track of + * There are serious limits in smart contract systems, but we need to keep track of * potentially thousands of nodes. MixnodeCache instances repeatedly make requests for * paged data about what mixnodes exist, and keep them locally in memory so that they're * available for querying. diff --git a/clients/validator/src/index.ts b/clients/validator/src/index.ts index c0e2ec4558..30f002984a 100644 --- a/clients/validator/src/index.ts +++ b/clients/validator/src/index.ts @@ -1,9 +1,19 @@ -import NetClient, { INetClient } from "./net-client"; -import { Gateway, GatewayBond, MixNode, MixNodeBond, SendRequest } from "./types"; -import { Bip39, Random } from "@cosmjs/crypto"; -import { DirectSecp256k1HdWallet, EncodeObject } from "@cosmjs/proto-signing"; +import NetClient, {INetClient} from "./net-client"; +import { + StateParams, + Delegation, + PagedMixDelegationsResponse, + PagedGatewayDelegationsResponse, + MixNodeBond, + MixNode, + GatewayBond, + Gateway, + SendRequest +} from "./types"; +import {Bip39, Random} from "@cosmjs/crypto"; +import {DirectSecp256k1HdWallet, EncodeObject} from "@cosmjs/proto-signing"; import MixnodesCache from "./caches/mixnodes"; -import { buildFeeTable, coin, Coin, coins, StdFee } from "@cosmjs/stargate"; +import {buildFeeTable, coin, Coin, coins, StdFee} from "@cosmjs/stargate"; import { ExecuteResult, InstantiateOptions, @@ -22,17 +32,17 @@ import { nativeToPrintable } from "./currency"; import GatewaysCache from "./caches/gateways"; -import QueryClient, { IQueryClient } from "./query-client"; -import { nymGasLimits, nymGasPrice } from "./stargate-helper"; -import { BroadcastTxSuccess, isBroadcastTxFailure } from "@cosmjs/stargate"; -import { makeBankMsgSend } from "./utils"; +import QueryClient, {IQueryClient} from "./query-client"; +import {nymGasLimits, nymGasPrice} from "./stargate-helper"; +import {BroadcastTxSuccess, isBroadcastTxFailure} from "@cosmjs/stargate"; +import {makeBankMsgSend} from "./utils"; export const VALIDATOR_API_PORT = "8080"; export const VALIDATOR_API_GATEWAYS = "v1/gateways"; export const VALIDATOR_API_MIXNODES = "v1/mixnodes"; -export { coins, coin }; -export { Coin }; +export {coins, coin}; +export {Coin}; export { displayAmountToNative, nativeCoinToDisplay, @@ -42,7 +52,7 @@ export { MappedCoin, CoinMap } -export { nymGasLimits, nymGasPrice } +export {nymGasLimits, nymGasPrice} export default class ValidatorClient { private readonly client: INetClient | IQueryClient @@ -619,75 +629,3 @@ export default class ValidatorClient { } } - -/// One page of a possible multi-page set of mixnodes. The paging interface is quite -/// inconvenient, as we don't have the two pieces of information we need to know -/// in order to do paging nicely (namely `currentPage` and `totalPages` parameters). -/// -/// Instead, we have only `start_next_page_after`, i.e. the key of the last record -/// on this page. In order to get the *next* page, CosmWasm looks at that value, -/// finds the next record, and builds the next page starting there. This happens -/// **in series** rather than **in parallel** (!). -/// -/// So we have some consistency problems: -/// -/// * we can't make requests at a given block height, so the result set -/// which we assemble over time may change while requests are being made. -/// * at some point we will make a request for a `start_next_page_after` key -/// which has just been deleted from the database. -/// -/// TODO: more robust error handling on the "deleted key" case. -export type PagedMixnodeResponse = { - nodes: MixNodeBond[], - per_page: number, // TODO: camelCase - start_next_after: string, // TODO: camelCase -} - -// a temporary way of achieving the same paging behaviour for the gateways -// the same points made for `PagedResponse` stand here. -export type PagedGatewayResponse = { - nodes: GatewayBond[], - per_page: number, // TODO: camelCase - start_next_after: string, // TODO: camelCase -} - -export type MixOwnershipResponse = { - address: string, - has_node: boolean, -} - -export type GatewayOwnershipResponse = { - address: string, - has_gateway: boolean, -} - -export type StateParams = { - epoch_length: number, - // ideally I'd want to define those as `number` rather than `string`, but - // rust-side they are defined as Uint128 and Decimal that don't have - // native javascript representations and therefore are interpreted as strings after deserialization - minimum_mixnode_bond: string, - minimum_gateway_bond: string, - mixnode_bond_reward_rate: string, - gateway_bond_reward_rate: string, - mixnode_delegation_reward_rate: string, - gateway_delegation_reward_rate: string, - mixnode_active_set_size: number, -} - -export type Delegation = { - owner: string, - amount: Coin, -} - -export type PagedMixDelegationsResponse = { - node_owner: string, - delegations: Delegation[], - start_next_after: string -} - -export type PagedGatewayDelegationsResponse = { - node_owner: string, - delegations: Delegation[], - start_next_after: string -} diff --git a/clients/validator/src/net-client.ts b/clients/validator/src/net-client.ts index 0f3edec7c7..5f99d93231 100644 --- a/clients/validator/src/net-client.ts +++ b/clients/validator/src/net-client.ts @@ -6,7 +6,7 @@ import { PagedGatewayResponse, PagedMixDelegationsResponse, PagedMixnodeResponse, StateParams -} from "./index"; +} from "./types"; import { DirectSecp256k1HdWallet, EncodeObject } from "@cosmjs/proto-signing"; import { Coin, StdFee } from "@cosmjs/stargate"; import { BroadcastTxResponse } from "@cosmjs/stargate" diff --git a/clients/validator/src/query-client.ts b/clients/validator/src/query-client.ts index 1be0a5becd..bf8ad35c5b 100644 --- a/clients/validator/src/query-client.ts +++ b/clients/validator/src/query-client.ts @@ -7,7 +7,7 @@ import { PagedGatewayResponse, PagedMixDelegationsResponse, PagedMixnodeResponse, StateParams -} from "./index"; +} from "./types"; export interface IQueryClient { getBalance(address: string, stakeDenom: string): Promise; diff --git a/clients/validator/src/types.ts b/clients/validator/src/types.ts index 72844c47e1..4d4b9cb8cf 100644 --- a/clients/validator/src/types.ts +++ b/clients/validator/src/types.ts @@ -1,5 +1,80 @@ import { Coin } from "@cosmjs/stargate"; + +/// One page of a possible multi-page set of mixnodes. The paging interface is quite +/// inconvenient, as we don't have the two pieces of information we need to know +/// in order to do paging nicely (namely `currentPage` and `totalPages` parameters). +/// +/// Instead, we have only `start_next_page_after`, i.e. the key of the last record +/// on this page. In order to get the *next* page, CosmWasm looks at that value, +/// finds the next record, and builds the next page starting there. This happens +/// **in series** rather than **in parallel** (!). +/// +/// So we have some consistency problems: +/// +/// * we can't make requests at a given block height, so the result set +/// which we assemble over time may change while requests are being made. +/// * at some point we will make a request for a `start_next_page_after` key +/// which has just been deleted from the database. +/// +/// TODO: more robust error handling on the "deleted key" case. +export type PagedMixnodeResponse = { + nodes: MixNodeBond[], + per_page: number, // TODO: camelCase + start_next_after: string, // TODO: camelCase +} + +// a temporary way of achieving the same paging behaviour for the gateways +// the same points made for `PagedResponse` stand here. +export type PagedGatewayResponse = { + nodes: GatewayBond[], + per_page: number, // TODO: camelCase + start_next_after: string, // TODO: camelCase +} + +export type MixOwnershipResponse = { + address: string, + has_node: boolean, +} + +export type GatewayOwnershipResponse = { + address: string, + has_gateway: boolean, +} + +export type StateParams = { + epoch_length: number, + // ideally I'd want to define those as `number` rather than `string`, but + // rust-side they are defined as Uint128 and Decimal that don't have + // native javascript representations and therefore are interpreted as strings after deserialization + minimum_mixnode_bond: string, + minimum_gateway_bond: string, + mixnode_bond_reward_rate: string, + gateway_bond_reward_rate: string, + mixnode_delegation_reward_rate: string, + gateway_delegation_reward_rate: string, + mixnode_active_set_size: number, + gateway_active_set_size: number, +} + +export type Delegation = { + owner: string, + amount: Coin, +} + +export type PagedMixDelegationsResponse = { + node_owner: string, + delegations: Delegation[], + start_next_after: string +} + +export type PagedGatewayDelegationsResponse = { + node_owner: string, + delegations: Delegation[], + start_next_after: string +} + + export enum Layer { Gateway, One, diff --git a/clients/webassembly/src/client/mod.rs b/clients/webassembly/src/client/mod.rs index bda0774024..91c0ab3e91 100644 --- a/clients/webassembly/src/client/mod.rs +++ b/clients/webassembly/src/client/mod.rs @@ -263,12 +263,12 @@ impl NymClient { pub(crate) async fn get_nym_topology(&self) -> NymTopology { let validator_client = validator_client::ApiClient::new(self.validator_server.clone()); - let mixnodes = match validator_client.get_cached_mixnodes().await { + let mixnodes = match validator_client.get_cached_active_mixnodes().await { Err(err) => panic!("{}", err), Ok(mixes) => mixes, }; - let gateways = match validator_client.get_cached_gateways().await { + let gateways = match validator_client.get_cached_active_gateways().await { Err(err) => panic!("{}", err), Ok(gateways) => gateways, }; diff --git a/common/client-libs/validator-client/src/client.rs b/common/client-libs/validator-client/src/client.rs index b418b5604a..da8a38965e 100644 --- a/common/client-libs/validator-client/src/client.rs +++ b/common/client-libs/validator-client/src/client.rs @@ -5,6 +5,9 @@ use crate::nymd::{ error::NymdError, CosmWasmClient, NymdClient, QueryNymdClient, SigningNymdClient, }; +#[cfg(feature = "nymd-client")] +use mixnet_contract::StateParams; + use crate::{validator_api, ValidatorClientError}; use coconut_interface::{BlindSignRequestBody, BlindedSignatureResponse, VerificationKeyResponse}; use mixnet_contract::{GatewayBond, MixNodeBond}; @@ -170,6 +173,13 @@ impl Client { Ok(self.validator_api.get_gateways().await?) } + pub async fn get_state_params(&self) -> Result + where + C: CosmWasmClient + Sync, + { + Ok(self.nymd.get_state_params().await?) + } + // basically handles paging for us pub async fn get_all_nymd_mixnodes(&self) -> Result, ValidatorClientError> where @@ -414,6 +424,18 @@ impl ApiClient { self.validator_api.change_url(new_endpoint); } + pub async fn get_cached_active_mixnodes( + &self, + ) -> Result, ValidatorClientError> { + Ok(self.validator_api.get_active_mixnodes().await?) + } + + pub async fn get_cached_active_gateways( + &self, + ) -> Result, ValidatorClientError> { + Ok(self.validator_api.get_active_gateways().await?) + } + pub async fn get_cached_mixnodes(&self) -> Result, ValidatorClientError> { Ok(self.validator_api.get_mixnodes().await?) } diff --git a/common/client-libs/validator-client/src/validator_api/mod.rs b/common/client-libs/validator-client/src/validator_api/mod.rs index 2c24f07b73..dcc448d7d9 100644 --- a/common/client-libs/validator-client/src/validator_api/mod.rs +++ b/common/client-libs/validator-client/src/validator_api/mod.rs @@ -68,6 +68,16 @@ impl Client { .await } + pub async fn get_active_mixnodes(&self) -> Result, ValidatorAPIError> { + self.query_validator_api(&[routes::API_VERSION, routes::MIXNODES, routes::ACTIVE]) + .await + } + + pub async fn get_active_gateways(&self) -> Result, ValidatorAPIError> { + self.query_validator_api(&[routes::API_VERSION, routes::GATEWAYS, routes::ACTIVE]) + .await + } + pub async fn blind_sign( &self, request_body: &BlindSignRequestBody, diff --git a/common/client-libs/validator-client/src/validator_api/routes.rs b/common/client-libs/validator-client/src/validator_api/routes.rs index 83fe3a1aaf..4a22b48150 100644 --- a/common/client-libs/validator-client/src/validator_api/routes.rs +++ b/common/client-libs/validator-client/src/validator_api/routes.rs @@ -7,5 +7,7 @@ pub const API_VERSION: &str = VALIDATOR_API_VERSION; pub const MIXNODES: &str = "mixnodes"; pub const GATEWAYS: &str = "gateways"; +pub const ACTIVE: &str = "active"; + pub const COCONUT_BLIND_SIGN: &str = "blind_sign"; pub const COCONUT_VERIFICATION_KEY: &str = "verification_key"; diff --git a/common/mixnet-contract/src/gateway.rs b/common/mixnet-contract/src/gateway.rs index 68acfc96de..6318452ecb 100644 --- a/common/mixnet-contract/src/gateway.rs +++ b/common/mixnet-contract/src/gateway.rs @@ -5,12 +5,13 @@ use crate::{IdentityKey, SphinxKey}; use cosmwasm_std::{coin, Addr, Coin}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use std::cmp::Ordering; use std::fmt::Display; use ts_rs::TS; use crate::current_block_height; -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema, TS)] +#[derive(Clone, Debug, Deserialize, PartialEq, PartialOrd, Serialize, JsonSchema, TS)] pub struct Gateway { pub host: String, pub mix_port: u16, @@ -60,6 +61,64 @@ impl GatewayBond { } } +impl PartialOrd for GatewayBond { + fn partial_cmp(&self, other: &Self) -> Option { + // first remove invalid cases + if self.bond_amount.denom != self.total_delegation.denom { + return None; + } + + if other.bond_amount.denom != other.total_delegation.denom { + return None; + } + + if self.bond_amount.denom != other.bond_amount.denom { + return None; + } + + // try to order by total bond + delegation + let total_cmp = (self.bond_amount.amount + self.total_delegation.amount) + .partial_cmp(&(self.bond_amount.amount + self.total_delegation.amount))?; + + if total_cmp != Ordering::Equal { + return Some(total_cmp); + } + + // then if those are equal, prefer higher bond over delegation + let bond_cmp = self + .bond_amount + .amount + .partial_cmp(&other.bond_amount.amount)?; + if bond_cmp != Ordering::Equal { + return Some(bond_cmp); + } + + // then look at delegation (I'm not sure we can get here, but better safe than sorry) + let delegation_cmp = self + .total_delegation + .amount + .partial_cmp(&other.total_delegation.amount)?; + if delegation_cmp != Ordering::Equal { + return Some(delegation_cmp); + } + + // then check block height + let height_cmp = self.block_height.partial_cmp(&other.block_height)?; + if height_cmp != Ordering::Equal { + return Some(height_cmp); + } + + // finally go by the rest of the fields in order. It doesn't really matter at this point + // but we should be deterministic. + let owner_cmp = self.owner.partial_cmp(&other.owner)?; + if owner_cmp != Ordering::Equal { + return Some(owner_cmp); + } + + self.gateway.partial_cmp(&other.gateway) + } +} + impl Display for GatewayBond { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!( @@ -96,3 +155,90 @@ pub struct GatewayOwnershipResponse { pub address: Addr, pub has_gateway: bool, } + +#[cfg(test)] +mod tests { + use super::*; + + fn gateway_fixture() -> Gateway { + Gateway { + host: "1.1.1.1".to_string(), + mix_port: 123, + clients_port: 456, + location: "foomplandia".to_string(), + sphinx_key: "sphinxkey".to_string(), + identity_key: "identitykey".to_string(), + version: "0.11.0".to_string(), + } + } + + #[test] + fn gateway_bond_partial_ord() { + let _150foos = Coin::new(150, "foo"); + let _50foos = Coin::new(50, "foo"); + let _0foos = Coin::new(0, "foo"); + + let gate1 = GatewayBond { + bond_amount: _150foos.clone(), + total_delegation: _50foos.clone(), + owner: Addr::unchecked("foo1"), + block_height: 100, + gateway: gateway_fixture(), + }; + + let gate2 = GatewayBond { + bond_amount: _150foos.clone(), + total_delegation: _50foos.clone(), + owner: Addr::unchecked("foo2"), + block_height: 120, + gateway: gateway_fixture(), + }; + + let gate3 = GatewayBond { + bond_amount: _50foos, + total_delegation: _150foos.clone(), + owner: Addr::unchecked("foo3"), + block_height: 120, + gateway: gateway_fixture(), + }; + + let gate4 = GatewayBond { + bond_amount: _150foos.clone(), + total_delegation: _0foos.clone(), + owner: Addr::unchecked("foo4"), + block_height: 120, + gateway: gateway_fixture(), + }; + + let gate5 = GatewayBond { + bond_amount: _0foos, + total_delegation: _150foos, + owner: Addr::unchecked("foo5"), + block_height: 120, + gateway: gateway_fixture(), + }; + + // summary: + // gate1: 150bond + 50delegation, foo1, 100 + // gate2: 150bond + 50delegation, foo2, 120 + // gate3: 50bond + 150delegation, foo3, 120 + // gate4: 150bond + 0delegation, foo4, 120 + // gate5: 0bond + 150delegation, foo5, 120 + + // highest total bond+delegation is used + // then bond followed by delegation + // finally just the rest of the fields + + // gate1 has higher total than gate4 or gate5 + assert!(gate1 > gate4); + assert!(gate1 > gate5); + + // gate1 has the same total as gate3, however, gate1 has more tokens in bond + assert!(gate1 > gate3); + // same case for gate4 and gate5 + assert!(gate4 > gate5); + + // same bond and delegation, so it's just ordered by height + assert!(gate1 < gate2); + } +} diff --git a/common/mixnet-contract/src/mixnode.rs b/common/mixnet-contract/src/mixnode.rs index 2c3303b3a9..53610ab36e 100644 --- a/common/mixnet-contract/src/mixnode.rs +++ b/common/mixnet-contract/src/mixnode.rs @@ -6,12 +6,13 @@ use cosmwasm_std::{coin, Addr, Coin}; 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; use crate::current_block_height; -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema, TS)] +#[derive(Clone, Debug, Deserialize, PartialEq, PartialOrd, Serialize, JsonSchema, TS)] pub struct MixNode { pub host: String, pub mix_port: u16, @@ -23,7 +24,9 @@ pub struct MixNode { pub version: String, } -#[derive(Copy, Clone, Debug, Serialize_repr, PartialEq, Deserialize_repr, JsonSchema)] +#[derive( + Copy, Clone, Debug, Serialize_repr, PartialEq, PartialOrd, Deserialize_repr, JsonSchema, +)] #[repr(u8)] pub enum Layer { Gateway = 0, @@ -78,6 +81,69 @@ impl MixNodeBond { } } +impl PartialOrd for MixNodeBond { + fn partial_cmp(&self, other: &Self) -> Option { + // first remove invalid cases + if self.bond_amount.denom != self.total_delegation.denom { + return None; + } + + if other.bond_amount.denom != other.total_delegation.denom { + return None; + } + + if self.bond_amount.denom != other.bond_amount.denom { + return None; + } + + // try to order by total bond + delegation + let total_cmp = (self.bond_amount.amount + self.total_delegation.amount) + .partial_cmp(&(self.bond_amount.amount + self.total_delegation.amount))?; + + if total_cmp != Ordering::Equal { + return Some(total_cmp); + } + + // then if those are equal, prefer higher bond over delegation + let bond_cmp = self + .bond_amount + .amount + .partial_cmp(&other.bond_amount.amount)?; + if bond_cmp != Ordering::Equal { + return Some(bond_cmp); + } + + // then look at delegation (I'm not sure we can get here, but better safe than sorry) + let delegation_cmp = self + .total_delegation + .amount + .partial_cmp(&other.total_delegation.amount)?; + if delegation_cmp != Ordering::Equal { + return Some(delegation_cmp); + } + + // then check block height + let height_cmp = self.block_height.partial_cmp(&other.block_height)?; + if height_cmp != Ordering::Equal { + return Some(height_cmp); + } + + // finally go by the rest of the fields in order. It doesn't really matter at this point + // but we should be deterministic. + let owner_cmp = self.owner.partial_cmp(&other.owner)?; + if owner_cmp != Ordering::Equal { + return Some(owner_cmp); + } + + let layer_cmp = self.layer.partial_cmp(&other.layer)?; + if layer_cmp != Ordering::Equal { + return Some(layer_cmp); + } + + self.mix_node.partial_cmp(&other.mix_node) + } +} + impl Display for MixNodeBond { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!( @@ -114,3 +180,95 @@ pub struct MixOwnershipResponse { pub address: Addr, pub has_node: bool, } + +#[cfg(test)] +mod tests { + use super::*; + + fn mixnode_fixture() -> MixNode { + MixNode { + host: "1.1.1.1".to_string(), + mix_port: 123, + verloc_port: 456, + http_api_port: 789, + sphinx_key: "sphinxkey".to_string(), + identity_key: "identitykey".to_string(), + version: "0.11.0".to_string(), + } + } + + #[test] + fn mixnode_bond_partial_ord() { + let _150foos = Coin::new(150, "foo"); + let _50foos = Coin::new(50, "foo"); + let _0foos = Coin::new(0, "foo"); + + let mix1 = MixNodeBond { + bond_amount: _150foos.clone(), + total_delegation: _50foos.clone(), + owner: Addr::unchecked("foo1"), + layer: Layer::One, + block_height: 100, + mix_node: mixnode_fixture(), + }; + + let mix2 = MixNodeBond { + bond_amount: _150foos.clone(), + total_delegation: _50foos.clone(), + owner: Addr::unchecked("foo2"), + layer: Layer::One, + block_height: 120, + mix_node: mixnode_fixture(), + }; + + let mix3 = MixNodeBond { + bond_amount: _50foos, + total_delegation: _150foos.clone(), + owner: Addr::unchecked("foo3"), + layer: Layer::One, + block_height: 120, + mix_node: mixnode_fixture(), + }; + + let mix4 = MixNodeBond { + bond_amount: _150foos.clone(), + total_delegation: _0foos.clone(), + owner: Addr::unchecked("foo4"), + layer: Layer::One, + block_height: 120, + mix_node: mixnode_fixture(), + }; + + let mix5 = MixNodeBond { + bond_amount: _0foos, + total_delegation: _150foos, + owner: Addr::unchecked("foo5"), + layer: Layer::One, + block_height: 120, + mix_node: mixnode_fixture(), + }; + + // summary: + // mix1: 150bond + 50delegation, foo1, 100 + // mix2: 150bond + 50delegation, foo2, 120 + // mix3: 50bond + 150delegation, foo3, 120 + // mix4: 150bond + 0delegation, foo4, 120 + // mix5: 0bond + 150delegation, foo5, 120 + + // highest total bond+delegation is used + // then bond followed by delegation + // finally just the rest of the fields + + // mix1 has higher total than mix4 or mix5 + assert!(mix1 > mix4); + assert!(mix1 > mix5); + + // mix1 has the same total as mix3, however, mix1 has more tokens in bond + assert!(mix1 > mix3); + // same case for mix4 and mix5 + assert!(mix4 > mix5); + + // same bond and delegation, so it's just ordered by height + assert!(mix1 < mix2); + } +} diff --git a/common/mixnet-contract/src/types.rs b/common/mixnet-contract/src/types.rs index 9d0404daf0..bc3e3eb24f 100644 --- a/common/mixnet-contract/src/types.rs +++ b/common/mixnet-contract/src/types.rs @@ -37,6 +37,7 @@ pub struct StateParams { pub mixnode_delegation_reward_rate: Decimal, // annual reward rate, expressed as a decimal like 1.25 pub gateway_delegation_reward_rate: Decimal, // annual reward rate, expressed as a decimal like 1.25 pub mixnode_active_set_size: u32, + pub gateway_active_set_size: u32, } impl Display for StateParams { @@ -67,8 +68,13 @@ impl Display for StateParams { )?; write!( f, - "mixnode active set size: {} ]", + "mixnode active set size: {}", self.mixnode_active_set_size + )?; + write!( + f, + "gateway active set size: {} ]", + self.gateway_active_set_size ) } } diff --git a/contracts/mixnet/Cargo.lock b/contracts/mixnet/Cargo.lock index 8dcd1c31c6..90c5f59a0f 100644 --- a/contracts/mixnet/Cargo.lock +++ b/contracts/mixnet/Cargo.lock @@ -2,6 +2,45 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" +dependencies = [ + "lazy_static", + "regex", +] + +[[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] + +[[package]] +name = "ast_node" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93f52ce8fac3d0e6720a92b0576d737c01b1b5db4dd786e962e5925f00bf755" +dependencies = [ + "darling", + "pmutil", + "proc-macro2", + "quote", + "swc_macros_common", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + [[package]] name = "base64" version = "0.13.0" @@ -38,6 +77,12 @@ dependencies = [ "byte-tools", ] +[[package]] +name = "bumpalo" +version = "3.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9df67f7bf9ef8498769f994239c45613ef0c5899415fb58e9add412d2c1a538" + [[package]] name = "byte-tools" version = "0.3.1" @@ -50,6 +95,12 @@ version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + [[package]] name = "cfg-if" version = "1.0.0" @@ -178,6 +229,41 @@ dependencies = [ "zeroize", ] +[[package]] +name = "darling" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72" +dependencies = [ + "darling_core", + "quote", + "syn", +] + [[package]] name = "der" version = "0.4.0" @@ -205,6 +291,45 @@ dependencies = [ "generic-array 0.14.4", ] +[[package]] +name = "dprint-core" +version = "0.35.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93bd44f40b1881477837edc7112695d4b174f058c36c1cbc4c50f8d0482e2ac8" +dependencies = [ + "bumpalo", + "fnv", + "serde", +] + +[[package]] +name = "dprint-plugin-typescript" +version = "0.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67ba0077bd2ab9235848e793fbbfb563e6a04b4c8e4149827802a84063c15805" +dependencies = [ + "dprint-core", + "dprint-swc-ecma-ast-view", + "fnv", + "serde", + "swc_common", + "swc_ecmascript", +] + +[[package]] +name = "dprint-swc-ecma-ast-view" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecf692a2ee5c5f699ed0e95f21686cf6367f3a591e5d8e7bd3041bbf184651f9" +dependencies = [ + "bumpalo", + "fnv", + "num-bigint", + "swc_atoms", + "swc_common", + "swc_ecmascript", +] + [[package]] name = "dyn-clone" version = "1.0.4" @@ -237,6 +362,12 @@ dependencies = [ "thiserror", ] +[[package]] +name = "either" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" + [[package]] name = "elliptic-curve" version = "0.10.4" @@ -253,6 +384,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "enum_kind" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b940da354ae81ef0926c5eaa428207b8f4f091d3956c891dfbd124162bed99" +dependencies = [ + "pmutil", + "proc-macro2", + "swc_macros_common", + "syn", +] + [[package]] name = "fake-simd" version = "0.1.2" @@ -269,6 +412,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "form_urlencoded" version = "1.0.1" @@ -279,6 +428,27 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "from_variant" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0951635027ca477be98f8774abd6f0345233439d63f307e47101acb40c7cc63d" +dependencies = [ + "pmutil", + "proc-macro2", + "swc_macros_common", + "syn", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + [[package]] name = "generic-array" version = "0.12.4" @@ -304,7 +474,7 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "libc", "wasi 0.9.0+wasi-snapshot-preview1", ] @@ -315,7 +485,7 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "libc", "wasi 0.10.2+wasi-snapshot-preview1", ] @@ -377,6 +547,12 @@ dependencies = [ "serde", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.2.3" @@ -388,6 +564,19 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "is-macro" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a322dd16d960e322c3d92f541b4c1a4f0a2e81e1fdeee430d8cecc8b72e8015f" +dependencies = [ + "Inflector", + "pmutil", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "itoa" version = "0.4.7" @@ -400,12 +589,18 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "008b0281ca8032567c9711cd48631781c15228301860a39b32deb28d63125e46" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "ecdsa", "elliptic-curve", "sha2", ] +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + [[package]] name = "libc" version = "0.2.100" @@ -418,7 +613,7 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", ] [[package]] @@ -433,6 +628,12 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" +[[package]] +name = "memchr" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" + [[package]] name = "mixnet-contract" version = "0.1.0" @@ -441,6 +642,7 @@ dependencies = [ "schemars", "serde", "serde_repr", + "ts-rs", ] [[package]] @@ -461,10 +663,54 @@ dependencies = [ name = "network-defaults" version = "0.1.0" dependencies = [ + "serde", "time", "url", ] +[[package]] +name = "new_debug_unreachable" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" + +[[package]] +name = "num-bigint" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "090c7f9998ee0ff65aa5b723e4009f7b217707f1fb5ea551329cc4d6231fb304" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", + "serde", +] + +[[package]] +name = "num-integer" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" + [[package]] name = "opaque-debug" version = "0.2.3" @@ -477,6 +723,15 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +[[package]] +name = "owning_ref" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ff55baddef9e4ad00f88b6c743a2a8062d4c6ade126c2a528644b8e444d52ce" +dependencies = [ + "stable_deref_trait", +] + [[package]] name = "percent-encoding" version = "2.1.0" @@ -526,6 +781,25 @@ dependencies = [ "sha-1", ] +[[package]] +name = "phf_generator" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_shared" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +dependencies = [ + "siphasher", +] + [[package]] name = "pkcs8" version = "0.7.5" @@ -536,6 +810,29 @@ dependencies = [ "spki", ] +[[package]] +name = "pmutil" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3894e5d549cccbe44afecf72922f277f603cd4bb0219c8342631ef18fffbe004" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + [[package]] name = "proc-macro2" version = "1.0.24" @@ -560,6 +857,30 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha", + "rand_core 0.5.1", + "rand_hc", + "rand_pcg", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + [[package]] name = "rand_core" version = "0.5.1" @@ -578,6 +899,41 @@ dependencies = [ "getrandom 0.2.3", ] +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_pcg" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "regex" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + [[package]] name = "ryu" version = "1.0.5" @@ -608,6 +964,12 @@ dependencies = [ "syn", ] +[[package]] +name = "scoped-tls" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" + [[package]] name = "serde" version = "1.0.122" @@ -689,7 +1051,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b362ae5752fd2137731f9fa25fd4d9058af34666ca1966fb969119cc35719f12" dependencies = [ "block-buffer 0.9.0", - "cfg-if", + "cfg-if 1.0.0", "cpufeatures", "digest 0.9.0", "opaque-debug 0.3.0", @@ -705,6 +1067,18 @@ dependencies = [ "rand_core 0.6.3", ] +[[package]] +name = "siphasher" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "533494a8f9b724d33625ab53c6c4800f7cc445895924a8ef649222dcb76e938b" + +[[package]] +name = "smallvec" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" + [[package]] name = "spki" version = "0.4.0" @@ -714,18 +1088,206 @@ dependencies = [ "der", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "static_assertions" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "string_cache" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ddb1139b5353f96e429e1a5e19fbaf663bddedaa06d1dbd49f82e352601209a" +dependencies = [ + "lazy_static", + "new_debug_unreachable", + "phf_shared", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f24c8e5e19d22a726626f1a5e16fe15b132dcf21d10177fa5a45ce7962996b97" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + +[[package]] +name = "string_enum" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f584cc881e9e5f1fd6bf827b0444aa94c30d8fe6378cf241071b5f5700b2871f" +dependencies = [ + "pmutil", + "proc-macro2", + "quote", + "swc_macros_common", + "syn", +] + +[[package]] +name = "strsim" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" + [[package]] name = "subtle" version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e81da0851ada1f3e9d4312c704aa4f8806f0f9d69faaf8df2f3464b4a9437c2" +[[package]] +name = "swc_atoms" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "837a3ef86c2817228e733b6f173c821fd76f9eb21a0bc9001a826be48b00b4e7" +dependencies = [ + "string_cache", + "string_cache_codegen", +] + +[[package]] +name = "swc_common" +version = "0.10.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c93df65683ec1a001e15ce1de438c7c2c226c0c2462d1cb93fa1bd2a7664170b" +dependencies = [ + "ast_node", + "cfg-if 0.1.10", + "either", + "from_variant", + "fxhash", + "log", + "num-bigint", + "once_cell", + "owning_ref", + "scoped-tls", + "serde", + "string_cache", + "swc_eq_ignore_macros", + "swc_visit", + "unicode-width", +] + +[[package]] +name = "swc_ecma_ast" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83eb6a73820660a5af3c24ae1d436e84e4d4c13822021140011361e678df247b" +dependencies = [ + "is-macro", + "num-bigint", + "serde", + "string_enum", + "swc_atoms", + "swc_common", +] + +[[package]] +name = "swc_ecma_parser" +version = "0.52.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c03250697857164f16fa98f8e1726f566652d13e52ea3f0c3ecea9deb63ee327" +dependencies = [ + "either", + "enum_kind", + "fxhash", + "log", + "num-bigint", + "serde", + "smallvec", + "swc_atoms", + "swc_common", + "swc_ecma_ast", + "swc_ecma_visit", + "unicode-xid", +] + +[[package]] +name = "swc_ecma_visit" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3d60b9dc97ae4f181d4d60f43142d8ac9669953db410bcedefb29a14627e19" +dependencies = [ + "num-bigint", + "swc_atoms", + "swc_common", + "swc_ecma_ast", + "swc_visit", +] + +[[package]] +name = "swc_ecmascript" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ffb53afe008c15d4dc4957e80148c4b457659f93e4d4e8736eaeae352e48ec8" +dependencies = [ + "swc_ecma_ast", + "swc_ecma_parser", +] + +[[package]] +name = "swc_eq_ignore_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c8f200a2eaed938e7c1a685faaa66e6d42fa9e17da5f62572d3cbc335898f5e" +dependencies = [ + "pmutil", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "swc_macros_common" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08ed2e930f5a1a4071fe62c90fd3a296f6030e5d94bfe13993244423caf59a78" +dependencies = [ + "pmutil", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "swc_visit" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a423caa0b4585118164dbad8f1ad52b592a9a9370b25decc4d84c6b4309132c0" +dependencies = [ + "either", + "swc_visit_macros", +] + +[[package]] +name = "swc_visit_macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3b2825fee79f10d0166e8e650e79c7a862fb991db275743083f07555d7641f0" +dependencies = [ + "Inflector", + "pmutil", + "proc-macro2", + "quote", + "swc_macros_common", + "syn", +] + [[package]] name = "syn" version = "1.0.65" @@ -797,6 +1359,28 @@ dependencies = [ "serde", ] +[[package]] +name = "ts-rs" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "369e48de67506679b3a576b0faf666fa9f9acf2fd00b4c61e28bdb6c8e08ec06" +dependencies = [ + "dprint-plugin-typescript", + "ts-rs-macros", +] + +[[package]] +name = "ts-rs-macros" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f269e8fd28e26b4cdbd01f81f345aaf666131511e54a735a76a614b5062d0a5a" +dependencies = [ + "Inflector", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "typenum" version = "1.13.0" @@ -839,6 +1423,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-width" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" + [[package]] name = "unicode-xid" version = "0.2.1" diff --git a/contracts/mixnet/src/contract.rs b/contracts/mixnet/src/contract.rs index 88b76a96bf..14b78993c1 100644 --- a/contracts/mixnet/src/contract.rs +++ b/contracts/mixnet/src/contract.rs @@ -10,9 +10,9 @@ use cosmwasm_std::{ entry_point, to_binary, Addr, Decimal, Deps, DepsMut, Env, MessageInfo, QueryResponse, Response, Uint128, }; -use mixnet_contract::{ - ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, RawDelegationData, StateParams, -}; +use cosmwasm_storage::singleton_read; +use mixnet_contract::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, StateParams}; +use serde::{Deserialize, Serialize}; pub const INITIAL_DEFAULT_EPOCH_LENGTH: u32 = 2; @@ -29,6 +29,7 @@ pub const INITIAL_MIXNODE_DELEGATION_REWARD_RATE: u64 = 110; pub const INITIAL_GATEWAY_DELEGATION_REWARD_RATE: u64 = 110; pub const INITIAL_MIXNODE_ACTIVE_SET_SIZE: u32 = 100; +pub const INITIAL_GATEWAY_ACTIVE_SET_SIZE: u32 = 20; fn default_initial_state(owner: Addr) -> State { let mixnode_bond_reward_rate = Decimal::percent(INITIAL_MIXNODE_BOND_REWARD_RATE); @@ -48,6 +49,7 @@ fn default_initial_state(owner: Addr) -> State { mixnode_delegation_reward_rate, gateway_delegation_reward_rate, mixnode_active_set_size: INITIAL_MIXNODE_ACTIVE_SET_SIZE, + gateway_active_set_size: INITIAL_GATEWAY_ACTIVE_SET_SIZE, }, mixnode_epoch_bond_reward: calculate_epoch_reward_rate( INITIAL_DEFAULT_EPOCH_LENGTH, @@ -207,69 +209,54 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> Result Result { - use crate::storage::{ - gateway_delegations, gateway_delegations_read, gateway_delegations_read_old, gateways_read, - mix_delegations, mix_delegations_read, mix_delegations_read_old, mixnodes_read, +pub fn migrate(deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result { + #[derive(Serialize, Deserialize)] + struct OldStateParams { + epoch_length: u32, + minimum_mixnode_bond: Uint128, + minimum_gateway_bond: Uint128, + mixnode_bond_reward_rate: Decimal, + gateway_bond_reward_rate: Decimal, + mixnode_delegation_reward_rate: Decimal, + gateway_delegation_reward_rate: Decimal, + mixnode_active_set_size: u32, + } + + // adding gateways active set + #[derive(Serialize, Deserialize)] + struct OldState { + owner: Addr, + network_monitor_address: Addr, + params: OldStateParams, + mixnode_epoch_bond_reward: Decimal, + gateway_epoch_bond_reward: Decimal, + mixnode_epoch_delegation_reward: Decimal, + gateway_epoch_delegation_reward: Decimal, + } + + let old_state: OldState = singleton_read(deps.storage, b"config").load()?; + + let new_state = State { + owner: old_state.owner, + network_monitor_address: old_state.network_monitor_address, + params: StateParams { + epoch_length: old_state.params.epoch_length, + minimum_mixnode_bond: old_state.params.minimum_mixnode_bond, + minimum_gateway_bond: old_state.params.minimum_gateway_bond, + mixnode_bond_reward_rate: old_state.params.mixnode_bond_reward_rate, + gateway_bond_reward_rate: old_state.params.gateway_bond_reward_rate, + mixnode_delegation_reward_rate: old_state.params.mixnode_delegation_reward_rate, + gateway_delegation_reward_rate: old_state.params.gateway_delegation_reward_rate, + mixnode_active_set_size: old_state.params.mixnode_active_set_size, + gateway_active_set_size: INITIAL_GATEWAY_ACTIVE_SET_SIZE, + }, + mixnode_epoch_bond_reward: old_state.mixnode_epoch_bond_reward, + gateway_epoch_bond_reward: old_state.gateway_epoch_bond_reward, + mixnode_epoch_delegation_reward: old_state.mixnode_epoch_delegation_reward, + gateway_epoch_delegation_reward: old_state.gateway_epoch_delegation_reward, }; - use crate::transactions::delegations; - use cosmwasm_std::{Order, StdResult}; - use mixnet_contract::{GatewayBond, MixNodeBond}; - // Read existing delegations data, drop invalid values, and rewrite delegations data with valid data only - fn overwrite_mixnode_delegations_data( - identity: &str, - deps: &mut DepsMut, - ) -> Result<(), ContractError> { - let delegations_bucket = mix_delegations_read(deps.storage, identity); - let old_delegations_bucket = mix_delegations_read_old(deps.storage, identity); - let mut delegations_vec = delegations(delegations_bucket)?; - let old_delegations = delegations::(old_delegations_bucket)?; - for delegation in old_delegations { - delegations_vec.push((delegation.0, RawDelegationData::new(delegation.1, 1))) - } - - for (key, delegation) in delegations_vec { - mix_delegations(deps.storage, identity).save(&key, &delegation)?; - } - Ok(()) - } - - fn overwrite_gateway_delegations_data( - identity: &str, - deps: &mut DepsMut, - ) -> Result<(), ContractError> { - let delegations_bucket = gateway_delegations_read(deps.storage, identity); - let old_delegations_bucket = gateway_delegations_read_old(deps.storage, identity); - let mut delegations_vec = delegations(delegations_bucket)?; - let old_delegations = delegations::(old_delegations_bucket)?; - for delegation in old_delegations { - delegations_vec.push((delegation.0, RawDelegationData::new(delegation.1, 1))) - } - - for (key, delegation) in delegations_vec { - gateway_delegations(deps.storage, identity).save(&key, &delegation)?; - } - Ok(()) - } - - let mixnet_bonds = mixnodes_read(deps.storage) - .range(None, None, Order::Ascending) - .map(|res| res.map(|item| item.1)) - .collect::>>()?; - - for bond in mixnet_bonds { - overwrite_mixnode_delegations_data(bond.identity(), &mut deps)?; - } - - let gateway_bonds = gateways_read(deps.storage) - .range(None, None, Order::Ascending) - .map(|res| res.map(|item| item.1)) - .collect::>>()?; - - for bond in gateway_bonds { - overwrite_gateway_delegations_data(bond.identity(), &mut deps)?; - } + config(deps.storage).save(&new_state)?; Ok(Default::default()) } diff --git a/contracts/mixnet/src/queries.rs b/contracts/mixnet/src/queries.rs index a9643a372e..96b6f93fe8 100644 --- a/contracts/mixnet/src/queries.rs +++ b/contracts/mixnet/src/queries.rs @@ -643,6 +643,7 @@ mod tests { mixnode_delegation_reward_rate: "7.89".parse().unwrap(), gateway_delegation_reward_rate: "0.12".parse().unwrap(), mixnode_active_set_size: 1000, + gateway_active_set_size: 20, }, mixnode_epoch_bond_reward: "1.23".parse().unwrap(), gateway_epoch_bond_reward: "4.56".parse().unwrap(), diff --git a/contracts/mixnet/src/storage.rs b/contracts/mixnet/src/storage.rs index 43f1398d89..bb7b514c9e 100644 --- a/contracts/mixnet/src/storage.rs +++ b/contracts/mixnet/src/storage.rs @@ -318,14 +318,6 @@ pub fn mix_delegations_read<'a>( ReadonlyBucket::multilevel(storage, &[PREFIX_MIX_DELEGATION, mix_identity.as_bytes()]) } -// https://github.com/nymtech/nym/blob/122f5d9f2e5c1ced96e3b9ba0c74ef8b7dbde2c7/contracts/mixnet/src/storage.rs -pub fn mix_delegations_read_old<'a>( - storage: &'a dyn Storage, - mix_identity: IdentityKeyRef, -) -> ReadonlyBucket<'a, Uint128> { - ReadonlyBucket::multilevel(storage, &[PREFIX_MIX_DELEGATION, mix_identity.as_bytes()]) -} - pub fn reverse_mix_delegations<'a>(storage: &'a mut dyn Storage, owner: &Addr) -> Bucket<'a, ()> { Bucket::multilevel(storage, &[PREFIX_REVERSE_MIX_DELEGATION, owner.as_bytes()]) } @@ -357,16 +349,6 @@ pub fn gateway_delegations_read<'a>( ) } -pub fn gateway_delegations_read_old<'a>( - storage: &'a dyn Storage, - gateway_identity: IdentityKeyRef, -) -> ReadonlyBucket<'a, Uint128> { - ReadonlyBucket::multilevel( - storage, - &[PREFIX_GATEWAY_DELEGATION, gateway_identity.as_bytes()], - ) -} - pub fn reverse_gateway_delegations<'a>( storage: &'a mut dyn Storage, owner: &Addr, diff --git a/contracts/mixnet/src/transactions.rs b/contracts/mixnet/src/transactions.rs index 74707e4e4d..4a0660897a 100644 --- a/contracts/mixnet/src/transactions.rs +++ b/contracts/mixnet/src/transactions.rs @@ -1658,6 +1658,7 @@ pub mod tests { INITIAL_GATEWAY_DELEGATION_REWARD_RATE, ), mixnode_active_set_size: 42, // change something + gateway_active_set_size: 5, }; // cannot be updated from non-owner account diff --git a/tauri-wallet/src-tauri/src/operations/admin.rs b/tauri-wallet/src-tauri/src/operations/admin.rs index 8e4f832936..2d3ad52ae7 100644 --- a/tauri-wallet/src-tauri/src/operations/admin.rs +++ b/tauri-wallet/src-tauri/src/operations/admin.rs @@ -20,6 +20,7 @@ pub struct TauriStateParams { mixnode_delegation_reward_rate: String, gateway_delegation_reward_rate: String, mixnode_active_set_size: u32, + gateway_active_set_size: u32, } impl From for TauriStateParams { @@ -33,6 +34,7 @@ impl From for TauriStateParams { mixnode_delegation_reward_rate: p.mixnode_delegation_reward_rate.to_string(), gateway_delegation_reward_rate: p.gateway_delegation_reward_rate.to_string(), mixnode_active_set_size: p.mixnode_active_set_size, + gateway_active_set_size: p.gateway_active_set_size, } } } @@ -50,6 +52,7 @@ impl TryFrom for StateParams { mixnode_delegation_reward_rate: Decimal::from_str(p.mixnode_delegation_reward_rate.as_str())?, gateway_delegation_reward_rate: Decimal::from_str(p.gateway_delegation_reward_rate.as_str())?, mixnode_active_set_size: p.mixnode_active_set_size, + gateway_active_set_size: p.gateway_active_set_size, }) } } diff --git a/tauri-wallet/src/types/rust/stateparams.ts b/tauri-wallet/src/types/rust/stateparams.ts index f0c18aa83e..b7dbabc531 100644 --- a/tauri-wallet/src/types/rust/stateparams.ts +++ b/tauri-wallet/src/types/rust/stateparams.ts @@ -7,4 +7,5 @@ export interface TauriStateParams { mixnode_delegation_reward_rate: string; gateway_delegation_reward_rate: string; mixnode_active_set_size: number; + gateway_active_set_size: number; } \ No newline at end of file diff --git a/validator-api/src/cache/mod.rs b/validator-api/src/cache/mod.rs index 16d94c1132..fbfa221e35 100644 --- a/validator-api/src/cache/mod.rs +++ b/validator-api/src/cache/mod.rs @@ -4,10 +4,10 @@ use crate::nymd_client::Client; use anyhow::Result; use config::defaults::VALIDATOR_API_VERSION; -use mixnet_contract::{GatewayBond, MixNodeBond}; +use mixnet_contract::{GatewayBond, MixNodeBond, StateParams}; use rocket::fairing::AdHoc; use serde::Serialize; -use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::Arc; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use tokio::sync::RwLock; @@ -31,6 +31,12 @@ struct ValidatorCacheInner { initialised: AtomicBool, mixnodes: RwLock>>, gateways: RwLock>>, + + active_mixnodes_available: AtomicBool, + active_gateways_available: AtomicBool, + + current_mixnode_active_set_size: AtomicUsize, + current_gateway_active_set_size: AtomicUsize, } #[derive(Default, Serialize, Clone)] @@ -75,13 +81,17 @@ impl ValidatorCacheRefresher { self.nymd_client.get_gateways() )?; + let state_params = self.nymd_client.get_state_params().await?; + info!( "Updating validator cache. There are {} mixnodes and {} gateways", mixnodes.len(), gateways.len() ); - self.cache.update_cache(mixnodes, gateways).await; + self.cache + .update_cache(mixnodes, gateways, state_params) + .await; Ok(()) } @@ -117,12 +127,95 @@ impl ValidatorCache { rocket.manage(Self::new()).mount( // this format! is so ugly... format!("/{}", VALIDATOR_API_VERSION), - routes![routes::get_mixnodes, routes::get_gateways], + routes![ + routes::get_mixnodes, + routes::get_gateways, + routes::get_active_mixnodes, + routes::get_active_gateways + ], ) }) } - async fn update_cache(&self, mixnodes: Vec, gateways: Vec) { + // TODO: check if all nodes can be compared together, + // i.e. they all have the same denom for bonds and delegations + fn verify_mixnodes(&self, mixnodes: &[MixNodeBond]) -> bool { + if mixnodes.is_empty() { + return true; + } + let expected_denom = &mixnodes[0].bond_amount.denom; + for mixnode in mixnodes { + if &mixnode.bond_amount.denom != expected_denom + || &mixnode.total_delegation.denom != expected_denom + { + return false; + } + } + + true + } + + // TODO: check if all nodes can be compared together, + // i.e. they all have the same denom for bonds and delegations + fn verify_gateways(&self, gateways: &[GatewayBond]) -> bool { + if gateways.is_empty() { + return true; + } + let expected_denom = &gateways[0].bond_amount.denom; + for gateway in gateways { + if &gateway.bond_amount.denom != expected_denom + || &gateway.total_delegation.denom != expected_denom + { + return false; + } + } + + false + } + + async fn update_cache( + &self, + mut mixnodes: Vec, + mut gateways: Vec, + state: StateParams, + ) { + // if our data is valid, it means the active sets are available, + // otherwise we must explicitly indicate nobody can read this data + + if self.verify_mixnodes(&mixnodes) { + // partial_cmp can only fail if the nodes have different denomination, + // but we just checked for that hence the unwraps are fine here + // Note the reverse order of comparison so that the "highest" node would be first + mixnodes.sort_by(|a, b| b.partial_cmp(a).unwrap()); + self.inner + .active_mixnodes_available + .store(true, Ordering::SeqCst); + self.inner + .current_mixnode_active_set_size + .store(state.mixnode_active_set_size as usize, Ordering::SeqCst); + } else { + self.inner + .active_mixnodes_available + .store(false, Ordering::SeqCst); + } + + if self.verify_gateways(&gateways) { + // partial_cmp can only fail if the nodes have different denomination, + // but we just checked for that hence the unwraps are fine here + // Note the reverse order of comparison so that the "highest" node would be first + gateways.sort_by(|a, b| b.partial_cmp(a).unwrap()); + self.inner + .active_gateways_available + .store(true, Ordering::SeqCst); + self.inner + .current_gateway_active_set_size + .store(state.gateway_active_set_size as usize, Ordering::SeqCst); + } else { + self.inner + .active_gateways_available + .store(false, Ordering::SeqCst); + } + self.inner.mixnodes.write().await.set(mixnodes); self.inner.gateways.write().await.set(gateways); } @@ -135,6 +228,54 @@ impl ValidatorCache { self.inner.gateways.read().await.clone() } + pub async fn active_mixnodes(&self) -> Option>> { + // if active set is available, it means it is already sorted + if self.inner.active_mixnodes_available.load(Ordering::SeqCst) { + let cache = self.inner.mixnodes.read().await; + let timestamp = cache.as_at; + let nodes = cache + .value + .iter() + .take( + self.inner + .current_mixnode_active_set_size + .load(Ordering::SeqCst), + ) + .cloned() + .collect(); + Some(Cache { + value: nodes, + as_at: timestamp, + }) + } else { + None + } + } + + pub async fn active_gateways(&self) -> Option>> { + // if active set is available, it means it is already sorted + if self.inner.active_gateways_available.load(Ordering::SeqCst) { + let cache = self.inner.gateways.read().await; + let timestamp = cache.as_at; + let nodes = cache + .value + .iter() + .take( + self.inner + .current_gateway_active_set_size + .load(Ordering::SeqCst), + ) + .cloned() + .collect(); + Some(Cache { + value: nodes, + as_at: timestamp, + }) + } else { + None + } + } + pub fn initialised(&self) -> bool { self.inner.initialised.load(Ordering::Relaxed) } @@ -158,6 +299,10 @@ impl ValidatorCacheInner { initialised: AtomicBool::new(false), mixnodes: RwLock::new(Cache::default()), gateways: RwLock::new(Cache::default()), + active_mixnodes_available: AtomicBool::new(false), + active_gateways_available: AtomicBool::new(false), + current_mixnode_active_set_size: Default::default(), + current_gateway_active_set_size: Default::default(), } } } diff --git a/validator-api/src/cache/routes.rs b/validator-api/src/cache/routes.rs index 2b947a3e7f..a076aac4ac 100644 --- a/validator-api/src/cache/routes.rs +++ b/validator-api/src/cache/routes.rs @@ -15,3 +15,17 @@ pub(crate) async fn get_mixnodes(cache: &State) -> Json) -> Json> { Json(cache.gateways().await.value) } + +#[get("/mixnodes/active")] +pub(crate) async fn get_active_mixnodes( + cache: &State, +) -> Option>> { + cache.active_mixnodes().await.map(|cache| Json(cache.value)) +} + +#[get("/gateways/active")] +pub(crate) async fn get_active_gateways( + cache: &State, +) -> Option>> { + cache.active_gateways().await.map(|cache| Json(cache.value)) +} diff --git a/validator-api/src/nymd_client.rs b/validator-api/src/nymd_client.rs index 2a76d854ff..f836d27358 100644 --- a/validator-api/src/nymd_client.rs +++ b/validator-api/src/nymd_client.rs @@ -8,7 +8,7 @@ use crate::rewarding::{ PER_MIXNODE_DELEGATION_GAS_INCREASE, REWARDING_GAS_LIMIT_MULTIPLIER, }; use config::defaults::DEFAULT_VALIDATOR_API_PORT; -use mixnet_contract::{Delegation, ExecuteMsg, GatewayBond, IdentityKey, MixNodeBond}; +use mixnet_contract::{Delegation, ExecuteMsg, GatewayBond, IdentityKey, MixNodeBond, StateParams}; use std::sync::Arc; use tokio::sync::RwLock; use validator_client::nymd::{ @@ -106,6 +106,13 @@ impl Client { self.0.read().await.get_all_nymd_gateways().await } + pub(crate) async fn get_state_params(&self) -> Result + where + C: CosmWasmClient + Sync, + { + self.0.read().await.get_state_params().await + } + pub(crate) async fn get_mixnode_delegations( &self, identity: IdentityKey, diff --git a/validator-api/src/rewarding/mod.rs b/validator-api/src/rewarding/mod.rs index 4e0d6cd210..e5030567fe 100644 --- a/validator-api/src/rewarding/mod.rs +++ b/validator-api/src/rewarding/mod.rs @@ -170,7 +170,7 @@ impl Rewarder { /// Queries the smart contract in order to obtain the current list of bonded mixnodes and then /// for each mixnode determines how many delegators it has. - async fn produce_mixnode_delegators_map( + async fn produce_active_mixnode_delegators_map( &self, ) -> Result, RewardingError> { // Technically we could optimise it by creating a concurrent stream and executing multiple @@ -189,8 +189,13 @@ impl Rewarder { // instantaneous. let mut map = HashMap::new(); - let bonded_mixnodes = self.validator_cache.mixnodes().await.into_inner(); - for mix in bonded_mixnodes.into_iter() { + let active_bonded_mixnodes = self + .validator_cache + .active_mixnodes() + .await + .ok_or(RewardingError::NoMixnodesToReward)? + .into_inner(); + for mix in active_bonded_mixnodes.into_iter() { let delegator_count = self .get_mixnode_delegators_count(mix.mix_node.identity_key.clone()) .await?; @@ -202,14 +207,19 @@ impl Rewarder { /// Queries the smart contract in order to obtain the current list of bonded gateways and then /// for each gateway determines how many delegators it has. - async fn produce_gateway_delegators_map( + async fn produce_active_gateway_delegators_map( &self, ) -> Result, RewardingError> { // look at comments in `produce_mixnode_delegators_map` for some optimisation elaboration let mut map = HashMap::new(); - let bonded_gateways = self.validator_cache.gateways().await.into_inner(); - for gateway in bonded_gateways.into_iter() { + let active_bonded_gateways = self + .validator_cache + .active_gateways() + .await + .ok_or(RewardingError::NoGatewaysToReward)? + .into_inner(); + for gateway in active_bonded_gateways.into_iter() { let delegator_count = self .get_gateway_delegators_count(gateway.gateway.identity_key.clone()) .await?; @@ -237,8 +247,8 @@ impl Rewarder { /// Given the list of mixnodes that were tested in the last epoch, tries to determine the /// subset that are eligible for any rewards. /// - /// As of right now, it is a rather straightforward process. It is only checked whether the node - /// is currently bonded and has uptime > 0. + /// As of right now, it is a rather straightforward process. It is checked whether the node + /// is currently bonded, has uptime > 0 and is part of the "active" set. /// Unlike the typescript rewards script, it currently does not look at the verloc data nor /// whether the non-mixing ports are open. /// @@ -259,10 +269,10 @@ impl Rewarder { // and the lack of port data / verloc data will eventually be balanced out anyway // by people hesitating to delegate to nodes without them and thus those nodes disappearing // from the active set (once introduced) - let mixnode_delegators = self.produce_mixnode_delegators_map().await?; + let mixnode_delegators = self.produce_active_mixnode_delegators_map().await?; // 1. go through all active mixnodes - // 2. filter out nodes that are currently not bonded (as `mixnode_delegators` was obtained by + // 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 @@ -286,8 +296,8 @@ impl Rewarder { /// Given the list of gateways that were tested in the last epoch, tries to determine the /// subset that are eligible for any rewards. /// - /// As of right now, it is a rather straightforward process. It is only checked whether the node - /// is currently bonded and has uptime > 0. + /// As of right now, it is a rather straightforward process. It is checked whether the node + /// is currently bonded, has uptime > 0 and is part of the "active" set. /// Unlike the typescript rewards script, it currently does not look at the non-mixing ports are open. /// /// The method also obtains the number of delegators towards the node in order to more accurately @@ -301,7 +311,7 @@ impl Rewarder { &self, active_gateways: &[GatewayStatusReport], ) -> Result, RewardingError> { - let gateway_delegators = self.produce_gateway_delegators_map().await?; + let gateway_delegators = self.produce_active_gateway_delegators_map().await?; let eligible_nodes = active_gateways .iter() @@ -329,7 +339,7 @@ impl Rewarder { /// # Arguments /// /// * `epoch`: the specified epoch. - async fn get_active_nodes( + async fn get_active_monitor_nodes( &self, epoch: Epoch, ) -> Result<(Vec, Vec), RewardingError> { @@ -450,25 +460,29 @@ impl Rewarder { /// /// * `epoch_rewarding_id`: id of the current epoch rewarding. /// - /// * `active_mixnodes`: list of the nodes that were tested at least once by the network monitor - /// in the last epoch. + /// * `active_monitor_mixnodes`: list of the nodes that were tested at least once by the network monitor + /// in the last epoch. /// - /// * `active_gateways`: list of the nodes that were tested at least once by the network monitor - /// in the last epoch. + /// * `active_monitor_gateways`: list of the nodes that were tested at least once by the network monitor + /// in the last epoch. async fn distribute_rewards( &self, epoch_rewarding_id: i64, - active_mixnodes: &[MixnodeStatusReport], - active_gateways: &[GatewayStatusReport], + active_monitor_mixnodes: &[MixnodeStatusReport], + active_monitor_gateways: &[GatewayStatusReport], ) -> Result<(RewardingReport, Option), RewardingError> { let mut failure_data = FailureData::default(); - let eligible_mixnodes = self.determine_eligible_mixnodes(active_mixnodes).await?; + let eligible_mixnodes = self + .determine_eligible_mixnodes(active_monitor_mixnodes) + .await?; if eligible_mixnodes.is_empty() { return Err(RewardingError::NoMixnodesToReward); } - let eligible_gateways = self.determine_eligible_gateways(active_gateways).await?; + let eligible_gateways = self + .determine_eligible_gateways(active_monitor_gateways) + .await?; if eligible_gateways.is_empty() { return Err(RewardingError::NoGatewaysToReward); } @@ -718,7 +732,8 @@ impl Rewarder { ); // get nodes that were active during the epoch - let (active_mixnodes, active_gateways) = self.get_active_nodes(epoch).await?; + let (active_monitor_mixnodes, active_monitor_gateways) = + self.get_active_monitor_nodes(epoch).await?; // insert information about beginning the procedure (so that if we crash during it, // we wouldn't attempt to possibly double reward operators) @@ -728,7 +743,11 @@ impl Rewarder { .await?; let (report, failure_data) = self - .distribute_rewards(epoch_rewarding_id, &active_mixnodes, &active_gateways) + .distribute_rewards( + epoch_rewarding_id, + &active_monitor_mixnodes, + &active_monitor_gateways, + ) .await?; self.storage @@ -764,7 +783,11 @@ impl Rewarder { "Updating historical daily uptimes of all nodes and purging old status reports..." ); self.storage - .update_historical_uptimes(&epoch_iso_8601, &active_mixnodes, &active_gateways) + .update_historical_uptimes( + &epoch_iso_8601, + &active_monitor_mixnodes, + &active_monitor_gateways, + ) .await?; self.storage.purge_old_statuses(two_days_ago).await?; } diff --git a/validator-api/src/storage/mod.rs b/validator-api/src/storage/mod.rs index fd394a1ce7..64cfac2487 100644 --- a/validator-api/src/storage/mod.rs +++ b/validator-api/src/storage/mod.rs @@ -443,7 +443,7 @@ impl NodeStatusStorage { Ok(run_count as usize) } - /// Given lists of reports of all active mixnodes and gateways, inserts the data into the + /// Given lists of reports of all monitor-active mixnodes and gateways, inserts the data into the /// historical uptime tables. /// /// This method is called at every reward cycle. Note that currently to work as expected, it @@ -453,8 +453,8 @@ impl NodeStatusStorage { /// # Arguments /// /// * `today_iso_8601`: today's date expressed in ISO 8601, i.e. YYYY-MM-DD - /// * `mixnode_reports`: slice of reports for all active mixnodes - /// * `gateway_reports`: slice of reports for all active gateways + /// * `mixnode_reports`: slice of reports for all monitor-active mixnodes + /// * `gateway_reports`: slice of reports for all monitor-active gateways pub(crate) async fn update_historical_uptimes( &self, today_iso_8601: &str, diff --git a/wallet-web/components/admin/AdminForm.tsx b/wallet-web/components/admin/AdminForm.tsx index fa0289ab34..6f20adef5e 100644 --- a/wallet-web/components/admin/AdminForm.tsx +++ b/wallet-web/components/admin/AdminForm.tsx @@ -102,13 +102,23 @@ export default function AdminForm(props: AdminFormProps) { + + +