diff --git a/Cargo.lock b/Cargo.lock index c67a8649c3..21af33af57 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3498,6 +3498,7 @@ dependencies = [ "serde", "serde_repr", "thiserror", + "time 0.3.5", "ts-rs", ] @@ -3624,7 +3625,7 @@ dependencies = [ "cfg-if 1.0.0", "hex-literal", "serde", - "time 0.3.3", + "time 0.3.5", "url", ] @@ -3972,7 +3973,6 @@ dependencies = [ "pretty_env_logger", "rand 0.7.3", "rand 0.8.4", - "rand_chacha 0.3.1", "reqwest", "rocket", "rocket_cors", @@ -3981,7 +3981,7 @@ dependencies = [ "serde_json", "sqlx", "thiserror", - "time 0.3.3", + "time 0.3.5", "tokio", "topology", "url", @@ -6887,7 +6887,7 @@ dependencies = [ "subtle 2.4.1", "subtle-encoding", "tendermint-proto", - "time 0.3.3", + "time 0.3.5", "zeroize", ] @@ -6920,7 +6920,7 @@ dependencies = [ "serde", "serde_bytes", "subtle-encoding", - "time 0.3.3", + "time 0.3.5", ] [[package]] @@ -6948,7 +6948,7 @@ dependencies = [ "tendermint-config", "tendermint-proto", "thiserror", - "time 0.3.3", + "time 0.3.5", "tokio", "tracing", "url", @@ -7048,9 +7048,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.3" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cde1cf55178e0293453ba2cca0d5f8392a922e52aa958aee9c28ed02becc6d03" +checksum = "41effe7cfa8af36f439fac33861b66b049edc6f9a32331e2312660529c1c24ad" dependencies = [ "itoa", "libc", diff --git a/clients/validator/src/index.ts b/clients/validator/src/index.ts index 76b475b8d3..5ed0cde383 100644 --- a/clients/validator/src/index.ts +++ b/clients/validator/src/index.ts @@ -180,8 +180,8 @@ export default class ValidatorClient implements INymClient { return this.client.getSybilResistancePercent(this.mixnetContract); } - public async getEpochRewardPercent(): Promise { - return this.client.getEpochRewardPercent(this.mixnetContract); + public async getIntervalRewardPercent(): Promise { + return this.client.getIntervalRewardPercent(this.mixnetContract); } public async getAllNymdMixnodes(): Promise { diff --git a/clients/validator/src/nymd-querier.ts b/clients/validator/src/nymd-querier.ts index 0db3ac3657..a0bdc911d3 100644 --- a/clients/validator/src/nymd-querier.ts +++ b/clients/validator/src/nymd-querier.ts @@ -18,7 +18,6 @@ import { PagedGatewayResponse, PagedMixDelegationsResponse, PagedMixnodeResponse, - RewardingIntervalResponse, RewardingStatus, } from './types'; @@ -79,12 +78,6 @@ export default class NymdQuerier implements INymdQuery { }); } - getCurrentRewardingInterval(mixnetContractAddress: string): Promise { - return this.client.queryContractSmart(mixnetContractAddress, { - current_rewarding_interval: {}, - }); - } - getAllNetworkDelegationsPaged( mixnetContractAddress: string, limit?: number, @@ -155,9 +148,9 @@ export default class NymdQuerier implements INymdQuery { }); } - getEpochRewardPercent(mixnetContractAddress: string): Promise { + getIntervalRewardPercent(mixnetContractAddress: string): Promise { return this.client.queryContractSmart(mixnetContractAddress, { - get_epoch_reward_percent: {}, + get_interval_reward_percent: {}, }); } diff --git a/clients/validator/src/query-client.ts b/clients/validator/src/query-client.ts index 49f4c127c2..dd423b1d4b 100644 --- a/clients/validator/src/query-client.ts +++ b/clients/validator/src/query-client.ts @@ -28,7 +28,6 @@ import { PagedGatewayResponse, PagedMixDelegationsResponse, PagedMixnodeResponse, - RewardingIntervalResponse, RewardingStatus, } from './types'; import ValidatorApiQuerier, { IValidatorApiQuery } from './validator-api-querier'; @@ -63,7 +62,6 @@ export interface INymdQuery { ownsMixNode(mixnetContractAddress: string, address: string): Promise; ownsGateway(mixnetContractAddress: string, address: string): Promise; getStateParams(mixnetContractAddress: string): Promise; - getCurrentRewardingInterval(mixnetContractAddress: string): Promise; getAllNetworkDelegationsPaged( mixnetContractAddress: string, @@ -87,7 +85,7 @@ export interface INymdQuery { getLayerDistribution(mixnetContractAddress: string): Promise; getRewardPool(mixnetContractAddress: string): Promise; getCirculatingSupply(mixnetContractAddress: string): Promise; - getEpochRewardPercent(mixnetContractAddress: string): Promise; + getIntervalRewardPercent(mixnetContractAddress: string): Promise; getSybilResistancePercent(mixnetContractAddress: string): Promise; getRewardingStatus( mixnetContractAddress: string, @@ -138,10 +136,6 @@ export default class QueryClient extends CosmWasmClient implements IQueryClient return this.nymdQuerier.getStateParams(mixnetContractAddress); } - getCurrentRewardingInterval(mixnetContractAddress: string): Promise { - return this.nymdQuerier.getCurrentRewardingInterval(mixnetContractAddress); - } - getAllNetworkDelegationsPaged( mixnetContractAddress: string, limit?: number, @@ -184,8 +178,8 @@ export default class QueryClient extends CosmWasmClient implements IQueryClient return this.nymdQuerier.getCirculatingSupply(mixnetContractAddress); } - getEpochRewardPercent(mixnetContractAddress: string): Promise { - return this.nymdQuerier.getEpochRewardPercent(mixnetContractAddress); + getIntervalRewardPercent(mixnetContractAddress: string): Promise { + return this.nymdQuerier.getIntervalRewardPercent(mixnetContractAddress); } getSybilResistancePercent(mixnetContractAddress: string): Promise { diff --git a/clients/validator/src/signing-client.ts b/clients/validator/src/signing-client.ts index 8b027e9480..5c3551f69f 100644 --- a/clients/validator/src/signing-client.ts +++ b/clients/validator/src/signing-client.ts @@ -31,7 +31,6 @@ import { PagedGatewayResponse, PagedMixDelegationsResponse, PagedMixnodeResponse, - RewardingIntervalResponse, RewardingStatus, } from './types'; import ValidatorApiQuerier from './validator-api-querier'; @@ -257,10 +256,6 @@ export default class SigningClient extends SigningCosmWasmClient implements ISig return this.nymdQuerier.getStateParams(mixnetContractAddress); } - getCurrentRewardingInterval(mixnetContractAddress: string): Promise { - return this.nymdQuerier.getCurrentRewardingInterval(mixnetContractAddress); - } - getAllNetworkDelegationsPaged( mixnetContractAddress: string, limit?: number, @@ -303,8 +298,8 @@ export default class SigningClient extends SigningCosmWasmClient implements ISig return this.nymdQuerier.getCirculatingSupply(mixnetContractAddress); } - getEpochRewardPercent(mixnetContractAddress: string): Promise { - return this.nymdQuerier.getEpochRewardPercent(mixnetContractAddress); + getIntervalRewardPercent(mixnetContractAddress: string): Promise { + return this.nymdQuerier.getIntervalRewardPercent(mixnetContractAddress); } getSybilResistancePercent(mixnetContractAddress: string): Promise { diff --git a/clients/validator/src/types.ts b/clients/validator/src/types.ts index 786b72ff6c..575702ec45 100644 --- a/clients/validator/src/types.ts +++ b/clients/validator/src/types.ts @@ -43,12 +43,6 @@ export type ContractStateParams = { mixnode_active_set_size: number; }; -export type RewardingIntervalResponse = { - current_rewarding_interval_starting_block: number; - current_rewarding_interval_nonce: number; - rewarding_in_progress: boolean; -}; - export type LayerDistribution = { gateways: number; layer1: number; diff --git a/common/client-libs/validator-client/Cargo.toml b/common/client-libs/validator-client/Cargo.toml index e5f831efbe..826366256d 100644 --- a/common/client-libs/validator-client/Cargo.toml +++ b/common/client-libs/validator-client/Cargo.toml @@ -10,10 +10,10 @@ rust-version = "1.56" [dependencies] base64 = "0.13" mixnet-contract-common = { path= "../../cosmwasm-smart-contracts/mixnet-contract" } -vesting-contract = { path="../../../contracts/vesting" } -serde = { version="1", features=["derive"] } +vesting-contract = { path = "../../../contracts/vesting" } +serde = { version = "1", features = ["derive"] } serde_json = "1" -reqwest = { version="0.11", features=["json"] } +reqwest = { version = "0.11", features = ["json"] } thiserror = "1" log = "0.4" url = { version = "2.2", features = ["serde"] } @@ -28,14 +28,28 @@ validator-api-requests = { path = "../../../validator-api/validator-api-requests async-trait = { version = "0.1.51", optional = true } bip39 = { version = "1", features = ["rand"], optional = true } config = { path = "../../config", optional = true } -cosmrs = { version = "0.4.1", features = ["rpc", "bip32", "cosmwasm"], optional = true } +cosmrs = { version = "0.4.1", features = [ + "rpc", + "bip32", + "cosmwasm", +], optional = true } prost = { version = "0.9", default-features = false, optional = true } flate2 = { version = "1.0.20", optional = true } sha2 = { version = "0.9.5", optional = true } itertools = { version = "0.10", optional = true } cosmwasm-std = { version = "1.0.0-beta3", optional = true } -ts-rs = {version = "5.1", optional = true} +ts-rs = { version = "5.1", optional = true } [features] -nymd-client = ["async-trait", "bip39", "config", "cosmrs", "prost", "flate2", "sha2", "itertools", "cosmwasm-std"] +nymd-client = [ + "async-trait", + "bip39", + "config", + "cosmrs", + "prost", + "flate2", + "sha2", + "itertools", + "cosmwasm-std", +] typescript-types = ["ts-rs", "validator-api-requests/ts-rs"] diff --git a/common/client-libs/validator-client/src/client.rs b/common/client-libs/validator-client/src/client.rs index cca670cfc5..9df13bc309 100644 --- a/common/client-libs/validator-client/src/client.rs +++ b/common/client-libs/validator-client/src/client.rs @@ -12,10 +12,14 @@ use crate::{validator_api, ValidatorClientError}; use coconut_interface::{BlindSignRequestBody, BlindedSignatureResponse, VerificationKeyResponse}; #[cfg(feature = "nymd-client")] use mixnet_contract_common::{ - Delegation, MixnetContractVersion, MixnodeRewardingStatusResponse, RewardingIntervalResponse, + Delegation, Interval, MixnetContractVersion, MixnodeRewardingStatusResponse, +}; +use mixnet_contract_common::{ + GatewayBond, IdentityKey, IdentityKeyRef, MixNodeBond, RewardedSetNodeStatus, + RewardedSetUpdateDetails, }; -use mixnet_contract_common::{GatewayBond, IdentityKeyRef, MixNodeBond}; +use std::collections::{HashMap, HashSet}; #[cfg(feature = "nymd-client")] use std::str::FromStr; use url::Url; @@ -35,6 +39,7 @@ pub struct Config { mixnode_page_limit: Option, gateway_page_limit: Option, mixnode_delegations_page_limit: Option, + rewarded_set_page_limit: Option, } #[cfg(feature = "nymd-client")] @@ -53,6 +58,7 @@ impl Config { mixnode_page_limit: None, gateway_page_limit: None, mixnode_delegations_page_limit: None, + rewarded_set_page_limit: None, } } @@ -70,6 +76,11 @@ impl Config { self.mixnode_delegations_page_limit = limit; self } + + pub fn with_rewarded_set_page_limit(mut self, limit: Option) -> Config { + self.rewarded_set_page_limit = limit; + self + } } #[cfg(feature = "nymd-client")] @@ -81,6 +92,7 @@ pub struct Client { mixnode_page_limit: Option, gateway_page_limit: Option, mixnode_delegations_page_limit: Option, + rewarded_set_page_limit: Option, // ideally they would have been read-only, but unfortunately rust doesn't have such features pub validator_api: validator_api::Client, @@ -109,6 +121,7 @@ impl Client { mixnode_page_limit: config.mixnode_page_limit, gateway_page_limit: config.gateway_page_limit, mixnode_delegations_page_limit: config.mixnode_delegations_page_limit, + rewarded_set_page_limit: None, validator_api: validator_api_client, nymd: nymd_client, }) @@ -149,6 +162,7 @@ impl Client { mixnode_page_limit: config.mixnode_page_limit, gateway_page_limit: config.gateway_page_limit, mixnode_delegations_page_limit: config.mixnode_delegations_page_limit, + rewarded_set_page_limit: config.rewarded_set_page_limit, validator_api: validator_api_client, nymd: nymd_client, }) @@ -214,15 +228,6 @@ impl Client { Ok(self.nymd.get_mixnet_contract_version().await?) } - pub async fn get_current_rewarding_interval( - &self, - ) -> Result - where - C: CosmWasmClient + Sync, - { - Ok(self.nymd.get_current_rewarding_interval().await?) - } - pub async fn get_rewarding_status( &self, mix_identity: mixnet_contract_common::IdentityKey, @@ -244,6 +249,13 @@ impl Client { Ok(self.nymd.get_reward_pool().await?.u128()) } + pub async fn get_current_interval(&self) -> Result + where + C: CosmWasmClient + Sync, + { + Ok(self.nymd.get_current_interval().await?) + } + pub async fn get_circulating_supply(&self) -> Result where C: CosmWasmClient + Sync, @@ -258,14 +270,129 @@ impl Client { Ok(self.nymd.get_sybil_resistance_percent().await?) } - pub async fn get_epoch_reward_percent(&self) -> Result + pub async fn get_interval_reward_percent(&self) -> Result where C: CosmWasmClient + Sync, { - Ok(self.nymd.get_epoch_reward_percent().await?) + Ok(self.nymd.get_interval_reward_percent().await?) + } + + pub async fn get_current_rewarded_set_update_details( + &self, + ) -> Result + where + C: CosmWasmClient + Sync, + { + Ok(self + .nymd + .query_current_rewarded_set_update_details() + .await?) } // basically handles paging for us + pub async fn get_all_nymd_rewarded_set_mixnode_identities( + &self, + ) -> Result, ValidatorClientError> + where + C: CosmWasmClient + Sync, + { + let mut identities = Vec::new(); + let mut start_after = None; + let mut height = None; + + loop { + let mut paged_response = self + .nymd + .get_rewarded_set_identities_paged( + start_after.take(), + self.rewarded_set_page_limit, + height, + ) + .await?; + identities.append(&mut paged_response.identities); + + if height.is_none() { + // keep using the same height (the first query happened at the most recent height) + height = Some(paged_response.at_height) + } + + if let Some(start_after_res) = paged_response.start_next_after { + start_after = Some(start_after_res) + } else { + break; + } + } + + Ok(identities) + } + + pub async fn get_nymd_rewarded_and_active_sets( + &self, + ) -> Result, ValidatorClientError> + where + C: CosmWasmClient + Sync, + { + let all_mixnodes = self.get_all_nymd_mixnodes().await?; + let rewarded_set_identities = self + .get_all_nymd_rewarded_set_mixnode_identities() + .await? + .into_iter() + .collect::>(); + + Ok(all_mixnodes + .into_iter() + .filter_map(|node| { + rewarded_set_identities + .get(node.identity()) + .map(|status| (node, *status)) + }) + .collect()) + } + + /// If you need both rewarded and the active set, consider using [Self::get_nymd_rewarded_and_active_sets] instead + pub async fn get_nymd_rewarded_set(&self) -> Result, ValidatorClientError> + where + C: CosmWasmClient + Sync, + { + let all_mixnodes = self.get_all_nymd_mixnodes().await?; + let rewarded_set_identities = self + .get_all_nymd_rewarded_set_mixnode_identities() + .await? + .into_iter() + .map(|(identity, _status)| identity) + .collect::>(); + + Ok(all_mixnodes + .into_iter() + .filter(|node| rewarded_set_identities.contains(node.identity())) + .collect()) + } + + /// If you need both rewarded and the active set, consider using [Self::get_nymd_rewarded_and_active_sets] instead + pub async fn get_nymd_active_set(&self) -> Result, ValidatorClientError> + where + C: CosmWasmClient + Sync, + { + let all_mixnodes = self.get_all_nymd_mixnodes().await?; + let active_set_identities = self + .get_all_nymd_rewarded_set_mixnode_identities() + .await? + .into_iter() + .filter_map(|(identity, status)| { + if status.is_active() { + Some(identity) + } else { + None + } + }) + .collect::>(); + + Ok(all_mixnodes + .into_iter() + .filter(|node| active_set_identities.contains(node.identity())) + .collect()) + } + pub async fn get_all_nymd_mixnodes(&self) -> Result, ValidatorClientError> where C: CosmWasmClient + Sync, @@ -314,8 +441,8 @@ impl Client { pub async fn get_all_nymd_single_mixnode_delegations( &self, - identity: mixnet_contract_common::IdentityKey, - ) -> Result, ValidatorClientError> + identity: IdentityKey, + ) -> Result, ValidatorClientError> where C: CosmWasmClient + Sync, { diff --git a/common/client-libs/validator-client/src/nymd/fee/helpers.rs b/common/client-libs/validator-client/src/nymd/fee/helpers.rs index 0ddbf15356..5c5eae4a11 100644 --- a/common/client-libs/validator-client/src/nymd/fee/helpers.rs +++ b/common/client-libs/validator-client/src/nymd/fee/helpers.rs @@ -41,6 +41,10 @@ pub enum Operation { WithdrawVestedCoins, TrackUndelegation, CreatePeriodicVestingAccount, + + AdvanceCurrentInterval, + WriteRewardedSet, + ClearRewardedSet, } pub(crate) fn calculate_fee(gas_price: &GasPrice, gas_limit: Gas) -> Coin { @@ -78,6 +82,9 @@ impl fmt::Display for Operation { Operation::WithdrawVestedCoins => f.write_str("WithdrawVestedCoins"), Operation::TrackUndelegation => f.write_str("TrackUndelegation"), Operation::CreatePeriodicVestingAccount => f.write_str("CreatePeriodicVestingAccount"), + Operation::AdvanceCurrentInterval => f.write_str("AdvanceCurrentInterval"), + Operation::WriteRewardedSet => f.write_str("WriteRewardedSet"), + Operation::ClearRewardedSet => f.write_str("ClearRewardedSet"), } } } @@ -115,6 +122,9 @@ impl Operation { Operation::WithdrawVestedCoins => 175_000u64.into(), Operation::TrackUndelegation => 175_000u64.into(), Operation::CreatePeriodicVestingAccount => 175_000u64.into(), + Operation::AdvanceCurrentInterval => 175_000u64.into(), + Operation::WriteRewardedSet => 175_000u64.into(), + Operation::ClearRewardedSet => 175_000u64.into(), } } diff --git a/common/client-libs/validator-client/src/nymd/mod.rs b/common/client-libs/validator-client/src/nymd/mod.rs index 95d699573d..d5c86ef28e 100644 --- a/common/client-libs/validator-client/src/nymd/mod.rs +++ b/common/client-libs/validator-client/src/nymd/mod.rs @@ -15,10 +15,10 @@ pub use fee::gas_price::GasPrice; use fee::helpers::Operation; use mixnet_contract_common::{ ContractStateParams, Delegation, ExecuteMsg, Gateway, GatewayBond, GatewayOwnershipResponse, - IdentityKey, LayerDistribution, MixNode, MixNodeBond, MixOwnershipResponse, + IdentityKey, Interval, LayerDistribution, MixNode, MixNodeBond, MixOwnershipResponse, MixnetContractVersion, MixnodeRewardingStatusResponse, PagedAllDelegationsResponse, PagedDelegatorDelegationsResponse, PagedGatewayResponse, PagedMixDelegationsResponse, - PagedMixnodeResponse, QueryMsg, RewardingIntervalResponse, + PagedMixnodeResponse, PagedRewardedSetResponse, QueryMsg, RewardedSetUpdateDetails, }; use serde::Serialize; use std::convert::TryInto; @@ -289,35 +289,65 @@ impl NymdClient { .await } - pub async fn get_current_rewarding_interval( - &self, - ) -> Result - where - C: CosmWasmClient + Sync, - { - let request = QueryMsg::CurrentRewardingInterval {}; - self.client - .query_contract_smart(self.mixnet_contract_address()?, &request) - .await - } - pub async fn get_rewarding_status( &self, mix_identity: mixnet_contract_common::IdentityKey, - rewarding_interval_nonce: u32, + interval_id: u32, ) -> Result where C: CosmWasmClient + Sync, { let request = QueryMsg::GetRewardingStatus { mix_identity, - rewarding_interval_nonce, + interval_id, }; self.client .query_contract_smart(self.mixnet_contract_address()?, &request) .await } + pub async fn query_current_rewarded_set_height(&self) -> Result + where + C: CosmWasmClient + Sync, + { + let request = QueryMsg::GetCurrentRewardedSetHeight {}; + self.client + .query_contract_smart(self.mixnet_contract_address()?, &request) + .await + } + + pub async fn query_current_rewarded_set_update_details( + &self, + ) -> Result + where + C: CosmWasmClient + Sync, + { + let request = QueryMsg::GetRewardedSetUpdateDetails {}; + self.client + .query_contract_smart(self.mixnet_contract_address()?, &request) + .await + } + + pub async fn get_rewarded_set_identities_paged( + &self, + start_after: Option, + page_limit: Option, + height: Option, + ) -> Result + where + C: CosmWasmClient + Sync, + { + let request = QueryMsg::GetRewardedSet { + height, + start_after, + limit: page_limit, + }; + + self.client + .query_contract_smart(self.mixnet_contract_address()?, &request) + .await + } + pub async fn get_layer_distribution(&self) -> Result where C: CosmWasmClient + Sync, @@ -328,6 +358,16 @@ impl NymdClient { .await } + pub async fn get_current_interval(&self) -> Result + where + C: CosmWasmClient + Sync, + { + let request = QueryMsg::GetCurrentInterval {}; + self.client + .query_contract_smart(self.mixnet_contract_address()?, &request) + .await + } + pub async fn get_reward_pool(&self) -> Result where C: CosmWasmClient + Sync, @@ -358,11 +398,11 @@ impl NymdClient { .await } - pub async fn get_epoch_reward_percent(&self) -> Result + pub async fn get_interval_reward_percent(&self) -> Result where C: CosmWasmClient + Sync, { - let request = QueryMsg::GetEpochRewardPercent {}; + let request = QueryMsg::GetIntervalRewardPercent {}; self.client .query_contract_smart(self.mixnet_contract_address()?, &request) .await @@ -1115,41 +1155,38 @@ impl NymdClient { .await } - pub async fn begin_mixnode_rewarding( - &self, - rewarding_interval_nonce: u32, - ) -> Result + pub async fn advance_current_interval(&self) -> Result where C: SigningCosmWasmClient + Sync, { - let fee = self.operation_fee(Operation::BeginMixnodeRewarding); + let fee = self.operation_fee(Operation::AdvanceCurrentInterval); - let req = ExecuteMsg::BeginMixnodeRewarding { - rewarding_interval_nonce, - }; + let req = ExecuteMsg::AdvanceCurrentInterval {}; self.client .execute( self.address(), self.mixnet_contract_address()?, &req, fee, - "Beginning mixnode rewarding procedure", + "Advancing current interval", Vec::new(), ) .await } - pub async fn finish_mixnode_rewarding( + pub async fn write_rewarded_set( &self, - rewarding_interval_nonce: u32, + rewarded_set: Vec, + expected_active_set_size: u32, ) -> Result where C: SigningCosmWasmClient + Sync, { - let fee = self.operation_fee(Operation::FinishMixnodeRewarding); + let fee = self.operation_fee(Operation::WriteRewardedSet); - let req = ExecuteMsg::FinishMixnodeRewarding { - rewarding_interval_nonce, + let req = ExecuteMsg::WriteRewardedSet { + rewarded_set, + expected_active_set_size, }; self.client .execute( @@ -1157,7 +1194,7 @@ impl NymdClient { self.mixnet_contract_address()?, &req, fee, - "Finishing mixnode rewarding procedure", + "Writing rewarded set", Vec::new(), ) .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 c30a76b174..ba737a3dfc 100644 --- a/common/client-libs/validator-client/src/validator_api/mod.rs +++ b/common/client-libs/validator-client/src/validator_api/mod.rs @@ -9,8 +9,8 @@ use serde::{Deserialize, Serialize}; use std::collections::HashMap; use url::Url; use validator_api_requests::models::{ - CoreNodeStatusResponse, MixnodeStatusResponse, RewardEstimationResponse, - StakeSaturationResponse, + CoreNodeStatusResponse, InclusionProbabilityResponse, MixnodeStatusResponse, + RewardEstimationResponse, StakeSaturationResponse, }; pub mod error; @@ -232,6 +232,23 @@ impl Client { .await } + pub async fn get_mixnode_inclusion_probability( + &self, + identity: IdentityKeyRef<'_>, + ) -> Result { + self.query_validator_api( + &[ + routes::API_VERSION, + routes::STATUS_ROUTES, + routes::MIXNODE, + identity, + routes::INCLUSION_CHANCE, + ], + NO_PARAMS, + ) + .await + } + pub async fn blind_sign( &self, request_body: &BlindSignRequestBody, diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/Cargo.toml b/common/cosmwasm-smart-contracts/mixnet-contract/Cargo.toml index ba0edd57b1..611e3be2c2 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/Cargo.toml +++ b/common/cosmwasm-smart-contracts/mixnet-contract/Cargo.toml @@ -18,6 +18,7 @@ network-defaults = { path = "../../network-defaults" } fixed = { version = "1.1", features = ["serde"] } az = "1.1" log = "0.4.14" +time = { version = "0.3.5", features = ["serde"] } contracts-common = { path = "../contracts-common" } diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/events.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/events.rs index d464e96acf..182b6d33ae 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/events.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/events.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::mixnode::NodeRewardResult; -use crate::{ContractStateParams, Delegation, IdentityKeyRef, Layer}; +use crate::{ContractStateParams, Delegation, IdentityKeyRef, Interval, Layer}; use cosmwasm_std::{Addr, Coin, Event, Uint128}; pub use contracts_common::events::*; @@ -15,10 +15,10 @@ pub const GATEWAY_UNBONDING_EVENT_TYPE: &str = "gateway_unbonding"; pub const MIXNODE_BONDING_EVENT_TYPE: &str = "mixnode_bonding"; pub const MIXNODE_UNBONDING_EVENT_TYPE: &str = "mixnode_unbonding"; pub const SETTINGS_UPDATE_EVENT_TYPE: &str = "settings_update"; -pub const BEGIN_REWARDING_EVENT_TYPE: &str = "begin_rewarding"; pub const OPERATOR_REWARDING_EVENT_TYPE: &str = "mix_rewarding"; pub const MIX_DELEGATORS_REWARDING_EVENT_TYPE: &str = "mix_delegators_rewarding"; -pub const FINISH_REWARDING_EVENT_TYPE: &str = "finish_rewarding"; +pub const CHANGE_REWARDED_SET_EVENT_TYPE: &str = "change_rewarded_set"; +pub const ADVANCE_INTERVAL_EVENT_TYPE: &str = "advance_interval"; // attributes that are used in multiple places pub const OWNER_KEY: &str = "owner"; @@ -50,7 +50,7 @@ pub const NEW_MIXNODE_ACTIVE_SET_SIZE_KEY: &str = "new_mixnode_active_set_size"; pub const NEW_ACTIVE_SET_WORK_FACTOR_KEY: &str = "new_active_set_work_factor"; // rewarding -pub const REWARDING_INTERVAL_NONCE_KEY: &str = "rewarding_interval_nonce"; +pub const INTERVAL_ID_KEY: &str = "interval_id"; pub const TOTAL_MIXNODE_REWARD_KEY: &str = "total_node_reward"; pub const OPERATOR_REWARD_KEY: &str = "operator_reward"; pub const LAMBDA_KEY: &str = "lambda"; @@ -62,6 +62,14 @@ pub const BOND_NOT_FOUND_VALUE: &str = "bond_not_found"; pub const BOND_TOO_FRESH_VALUE: &str = "bond_too_fresh"; pub const ZERO_UPTIME_VALUE: &str = "zero_uptime"; +// rewarded set update +pub const ACTIVE_SET_SIZE_KEY: &str = "active_set_size"; +pub const REWARDED_SET_SIZE_KEY: &str = "rewarded_set_size"; +pub const NODES_IN_REWARDED_SET_KEY: &str = "nodes_in_rewarded_set"; +pub const CURRENT_INTERVAL_ID_KEY: &str = "current_interval"; + +pub const NEW_CURRENT_INTERVAL_KEY: &str = "new_current_interval"; + pub fn new_delegation_event( delegator: &Addr, proxy: &Option, @@ -246,61 +254,38 @@ pub fn new_settings_update_event( event } -pub fn new_begin_rewarding_event(rewarding_interval_nonce: u32) -> Event { - Event::new(BEGIN_REWARDING_EVENT_TYPE).add_attribute( - REWARDING_INTERVAL_NONCE_KEY, - rewarding_interval_nonce.to_string(), - ) -} - -pub fn new_finish_rewarding_event(rewarding_interval_nonce: u32) -> Event { - Event::new(FINISH_REWARDING_EVENT_TYPE).add_attribute( - REWARDING_INTERVAL_NONCE_KEY, - rewarding_interval_nonce.to_string(), - ) -} - pub fn new_not_found_mix_operator_rewarding_event( - rewarding_interval_nonce: u32, + interval_id: u32, identity: IdentityKeyRef, ) -> Event { Event::new(OPERATOR_REWARDING_EVENT_TYPE) - .add_attribute( - REWARDING_INTERVAL_NONCE_KEY, - rewarding_interval_nonce.to_string(), - ) + .add_attribute(INTERVAL_ID_KEY, interval_id.to_string()) .add_attribute(NODE_IDENTITY_KEY, identity) .add_attribute(NO_REWARD_REASON_KEY, BOND_NOT_FOUND_VALUE) } pub fn new_too_fresh_bond_mix_operator_rewarding_event( - rewarding_interval_nonce: u32, + interval_id: u32, identity: IdentityKeyRef, ) -> Event { Event::new(OPERATOR_REWARDING_EVENT_TYPE) - .add_attribute( - REWARDING_INTERVAL_NONCE_KEY, - rewarding_interval_nonce.to_string(), - ) + .add_attribute(INTERVAL_ID_KEY, interval_id.to_string()) .add_attribute(NODE_IDENTITY_KEY, identity) .add_attribute(NO_REWARD_REASON_KEY, BOND_TOO_FRESH_VALUE) } pub fn new_zero_uptime_mix_operator_rewarding_event( - rewarding_interval_nonce: u32, + interval_id: u32, identity: IdentityKeyRef, ) -> Event { Event::new(OPERATOR_REWARDING_EVENT_TYPE) - .add_attribute( - REWARDING_INTERVAL_NONCE_KEY, - rewarding_interval_nonce.to_string(), - ) + .add_attribute(INTERVAL_ID_KEY, interval_id.to_string()) .add_attribute(NODE_IDENTITY_KEY, identity) .add_attribute(NO_REWARD_REASON_KEY, ZERO_UPTIME_VALUE) } pub fn new_mix_operator_rewarding_event( - rewarding_interval_nonce: u32, + interval_id: u32, identity: IdentityKeyRef, node_reward_result: NodeRewardResult, operator_reward: Uint128, @@ -308,10 +293,7 @@ pub fn new_mix_operator_rewarding_event( further_delegations: bool, ) -> Event { Event::new(OPERATOR_REWARDING_EVENT_TYPE) - .add_attribute( - REWARDING_INTERVAL_NONCE_KEY, - rewarding_interval_nonce.to_string(), - ) + .add_attribute(INTERVAL_ID_KEY, interval_id.to_string()) .add_attribute(NODE_IDENTITY_KEY, identity) .add_attribute( TOTAL_MIXNODE_REWARD_KEY, @@ -331,16 +313,13 @@ pub fn new_mix_operator_rewarding_event( } pub fn new_mix_delegators_rewarding_event( - rewarding_interval_nonce: u32, + interval_id: u32, identity: IdentityKeyRef, delegation_rewards_distributed: Uint128, further_delegations: bool, ) -> Event { Event::new(MIX_DELEGATORS_REWARDING_EVENT_TYPE) - .add_attribute( - REWARDING_INTERVAL_NONCE_KEY, - rewarding_interval_nonce.to_string(), - ) + .add_attribute(INTERVAL_ID_KEY, interval_id.to_string()) .add_attribute(NODE_IDENTITY_KEY, identity) .add_attribute( DISTRIBUTED_DELEGATION_REWARDS_KEY, @@ -351,3 +330,22 @@ pub fn new_mix_delegators_rewarding_event( further_delegations.to_string(), ) } + +// note that when this event is emitted, we'll know the current block height +pub fn new_change_rewarded_set_event( + active_set_size: u32, + rewarded_set_size: u32, + nodes_in_rewarded_set: u32, + current_interval_id: u32, +) -> Event { + Event::new(CHANGE_REWARDED_SET_EVENT_TYPE) + .add_attribute(ACTIVE_SET_SIZE_KEY, active_set_size.to_string()) + .add_attribute(REWARDED_SET_SIZE_KEY, rewarded_set_size.to_string()) + .add_attribute(NODES_IN_REWARDED_SET_KEY, nodes_in_rewarded_set.to_string()) + .add_attribute(CURRENT_INTERVAL_ID_KEY, current_interval_id.to_string()) +} + +pub fn new_advance_interval_event(interval: Interval) -> Event { + Event::new(ADVANCE_INTERVAL_EVENT_TYPE) + .add_attribute(NEW_CURRENT_INTERVAL_KEY, interval.to_string()) +} diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/interval.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/interval.rs new file mode 100644 index 0000000000..1f77b90eab --- /dev/null +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/interval.rs @@ -0,0 +1,325 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use network_defaults::{DEFAULT_FIRST_INTERVAL_START, DEFAULT_INTERVAL_LENGTH}; +use serde::{Deserialize, Serialize}; +use std::convert::TryInto; +use std::fmt::{Display, Formatter}; +use std::time::Duration; +use time::OffsetDateTime; + +/// Representation of rewarding interval. +#[derive(Clone, Copy, Debug, Deserialize, PartialEq, PartialOrd, Serialize)] +pub struct Interval { + id: u32, + start: OffsetDateTime, + length: Duration, +} + +impl Interval { + /// Creates new interval instance. + pub const fn new(id: u32, start: OffsetDateTime, length: Duration) -> Self { + Interval { id, start, length } + } + + /// Returns the next interval. + #[must_use] + pub fn next_interval(&self) -> Self { + Interval { + id: self.id + 1, + start: self.end(), + length: self.length, + } + } + + /// Returns the last interval. + pub fn previous_interval(&self) -> Option { + if self.id > 0 { + Some(Interval { + id: self.id - 1, + start: self.start - self.length, + length: self.length, + }) + } else { + None + } + } + + /// Determines whether the provided datetime is contained within the interval + /// + /// # Arguments + /// + /// * `datetime`: specified datetime + pub fn contains(&self, datetime: OffsetDateTime) -> bool { + self.start <= datetime && datetime <= self.end() + } + + /// Determines whether the provided unix timestamp is contained within the interval + /// + /// # Arguments + /// + /// * `timestamp`: specified timestamp + pub fn contains_timestamp(&self, timestamp: i64) -> bool { + self.start_unix_timestamp() <= timestamp && timestamp <= self.end_unix_timestamp() + } + + /// Returns new instance of [Interval] such that the provided datetime would be within + /// its duration. + /// + /// # Arguments + /// + /// * `now`: current datetime + pub fn current(&self, now: OffsetDateTime) -> Option { + let mut candidate = *self; + + if now > self.start { + loop { + if candidate.contains(now) { + return Some(candidate); + } + candidate = candidate.next_interval(); + } + } else { + loop { + if candidate.contains(now) { + return Some(candidate); + } + candidate = candidate.previous_interval()?; + } + } + } + + /// Returns new instance of [Interval] such that the provided unix timestamp would be within + /// its duration. + /// + /// # Arguments + /// + /// * `now_unix`: current unix time + pub fn current_with_timestamp(&self, now_unix: i64) -> Option { + let mut candidate = *self; + + if now_unix > self.start_unix_timestamp() { + loop { + if candidate.contains_timestamp(now_unix) { + return Some(candidate); + } + candidate = candidate.next_interval(); + } + } else { + loop { + if candidate.contains_timestamp(now_unix) { + return Some(candidate); + } + candidate = candidate.previous_interval()?; + } + } + } + + /// Checks whether this interval has already finished + /// + /// # Arguments + /// + /// * `now`: current datetime + pub fn has_elapsed(&self, now: OffsetDateTime) -> bool { + self.end() < now + } + + /// Returns id of this interval + pub const fn id(&self) -> u32 { + self.id + } + + /// Determines amount of time left until this interval finishes. + /// + /// # Arguments + /// + /// * `now`: current datetime + pub fn until_end(&self, now: OffsetDateTime) -> Option { + let remaining = self.end() - now; + if remaining.is_negative() { + None + } else { + remaining.try_into().ok() + } + } + + /// Returns the starting datetime of this interval. + pub const fn start(&self) -> OffsetDateTime { + self.start + } + + /// Returns the length of this interval. + pub const fn length(&self) -> Duration { + self.length + } + + /// Returns the ending datetime of this interval. + pub fn end(&self) -> OffsetDateTime { + self.start + self.length + } + + /// Returns the unix timestamp of the start of this interval. + pub const fn start_unix_timestamp(&self) -> i64 { + self.start().unix_timestamp() + } + + /// Returns the unix timestamp of the end of this interval. + pub fn end_unix_timestamp(&self) -> i64 { + self.end().unix_timestamp() + } +} + +impl Display for Interval { + fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { + let length = self.length(); + let hours = length.as_secs_f32() / 3600.0; + write!( + f, + "Interval {}: {} - {} ({:.1} hours)", + self.id, + self.start(), + self.end(), + hours + ) + } +} + +impl Default for Interval { + fn default() -> Self { + Interval { + id: 0, + start: DEFAULT_FIRST_INTERVAL_START, + length: DEFAULT_INTERVAL_LENGTH, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn previous_interval() { + let interval = Interval { + id: 1, + start: time::macros::datetime!(2021-08-23 12:00 UTC), + length: Duration::from_secs(24 * 60 * 60), + }; + let expected = Interval { + id: 0, + start: time::macros::datetime!(2021-08-22 12:00 UTC), + length: Duration::from_secs(24 * 60 * 60), + }; + assert_eq!(expected, interval.previous_interval().unwrap()); + + let genesis_interval = Interval { + id: 0, + start: time::macros::datetime!(2021-08-23 12:00 UTC), + length: Duration::from_secs(24 * 60 * 60), + }; + assert!(genesis_interval.previous_interval().is_none()); + } + + #[test] + fn next_interval() { + let interval = Interval { + id: 0, + start: time::macros::datetime!(2021-08-23 12:00 UTC), + length: Duration::from_secs(24 * 60 * 60), + }; + let expected = Interval { + id: 1, + start: time::macros::datetime!(2021-08-24 12:00 UTC), + length: Duration::from_secs(24 * 60 * 60), + }; + + assert_eq!(expected, interval.next_interval()) + } + + #[test] + fn checking_for_datetime_inclusion() { + let interval = Interval { + id: 100, + start: time::macros::datetime!(2021-08-23 12:00 UTC), + length: Duration::from_secs(24 * 60 * 60), + }; + + // it must contain its own boundaries + assert!(interval.contains(interval.start)); + assert!(interval.contains(interval.end())); + + let in_the_midle = interval.start + Duration::from_secs(interval.length.as_secs() / 2); + assert!(interval.contains(in_the_midle)); + + assert!(!interval.contains(interval.next_interval().end())); + assert!(!interval.contains(interval.previous_interval().unwrap().start())); + } + + #[test] + fn determining_current_interval() { + let first_interval = Interval { + id: 100, + start: time::macros::datetime!(2021-08-23 12:00 UTC), + length: Duration::from_secs(24 * 60 * 60), + }; + + // interval just before + let fake_now = first_interval.start - Duration::from_secs(123); + assert_eq!( + first_interval.previous_interval(), + first_interval.current(fake_now) + ); + + // this interval (start boundary) + assert_eq!( + first_interval, + first_interval.current(first_interval.start).unwrap() + ); + + // this interval (in the middle) + let fake_now = first_interval.start + Duration::from_secs(123); + assert_eq!(first_interval, first_interval.current(fake_now).unwrap()); + + // this interval (end boundary) + assert_eq!( + first_interval, + first_interval.current(first_interval.end()).unwrap() + ); + + // next interval + let fake_now = first_interval.end() + Duration::from_secs(123); + assert_eq!( + first_interval.next_interval(), + first_interval.current(fake_now).unwrap() + ); + + // few intervals in the past + let fake_now = first_interval.start() + - first_interval.length + - first_interval.length + - first_interval.length; + assert_eq!( + first_interval + .previous_interval() + .unwrap() + .previous_interval() + .unwrap() + .previous_interval() + .unwrap(), + first_interval.current(fake_now).unwrap() + ); + + // few intervals in the future + let fake_now = first_interval.end() + + first_interval.length + + first_interval.length + + first_interval.length; + assert_eq!( + first_interval + .next_interval() + .next_interval() + .next_interval(), + first_interval.current(fake_now).unwrap() + ); + } +} diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/lib.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/lib.rs index 6f1854cc12..8bb4f35f32 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/lib.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/lib.rs @@ -5,6 +5,7 @@ mod delegation; pub mod error; pub mod events; mod gateway; +mod interval; pub mod mixnode; mod msg; mod types; @@ -17,6 +18,9 @@ pub use delegation::{ PagedMixDelegationsResponse, }; pub use gateway::{Gateway, GatewayBond, GatewayOwnershipResponse, PagedGatewayResponse}; -pub use mixnode::{Layer, MixNode, MixNodeBond, MixOwnershipResponse, PagedMixnodeResponse}; +pub use interval::Interval; +pub use mixnode::{ + Layer, MixNode, MixNodeBond, MixOwnershipResponse, PagedMixnodeResponse, RewardedSetNodeStatus, +}; pub use msg::*; pub use types::*; diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs index ff77ec628a..605d73c664 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs @@ -5,7 +5,7 @@ use crate::{IdentityKey, SphinxKey}; use az::CheckedCast; use cosmwasm_std::{coin, Addr, Coin, Uint128}; use log::error; -use network_defaults::DEFAULT_OPERATOR_EPOCH_COST; +use network_defaults::DEFAULT_OPERATOR_INTERVAL_COST; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; @@ -18,6 +18,19 @@ fixed::const_fixed_from_int! { const ONE: U128 = 1; } +#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))] +#[derive(Clone, Copy, Debug, Deserialize, PartialEq, PartialOrd, Serialize, JsonSchema)] +pub enum RewardedSetNodeStatus { + Active, + Standby, +} + +impl RewardedSetNodeStatus { + pub fn is_active(&self) -> bool { + matches!(self, RewardedSetNodeStatus::Active) + } +} + #[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))] #[derive(Clone, Debug, Deserialize, PartialEq, PartialOrd, Serialize, JsonSchema)] pub struct MixNode { @@ -133,7 +146,7 @@ impl NodeRewardParams { } pub fn operator_cost(&self) -> U128 { - U128::from_num(self.uptime.u128() / 100u128 * DEFAULT_OPERATOR_EPOCH_COST as u128) + U128::from_num(self.uptime.u128() / 100u128 * DEFAULT_OPERATOR_INTERVAL_COST as u128) } pub fn set_reward_blockstamp(&mut self, blockstamp: u64) { @@ -325,7 +338,7 @@ impl MixNodeBond { &self.mix_node } - pub fn total_stake(&self) -> Option { + pub fn total_bond(&self) -> Option { if self.pledge_amount.denom != self.total_delegation.denom { None } else { diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs index 8f74824ec0..6f12f9b9c5 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs @@ -38,28 +38,18 @@ pub enum ExecuteMsg { mix_identity: IdentityKey, }, - BeginMixnodeRewarding { - // nonce of the current rewarding interval - rewarding_interval_nonce: u32, - }, - - FinishMixnodeRewarding { - // nonce of the current rewarding interval - rewarding_interval_nonce: u32, - }, - RewardMixnode { identity: IdentityKey, // percentage value in range 0-100 params: NodeRewardParams, - // nonce of the current rewarding interval - rewarding_interval_nonce: u32, + // id of the current rewarding interval + interval_id: u32, }, RewardNextMixDelegators { mix_identity: IdentityKey, - // nonce of the current rewarding interval - rewarding_interval_nonce: u32, + // id of the current rewarding interval + interval_id: u32, }, DelegateToMixnodeOnBehalf { mix_identity: IdentityKey, @@ -85,6 +75,11 @@ pub enum ExecuteMsg { UnbondGatewayOnBehalf { owner: String, }, + WriteRewardedSet { + rewarded_set: Vec, + expected_active_set_size: u32, + }, + AdvanceCurrentInterval {}, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] @@ -106,7 +101,6 @@ pub enum QueryMsg { address: String, }, StateParams {}, - CurrentRewardingInterval {}, // gets all [paged] delegations in the entire network // TODO: do we even want that? GetAllNetworkDelegations { @@ -137,12 +131,24 @@ pub enum QueryMsg { LayerDistribution {}, GetRewardPool {}, GetCirculatingSupply {}, - GetEpochRewardPercent {}, + GetIntervalRewardPercent {}, GetSybilResistancePercent {}, GetRewardingStatus { mix_identity: IdentityKey, - rewarding_interval_nonce: u32, + interval_id: u32, }, + GetRewardedSet { + height: Option, + start_after: Option, + limit: Option, + }, + GetRewardedSetHeightsForInterval { + interval_id: u32, + }, + GetRewardedSetUpdateDetails {}, + GetCurrentRewardedSetHeight {}, + GetCurrentInterval {}, + GetRewardedSetRefreshBlocks {}, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs index 81db65650b..865312f1fe 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::mixnode::DelegatorRewardParams; -use crate::Layer; +use crate::{Layer, RewardedSetNodeStatus}; use cosmwasm_std::{Addr, Uint128}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -27,19 +27,12 @@ impl LayerDistribution { } } -#[derive(Debug, Default, Serialize, Deserialize, Copy, Clone, Eq, PartialEq)] -pub struct RewardingIntervalResponse { - pub current_rewarding_interval_starting_block: u64, - pub current_rewarding_interval_nonce: u32, - pub rewarding_in_progress: bool, -} - #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] pub struct ContractStateParams { - // so currently epoch_length is being unused and validator API performs rewarding - // based on its own epoch length config value. I guess that's fine for time being + // so currently interval_length is being unused and validator API performs rewarding + // based on its own interval length config value. I guess that's fine for time being // however, in the future, the contract constant should be controlling it instead. - // pub epoch_length: u32, // length of a rewarding epoch/interval, expressed in hours + // pub interval_length: u32, // length of a rewarding interval/interval, expressed in hours pub minimum_mixnode_pledge: Uint128, // minimum amount a mixnode must pledge to get into the system pub minimum_gateway_pledge: Uint128, // minimum amount a gateway must pledge to get into the system @@ -136,3 +129,23 @@ pub struct MixnetContractVersion { pub type IdentityKey = String; pub type IdentityKeyRef<'a> = &'a str; pub type SphinxKey = String; + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] +pub struct PagedRewardedSetResponse { + pub identities: Vec<(IdentityKey, RewardedSetNodeStatus)>, + pub start_next_after: Option, + pub at_height: u64, +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] +pub struct RewardedSetUpdateDetails { + pub refresh_rate_blocks: u64, + pub last_refreshed_block: u64, + pub current_height: u64, +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] +pub struct IntervalRewardedSetHeightsResponse { + pub interval_id: u32, + pub heights: Vec, +} diff --git a/common/credentials/src/coconut/bandwidth.rs b/common/credentials/src/coconut/bandwidth.rs index ff9ec442a0..cb72374b6c 100644 --- a/common/credentials/src/coconut/bandwidth.rs +++ b/common/credentials/src/coconut/bandwidth.rs @@ -27,7 +27,7 @@ pub struct BandwidthVoucherAttributes { pub binding_number: PrivateAttribute, // the value (e.g., bandwidth) encoded in this voucher pub voucher_value: PublicAttribute, - // a field with public information, e.g., type of voucher, epoch etc. + // a field with public information, e.g., type of voucher, interval etc. pub voucher_info: PublicAttribute, } diff --git a/common/network-defaults/src/lib.rs b/common/network-defaults/src/lib.rs index c3f1638b24..eb1adb030b 100644 --- a/common/network-defaults/src/lib.rs +++ b/common/network-defaults/src/lib.rs @@ -152,10 +152,11 @@ pub const DEFAULT_VALIDATOR_API_PORT: u16 = 8080; pub const VALIDATOR_API_VERSION: &str = "v1"; // REWARDING -pub const DEFAULT_FIRST_EPOCH_START: OffsetDateTime = time::macros::datetime!(2021-08-23 12:00 UTC); -pub const DEFAULT_EPOCH_LENGTH: Duration = Duration::from_secs(24 * 60 * 60 * 30); // 30 days -/// We'll be assuming a few more things, profit margin and cost function. Since we don't have relialable package measurement, we'll be using uptime. We'll also set the value of 1 Nym to 1 $, to be able to translate epoch costs to Nyms. We'll also assume a cost of 40$ per epoch(month), converting that to Nym at our 1$ rate translates to 40_000_000 uNyms -pub const DEFAULT_OPERATOR_EPOCH_COST: u64 = 40_000_000; // 40$/(30 days) at 1 Nym == 1$ +pub const DEFAULT_FIRST_INTERVAL_START: OffsetDateTime = + time::macros::datetime!(2021-08-23 12:00 UTC); +pub const DEFAULT_INTERVAL_LENGTH: Duration = Duration::from_secs(24 * 60 * 60 * 30); // 30 days, i.e. 720h +/// We'll be assuming a few more things, profit margin and cost function. Since we don't have relialable package measurement, we'll be using uptime. We'll also set the value of 1 Nym to 1 $, to be able to translate interval costs to Nyms. We'll also assume a cost of 40$ per interval(month), converting that to Nym at our 1$ rate translates to 40_000_000 uNyms +pub const DEFAULT_OPERATOR_INTERVAL_COST: u64 = 40_000_000; // 40$/(30 days) at 1 Nym == 1$ // TODO: is there a way to get this from the chain pub const TOTAL_SUPPLY: u128 = 1_000_000_000_000_000; diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock index e0ad7ddf60..4e99ffc019 100644 --- a/contracts/Cargo.lock +++ b/contracts/Cargo.lock @@ -17,9 +17,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.49" +version = "1.0.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a03e93e97a28fbc9f42fbc5ba0886a3c67eb637b476dbee711f80a6ffe8223d" +checksum = "84450d0b4a8bd1ba4144ce8ce718fbc5d071358b1e5384bace6536b3d1f2d5b3" [[package]] name = "arrayref" @@ -41,9 +41,9 @@ checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" [[package]] name = "az" -version = "1.1.2" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d6dff4a1892b54d70af377bf7a17064192e822865791d812957f21e3108c325" +checksum = "f771a5d1f5503f7f4279a30f3643d3421ba149848b89ecaaec0ea2acf04a5ac4" [[package]] name = "bandwidth-claim" @@ -123,7 +123,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" dependencies = [ - "generic-array 0.14.4", + "generic-array 0.14.5", ] [[package]] @@ -143,9 +143,9 @@ checksum = "771fe0050b883fcc3ea2359b1a96bcfbc090b7116eae7c3c512c7a083fdf23d3" [[package]] name = "bumpalo" -version = "3.8.0" +version = "3.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f1e260c3a9040a7c19a12468758f4c16f31a81a1fe087482be9570ec864bb6c" +checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899" [[package]] name = "byte-tools" @@ -155,9 +155,9 @@ checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" [[package]] name = "bytemuck" -version = "1.7.2" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72957246c41db82b8ef88a5486143830adeb8227ef9837740bdec67724cf2c5b" +checksum = "439989e6b8c38d1b6570a384ef1e49c8848128f5a97f3914baef02920842712f" [[package]] name = "byteorder" @@ -209,7 +209,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" dependencies = [ - "generic-array 0.14.4", + "generic-array 0.14.5", ] [[package]] @@ -245,9 +245,9 @@ dependencies = [ [[package]] name = "cosmwasm-crypto" -version = "1.0.0-beta4" +version = "1.0.0-beta3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f903ebbabc0d4880dbc76148efb8be8fc29fa4bf294c440c3d70da1c8bcafff7" +checksum = "a380b87642204557629c9b72988c47b55fbfe6d474960adba56b22331504956a" dependencies = [ "digest 0.9.0", "ed25519-zebra", @@ -258,9 +258,9 @@ dependencies = [ [[package]] name = "cosmwasm-derive" -version = "1.0.0-beta4" +version = "1.0.0-beta3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "832bebef577ecb394603de8e2bf0de429b74aa364e17dec18e15ce37e71b0cae" +checksum = "866713b2fe13f23038c7d8824c3059d1f28dd94685fb406d1533c4eeeefeefae" dependencies = [ "syn", ] @@ -277,9 +277,9 @@ dependencies = [ [[package]] name = "cosmwasm-std" -version = "1.0.0-beta4" +version = "1.0.0-beta3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6238c45840cc9de5a39f0f619e3a4f7c38c5d2c6ac9e3e4d72751ee045e6d7da" +checksum = "8dbb9939b31441dfa9af3ec9740c8a24d585688401eff1b6b386abb7ad0d10a8" dependencies = [ "base64", "cosmwasm-crypto", @@ -293,9 +293,9 @@ dependencies = [ [[package]] name = "cosmwasm-storage" -version = "1.0.0-beta4" +version = "1.0.0-beta3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f472c0bfd584a4510702537d0b65c5331095e76099586a12f749fd69afda9c5e" +checksum = "b4a4e55f0d64fed54cd2202301b8d466af8de044589247dabd77a4222f52f749" dependencies = [ "cosmwasm-std", "serde", @@ -327,7 +327,7 @@ dependencies = [ "config", "digest 0.9.0", "ed25519-dalek", - "generic-array 0.14.4", + "generic-array 0.14.5", "hkdf", "hmac", "log", @@ -344,7 +344,7 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f83bd3bb4314701c568e340cd8cf78c975aa0ca79e03d3f6d1677d5b0c9c0c03" dependencies = [ - "generic-array 0.14.4", + "generic-array 0.14.5", "rand_core 0.6.3", "subtle 2.4.1", "zeroize", @@ -366,7 +366,7 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" dependencies = [ - "generic-array 0.14.4", + "generic-array 0.14.5", "subtle 2.4.1", ] @@ -405,9 +405,9 @@ dependencies = [ [[package]] name = "der" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28e98c534e9c8a0483aa01d6f6913bc063de254311bd267c9cf535e9b70e15b2" +checksum = "79b71cca7d95d7681a4b3b9cdf63c8dbc3730d0584c2c74e31416d64a90493f4" dependencies = [ "const-oid", ] @@ -427,7 +427,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" dependencies = [ - "generic-array 0.14.4", + "generic-array 0.14.5", ] [[package]] @@ -493,7 +493,7 @@ checksum = "beca177dcb8eb540133e7680baff45e7cc4d93bf22002676cec549f82343721b" dependencies = [ "crypto-bigint", "ff", - "generic-array 0.14.4", + "generic-array 0.14.5", "group", "pkcs8", "rand_core 0.6.3", @@ -539,9 +539,9 @@ dependencies = [ [[package]] name = "fixed" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d333a26ec13a023c6dff4b7584de4d323cfee2e508f5dd2bbee6669e4f7efdf" +checksum = "80a9a8cb2e34880a498f09367089339bda5e12d6f871640f947850f7113058c0" dependencies = [ "az", "bytemuck", @@ -571,9 +571,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.4" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" +checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" dependencies = [ "typenum", "version_check", @@ -605,9 +605,9 @@ dependencies = [ [[package]] name = "getset" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24b328c01a4d71d2d8173daa93562a73ab0fe85616876f02500f53d82948c504" +checksum = "e45727250e75cc04ff2846a66397da8ef2b3db8e40e0cef4df67950a07621eb9" dependencies = [ "proc-macro-error", "proc-macro2", @@ -617,9 +617,9 @@ dependencies = [ [[package]] name = "git2" -version = "0.13.24" +version = "0.13.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "845e007a28f1fcac035715988a234e8ec5458fd825b20a20c7dec74237ef341f" +checksum = "f29229cc1b24c0e6062f6e742aa3e256492a5323365e5ed3413599f8a5eff7d6" dependencies = [ "bitflags", "libc", @@ -720,9 +720,9 @@ dependencies = [ [[package]] name = "itoa" -version = "0.4.8" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" +checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" [[package]] name = "jobserver" @@ -768,15 +768,15 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.107" +version = "0.2.112" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbe5e23404da5b4f555ef85ebed98fb4083e55a00c317800bc2a50ede9f3d219" +checksum = "1b03d17f364a3a042d5e5d46b053bbbf82c92c9430c592dd4c064dc6ee997125" [[package]] name = "libgit2-sys" -version = "0.12.25+1.3.0" +version = "0.12.26+1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f68169ef08d6519b2fe133ecc637408d933c0174b23b80bb2f79828966fbaab" +checksum = "19e1c899248e606fbfe68dcb31d8b0176ebab833b103824af31bddf4b7457494" dependencies = [ "cc", "libc", @@ -853,6 +853,7 @@ dependencies = [ "schemars", "serde", "thiserror", + "time 0.3.5", "vergen", "vesting-contract", ] @@ -871,6 +872,7 @@ dependencies = [ "serde", "serde_repr", "thiserror", + "time 0.3.5", ] [[package]] @@ -913,9 +915,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" +checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5" [[package]] name = "opaque-debug" @@ -1008,15 +1010,15 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.22" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12295df4f294471248581bc09bef3c38a5e46f1e36d6a37353621a0c6c357e1f" +checksum = "58893f751c9b0412871a09abd62ecd2a00298c6c83befa223ef98c52aef40cbe" [[package]] name = "ppv-lite86" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed0cfbc8191465bed66e1718596ee0b0b35d5ee1f41c5df2189d0fe8bde535ba" +checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" [[package]] name = "proc-macro-error" @@ -1044,9 +1046,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.32" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba508cc11742c0dc5c1659771673afbab7a0efab23aa17e854cbab0837ed0b43" +checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" dependencies = [ "unicode-xid", ] @@ -1059,9 +1061,9 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quote" -version = "1.0.10" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" +checksum = "47aa80447ce4daf1717500037052af176af5d38cc3e571d9ec1c7353fc10c87d" dependencies = [ "proc-macro2", ] @@ -1152,21 +1154,21 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61b3909d758bb75c79f23d4736fac9433868679d3ad2ea7a61e3c25cfda9a088" +checksum = "f2cc38e8fa666e2de3c4aba7edeb5ffc5246c1c2ed0e3d17e560aeeba736b23f" [[package]] name = "ryu" -version = "1.0.5" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" +checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" [[package]] name = "schemars" -version = "0.8.7" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271ac0c667b8229adf70f0f957697c96fafd7486ab7481e15dc5e45e3e6a4368" +checksum = "c6b5a3c80cea1ab61f4260238409510e814e38b4b563c06044edf91e7dc070e3" dependencies = [ "dyn-clone", "schemars_derive", @@ -1176,9 +1178,9 @@ dependencies = [ [[package]] name = "schemars_derive" -version = "0.8.7" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ebda811090b257411540779860bc09bf321bc587f58d2c5864309d1566214e7" +checksum = "41ae4dce13e8614c46ac3c38ef1c0d668b101df6ac39817aebdaa26642ddae9b" dependencies = [ "proc-macro2", "quote", @@ -1194,9 +1196,9 @@ checksum = "568a8e6258aa33c13358f81fd834adb854c6f7c9468520910a9b1e8fac068012" [[package]] name = "serde" -version = "1.0.130" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913" +checksum = "97565067517b60e2d1ea8b268e59ce036de907ac523ad83a0475da04e818989a" dependencies = [ "serde_derive", ] @@ -1212,9 +1214,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.130" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b" +checksum = "ed201699328568d8d08208fdd080e3ff594e6c422e438b6705905da01005d537" dependencies = [ "proc-macro2", "quote", @@ -1234,9 +1236,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.70" +version = "1.0.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e277c495ac6cd1a01a58d0a0c574568b4d1ddf14f59965c6a58b8d96400b54f3" +checksum = "ee2bb9cd061c5865d345bb02ca49fcef1391741b672b54a0bf7b679badec3142" dependencies = [ "itoa", "ryu", @@ -1268,9 +1270,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.9.8" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b69f9a4c9740d74c5baa3fd2e547f9525fa8088a8a958e0ca2409a514e33f5fa" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" dependencies = [ "block-buffer 0.9.0", "cfg-if", @@ -1350,9 +1352,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.81" +version = "1.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2afee18b8beb5a596ecb4a2dce128c719b4ba399d34126b9e4396e3f9860966" +checksum = "a684ac3dcd8913827e18cd09a68384ee66c1de24157e3c556c9ab16d85695fb7" dependencies = [ "proc-macro2", "quote", @@ -1408,6 +1410,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41effe7cfa8af36f439fac33861b66b049edc6f9a32331e2312660529c1c24ad" dependencies = [ "libc", + "serde", "time-macros", ] @@ -1443,9 +1446,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b63708a265f51345575b27fe43f9500ad611579e764c79edbc2037b1121959ec" +checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" [[package]] name = "ucd-trie" @@ -1506,9 +1509,9 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "vergen" -version = "5.1.18" +version = "5.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d48696c0fbbdafd9553e14c4584b4b9583931e9474a3ae506f1872b890d0b47" +checksum = "6cf88d94e969e7956d924ba70741316796177fa0c79a2c9f4ab04998d96e966e" dependencies = [ "anyhow", "cfg-if", @@ -1523,9 +1526,9 @@ dependencies = [ [[package]] name = "version_check" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "vesting-contract" diff --git a/contracts/mixnet/Cargo.toml b/contracts/mixnet/Cargo.toml index d1ca3066fe..fe9927d138 100644 --- a/contracts/mixnet/Cargo.toml +++ b/contracts/mixnet/Cargo.toml @@ -20,8 +20,8 @@ mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet- config = { path = "../../common/config"} vesting-contract = { path = "../vesting" } -cosmwasm-std = "1.0.0-beta4" -cosmwasm-storage = "1.0.0-beta4" +cosmwasm-std = "1.0.0-beta3" +cosmwasm-storage = "1.0.0-beta3" cw-storage-plus = "0.11.1" bs58 = "0.4.0" @@ -35,6 +35,7 @@ fixed = "1.1" rand_chacha = "0.2" rand = "0.7" crypto = { path = "../../common/crypto" } +time = "0.3" [build-dependencies] vergen = { version = "5", default-features = false, features = ["build", "git", "rustc"] } diff --git a/contracts/mixnet/src/contract.rs b/contracts/mixnet/src/contract.rs index 420a011ae0..e7eaf832a8 100644 --- a/contracts/mixnet/src/contract.rs +++ b/contracts/mixnet/src/contract.rs @@ -8,8 +8,13 @@ use crate::delegations::queries::query_mixnode_delegations_paged; use crate::error::ContractError; use crate::gateways::queries::query_gateways_paged; use crate::gateways::queries::query_owns_gateway; +use crate::interval::queries::{ + query_current_interval, query_current_rewarded_set_height, query_rewarded_set, + query_rewarded_set_heights_for_interval, query_rewarded_set_refresh_minimum_blocks, + query_rewarded_set_update_details, +}; +use crate::interval::storage as interval_storage; use crate::mixnet_contract_settings::models::ContractState; -use crate::mixnet_contract_settings::queries::query_rewarding_interval; use crate::mixnet_contract_settings::queries::{ query_contract_settings_params, query_contract_version, }; @@ -17,14 +22,16 @@ use crate::mixnet_contract_settings::storage as mixnet_params_storage; use crate::mixnodes::bonding_queries as mixnode_queries; use crate::mixnodes::bonding_queries::query_mixnodes_paged; use crate::mixnodes::layer_queries::query_layer_distribution; -use crate::rewards::queries::query_reward_pool; -use crate::rewards::queries::{query_circulating_supply, query_rewarding_status}; +use crate::rewards::queries::{ + query_circulating_supply, query_reward_pool, query_rewarding_status, +}; use crate::rewards::storage as rewards_storage; + use cosmwasm_std::{ entry_point, to_binary, Addr, Deps, DepsMut, Env, MessageInfo, QueryResponse, Response, Uint128, }; use mixnet_contract_common::{ - ContractStateParams, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, + ContractStateParams, ExecuteMsg, InstantiateMsg, Interval, MigrateMsg, QueryMsg, }; /// Constant specifying minimum of coin required to bond a gateway @@ -37,15 +44,13 @@ pub const INITIAL_MIXNODE_REWARDED_SET_SIZE: u32 = 200; pub const INITIAL_MIXNODE_ACTIVE_SET_SIZE: u32 = 100; pub const INITIAL_REWARD_POOL: u128 = 250_000_000_000_000; -pub const EPOCH_REWARD_PERCENT: u8 = 2; // Used to calculate epoch reward pool +pub const INTERVAL_REWARD_PERCENT: u8 = 2; // Used to calculate interval reward pool pub const DEFAULT_SYBIL_RESISTANCE_PERCENT: u8 = 30; pub const DEFAULT_ACTIVE_SET_WORK_FACTOR: u8 = 10; -fn default_initial_state( - owner: Addr, - rewarding_validator_address: Addr, - env: Env, -) -> ContractState { +pub const REWARDED_SET_REFRESH_BLOCKS: u64 = 720; // with blocktime being approximately 5s, it should be roughly 1h + +fn default_initial_state(owner: Addr, rewarding_validator_address: Addr) -> ContractState { ContractState { owner, rewarding_validator_address, @@ -56,9 +61,6 @@ fn default_initial_state( mixnode_active_set_size: INITIAL_MIXNODE_ACTIVE_SET_SIZE, active_set_work_factor: DEFAULT_ACTIVE_SET_WORK_FACTOR, }, - rewarding_interval_starting_block: env.block.height, - latest_rewarding_interval_nonce: 0, - rewarding_in_progress: false, } } @@ -75,11 +77,13 @@ pub fn instantiate( msg: InstantiateMsg, ) -> Result { let rewarding_validator_address = deps.api.addr_validate(&msg.rewarding_validator_address)?; - let state = default_initial_state(info.sender, rewarding_validator_address, env); + let state = default_initial_state(info.sender, rewarding_validator_address); mixnet_params_storage::CONTRACT_STATE.save(deps.storage, &state)?; mixnet_params_storage::LAYERS.save(deps.storage, &Default::default())?; rewards_storage::REWARD_POOL.save(deps.storage, &Uint128::new(INITIAL_REWARD_POOL))?; + interval_storage::CURRENT_INTERVAL.save(deps.storage, &Interval::default())?; + interval_storage::CURRENT_REWARDED_SET_HEIGHT.save(deps.storage, &env.block.height)?; Ok(Response::default()) } @@ -135,14 +139,14 @@ pub fn execute( ExecuteMsg::RewardMixnode { identity, params, - rewarding_interval_nonce, + interval_id, } => crate::rewards::transactions::try_reward_mixnode( deps, env, info, identity, params, - rewarding_interval_nonce, + interval_id, ), ExecuteMsg::DelegateToMixnode { mix_identity } => { crate::delegations::transactions::try_delegate_to_mixnode(deps, env, info, mix_identity) @@ -154,29 +158,14 @@ pub fn execute( mix_identity, ) } - ExecuteMsg::BeginMixnodeRewarding { - rewarding_interval_nonce, - } => crate::rewards::transactions::try_begin_mixnode_rewarding( - deps, - env, - info, - rewarding_interval_nonce, - ), - ExecuteMsg::FinishMixnodeRewarding { - rewarding_interval_nonce, - } => crate::rewards::transactions::try_finish_mixnode_rewarding( - deps, - info, - rewarding_interval_nonce, - ), ExecuteMsg::RewardNextMixDelegators { mix_identity, - rewarding_interval_nonce, + interval_id, } => crate::rewards::transactions::try_reward_next_mixnode_delegators( deps, info, mix_identity, - rewarding_interval_nonce, + interval_id, ), ExecuteMsg::DelegateToMixnodeOnBehalf { mix_identity, @@ -227,11 +216,24 @@ pub fn execute( ExecuteMsg::UnbondGatewayOnBehalf { owner } => { crate::gateways::transactions::try_remove_gateway_on_behalf(deps, info, owner) } + ExecuteMsg::WriteRewardedSet { + rewarded_set, + expected_active_set_size, + } => crate::interval::transactions::try_write_rewarded_set( + deps, + env, + info, + rewarded_set, + expected_active_set_size, + ), + ExecuteMsg::AdvanceCurrentInterval {} => { + crate::interval::transactions::try_advance_interval(env, deps.storage) + } } } #[entry_point] -pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> Result { +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result { let query_res = match msg { QueryMsg::GetContractVersion {} => to_binary(&query_contract_version()), QueryMsg::GetMixNodes { start_after, limit } => { @@ -245,7 +247,6 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> Result to_binary(&query_owns_gateway(deps, address)?), QueryMsg::StateParams {} => to_binary(&query_contract_settings_params(deps)?), - QueryMsg::CurrentRewardingInterval {} => to_binary(&query_rewarding_interval(deps)?), QueryMsg::LayerDistribution {} => to_binary(&query_layer_distribution(deps)?), QueryMsg::GetMixnodeDelegations { mix_identity, @@ -276,22 +277,77 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> Result to_binary(&query_mixnode_delegation(deps, mix_identity, delegator)?), QueryMsg::GetRewardPool {} => to_binary(&query_reward_pool(deps)?), QueryMsg::GetCirculatingSupply {} => to_binary(&query_circulating_supply(deps)?), - QueryMsg::GetEpochRewardPercent {} => to_binary(&EPOCH_REWARD_PERCENT), + QueryMsg::GetIntervalRewardPercent {} => to_binary(&INTERVAL_REWARD_PERCENT), QueryMsg::GetSybilResistancePercent {} => to_binary(&DEFAULT_SYBIL_RESISTANCE_PERCENT), QueryMsg::GetRewardingStatus { mix_identity, - rewarding_interval_nonce, - } => to_binary(&query_rewarding_status( - deps, - mix_identity, - rewarding_interval_nonce, + interval_id, + } => to_binary(&query_rewarding_status(deps, mix_identity, interval_id)?), + QueryMsg::GetRewardedSet { + height, + start_after, + limit, + } => to_binary(&query_rewarded_set( + deps.storage, + height, + start_after, + limit, )?), + QueryMsg::GetRewardedSetHeightsForInterval { interval_id } => to_binary( + &query_rewarded_set_heights_for_interval(deps.storage, interval_id)?, + ), + QueryMsg::GetRewardedSetUpdateDetails {} => { + to_binary(&query_rewarded_set_update_details(env, deps.storage)?) + } + QueryMsg::GetCurrentRewardedSetHeight {} => { + to_binary(&query_current_rewarded_set_height(deps.storage)?) + } + QueryMsg::GetCurrentInterval {} => to_binary(&query_current_interval(deps.storage)?), + QueryMsg::GetRewardedSetRefreshBlocks {} => { + to_binary(&query_rewarded_set_refresh_minimum_blocks()) + } }; Ok(query_res?) } #[entry_point] -pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result { +pub fn migrate(deps: DepsMut, env: Env, _msg: MigrateMsg) -> Result { + use cw_storage_plus::Item; + use serde::{Deserialize, Serialize}; + + // needed migration: + /* + 1. removal of rewarding_interval_starting_block field from ContractState + 2. removal of latest_rewarding_interval_nonce field from ContractState + 3. removal of rewarding_in_progress field from ContractState + 4. interval_storage::CURRENT_INTERVAL.save(deps.storage, &Interval::default())?; + 5. interval_storage::CURRENT_REWARDED_SET_HEIGHT.save(deps.storage, &env.block.height)?; + */ + + #[derive(Serialize, Deserialize)] + struct OldContractState { + pub owner: Addr, // only the owner account can update state + pub rewarding_validator_address: Addr, + pub params: ContractStateParams, + pub rewarding_interval_starting_block: u64, + pub latest_rewarding_interval_nonce: u32, + pub rewarding_in_progress: bool, + } + + let old_contract_state: Item = Item::new("config"); + + let old_state = old_contract_state.load(deps.storage)?; + let new_state = crate::mixnet_contract_settings::models::ContractState { + owner: old_state.owner, + rewarding_validator_address: old_state.rewarding_validator_address, + params: old_state.params, + }; + + mixnet_params_storage::CONTRACT_STATE.save(deps.storage, &new_state)?; + + interval_storage::CURRENT_INTERVAL.save(deps.storage, &Interval::default())?; + interval_storage::CURRENT_REWARDED_SET_HEIGHT.save(deps.storage, &env.block.height)?; + Ok(Default::default()) } diff --git a/contracts/mixnet/src/error.rs b/contracts/mixnet/src/error.rs index 46e85e9de0..ef12953200 100644 --- a/contracts/mixnet/src/error.rs +++ b/contracts/mixnet/src/error.rs @@ -42,7 +42,7 @@ pub enum ContractError { #[error("No coin was sent for the bonding, you must send {}", DENOM)] NoBondFound, - #[error("Provided active set size is bigger than the demanded set")] + #[error("Provided active set size is bigger than the rewarded set")] InvalidActiveSetSize, #[error("Provided active set size is zero")] @@ -75,14 +75,8 @@ pub enum ContractError { #[error("We tried to remove more funds then are available in the Reward pool. Wanted to remove {to_remove}, but have only {reward_pool}")] OutOfFunds { to_remove: u128, reward_pool: u128 }, - #[error("Received invalid rewarding interval nonce. Expected {expected}, received {received}")] - InvalidRewardingIntervalNonce { received: u32, expected: u32 }, - - #[error("Rewarding distribution is currently in progress")] - RewardingInProgress, - - #[error("Rewarding distribution is currently not in progress")] - RewardingNotInProgress, + #[error("Received invalid interval id. Expected {expected}, received {received}")] + InvalidIntervalId { received: u32, expected: u32 }, #[error("Mixnode {identity} has already been rewarded during the current rewarding interval")] MixnodeAlreadyRewarded { identity: IdentityKey }, @@ -105,6 +99,29 @@ pub enum ContractError { #[error("Provided ed25519 signature did not verify correctly")] InvalidEd25519Signature, - #[error("Profit margin percent needs to be an integer in range [0, 100], recieved {0}")] + #[error("Profit margin percent needs to be an integer in range [0, 100], received {0}")] InvalidProfitMarginPercent(u8), + + #[error("Rewarded set height not set, was rewarding set determined?")] + RewardSetHeightMapEmpty, + + #[error("Received unexpected value for the active set. Got: {received}, expected: {expected}")] + UnexpectedActiveSetSize { received: u32, expected: u32 }, + + #[error("Received unexpected value for the rewarded set. Got: {received}, expected at most: {expected}")] + UnexpectedRewardedSetSize { received: u32, expected: u32 }, + + #[error("There hasn't been sufficient delay since last rewarded set update. It was last updated at height {last_update}. The delay is {minimum_delay}. The current block height is {current_height}")] + TooFrequentRewardedSetUpdate { + last_update: u64, + minimum_delay: u64, + current_height: u64, + }, + + #[error("Can't change to the desired interval as it's not in progress yet. It starts at {interval_start} and finishes at {interval_end}, while the current block time is {current_block_time}")] + IntervalNotInProgress { + current_block_time: u64, + interval_start: i64, + interval_end: i64, + }, } diff --git a/contracts/mixnet/src/interval/mod.rs b/contracts/mixnet/src/interval/mod.rs new file mode 100644 index 0000000000..11f07ae776 --- /dev/null +++ b/contracts/mixnet/src/interval/mod.rs @@ -0,0 +1,6 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod queries; +pub mod storage; +pub mod transactions; diff --git a/contracts/mixnet/src/interval/queries.rs b/contracts/mixnet/src/interval/queries.rs new file mode 100644 index 0000000000..612b0b5634 --- /dev/null +++ b/contracts/mixnet/src/interval/queries.rs @@ -0,0 +1,425 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use super::storage; +use crate::error::ContractError; +use cosmwasm_std::{Env, Order, StdResult, Storage}; +use cw_storage_plus::Bound; +use mixnet_contract_common::{ + IdentityKey, Interval, IntervalRewardedSetHeightsResponse, PagedRewardedSetResponse, + RewardedSetNodeStatus, RewardedSetUpdateDetails, +}; + +pub fn query_current_interval(storage: &dyn Storage) -> Result { + Ok(storage::CURRENT_INTERVAL.load(storage)?) +} + +pub(crate) fn query_rewarded_set_refresh_minimum_blocks() -> u64 { + crate::contract::REWARDED_SET_REFRESH_BLOCKS +} + +pub fn query_rewarded_set_heights_for_interval( + storage: &dyn Storage, + interval_id: u32, +) -> Result { + // I don't think we have to deal with paging here as at most we're going to have 720 values here + // and I think the validators are capable of performing 720 storage reads at once if they're only + // reading u64 (+ u8) values... + let heights = storage::REWARDED_SET_HEIGHTS_FOR_INTERVAL + .prefix(interval_id) + .range(storage, None, None, Order::Ascending) + .map(|val| val.map(|(height, _)| height)) + .collect::>>()?; + + Ok(IntervalRewardedSetHeightsResponse { + interval_id, + heights, + }) +} + +// note: I have removed the `query_rewarded_set_for_interval`, because I don't think it's appropriate +// for the contract to go through so much data (i.e. all "rewarded" sets of particular interval) in one go. +// To achieve the same result, the client would have to instead first call `query_rewarded_set_heights_for_interval` +// to learn the heights used in given interval and then for each of them `query_rewarded_set` for that particular height. + +pub fn query_current_rewarded_set_height(storage: &dyn Storage) -> Result { + Ok(storage::CURRENT_REWARDED_SET_HEIGHT.load(storage)?) +} + +fn query_rewarded_set_at_height( + storage: &dyn Storage, + height: u64, + start_after: Option, + limit: u32, +) -> Result, ContractError> { + let start = start_after.map(Bound::exclusive); + + let rewarded_set = storage::REWARDED_SET + .prefix(height) + .range(storage, start, None, Order::Ascending) + .take(limit as usize) + .collect::>()?; + Ok(rewarded_set) +} + +pub fn query_rewarded_set( + storage: &dyn Storage, + height: Option, + start_after: Option, + limit: Option, +) -> Result { + let height = match height { + Some(height) => height, + None => query_current_rewarded_set_height(storage)?, + }; + let limit = limit + .unwrap_or(storage::REWARDED_NODE_DEFAULT_PAGE_LIMIT) + .min(storage::REWARDED_NODE_MAX_PAGE_LIMIT); + + // query for an additional element to determine paging requirements + let mut paged_result = query_rewarded_set_at_height(storage, height, start_after, limit + 1)?; + + if paged_result.len() > limit as usize { + paged_result.truncate(limit as usize); + Ok(PagedRewardedSetResponse { + start_next_after: paged_result.last().map(|res| res.0.clone()), + identities: paged_result, + at_height: height, + }) + } else { + Ok(PagedRewardedSetResponse { + identities: paged_result, + start_next_after: None, + at_height: height, + }) + } +} + +// this was all put together into the same query so that all information would be synced together +pub fn query_rewarded_set_update_details( + env: Env, + storage: &dyn Storage, +) -> Result { + Ok(RewardedSetUpdateDetails { + refresh_rate_blocks: query_rewarded_set_refresh_minimum_blocks(), + last_refreshed_block: query_current_rewarded_set_height(storage)?, + current_height: env.block.height, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::interval::storage::REWARDED_NODE_MAX_PAGE_LIMIT; + use crate::support::tests::test_helpers; + use cosmwasm_std::testing::mock_env; + + fn store_rewarded_nodes( + storage: &mut dyn Storage, + height: u64, + active_set: u32, + rewarded_set: u32, + ) -> Vec { + let identities = (0..rewarded_set) + .map(|i| format!("identity{:04}", i)) + .collect::>(); + storage::save_rewarded_set(storage, height, active_set, identities.clone()).unwrap(); + identities + } + + #[test] + fn querying_for_rewarded_set_heights_for_interval() { + let mut deps = test_helpers::init_contract(); + + // no data + assert!( + query_rewarded_set_heights_for_interval(deps.as_ref().storage, 0) + .unwrap() + .heights + .is_empty() + ); + + // 100 heights + for i in 0..100 { + storage::REWARDED_SET_HEIGHTS_FOR_INTERVAL + .save(deps.as_mut().storage, (1, i), &0u8) + .unwrap(); + } + let expected = (0..100).collect::>(); + assert_eq!( + expected, + query_rewarded_set_heights_for_interval(deps.as_ref().storage, 1) + .unwrap() + .heights + ); + + // 100 heights for different interval + for i in 200..300 { + storage::REWARDED_SET_HEIGHTS_FOR_INTERVAL + .save(deps.as_mut().storage, (10, i), &0u8) + .unwrap(); + } + let expected = (200..300).collect::>(); + assert_eq!( + expected, + query_rewarded_set_heights_for_interval(deps.as_ref().storage, 10) + .unwrap() + .heights + ) + } + + #[test] + fn querying_for_rewarded_set_at_height() { + let mut deps = test_helpers::init_contract(); + + // store some nodes + let identities1 = store_rewarded_nodes(deps.as_mut().storage, 1, 100, 200); + let identities2 = store_rewarded_nodes(deps.as_mut().storage, 2, 50, 200); + let identities3 = store_rewarded_nodes(deps.as_mut().storage, 3, 150, 200); + let identities4 = store_rewarded_nodes(deps.as_mut().storage, 4, 300, 500); + let identities5 = store_rewarded_nodes(deps.as_mut().storage, 5, 500, 500); + + // expected2 and 3 are basically sanity checks to ensure changing active set size (increase or decrease) + // doesn't affect the ordering + + let expected1 = identities1 + .into_iter() + .enumerate() + .map(|(i, identity)| { + if i < 100 { + (identity, RewardedSetNodeStatus::Active) + } else { + (identity, RewardedSetNodeStatus::Standby) + } + }) + .collect::>(); + + assert_eq!( + expected1, + query_rewarded_set_at_height(deps.as_ref().storage, 1, None, 1000).unwrap() + ); + + let expected2 = identities2 + .into_iter() + .enumerate() + .map(|(i, identity)| { + if i < 50 { + (identity, RewardedSetNodeStatus::Active) + } else { + (identity, RewardedSetNodeStatus::Standby) + } + }) + .collect::>(); + + assert_eq!( + expected2, + query_rewarded_set_at_height(deps.as_ref().storage, 2, None, 1000).unwrap() + ); + + let expected3 = identities3 + .into_iter() + .enumerate() + .map(|(i, identity)| { + if i < 150 { + (identity, RewardedSetNodeStatus::Active) + } else { + (identity, RewardedSetNodeStatus::Standby) + } + }) + .collect::>(); + + assert_eq!( + expected3, + query_rewarded_set_at_height(deps.as_ref().storage, 3, None, 1000).unwrap() + ); + + // check limit and paging + // active: 300, rewarded: 500 + let first_100 = identities4 + .iter() + .take(100) + .map(|identity| (identity.clone(), RewardedSetNodeStatus::Active)) + .collect::>(); + assert_eq!( + first_100, + query_rewarded_set_at_height(deps.as_ref().storage, 4, None, 100).unwrap() + ); + + let expected_single1 = vec![("identity0299".to_string(), RewardedSetNodeStatus::Active)]; + let expected_single2 = vec![("identity0300".to_string(), RewardedSetNodeStatus::Standby)]; + assert_eq!( + expected_single1, + query_rewarded_set_at_height( + deps.as_ref().storage, + 4, + Some("identity0298".to_string()), + 1 + ) + .unwrap() + ); + assert_eq!( + expected_single2, + query_rewarded_set_at_height( + deps.as_ref().storage, + 4, + Some("identity0299".to_string()), + 1 + ) + .unwrap() + ); + + let last_100 = identities4 + .iter() + .skip(400) + .map(|identity| (identity.clone(), RewardedSetNodeStatus::Standby)) + .collect::>(); + assert_eq!( + last_100, + query_rewarded_set_at_height( + deps.as_ref().storage, + 4, + Some("identity0399".to_string()), + 100 + ) + .unwrap() + ); + + // all nodes are in the active set + let expected5 = identities5 + .into_iter() + .map(|identity| (identity, RewardedSetNodeStatus::Active)) + .collect::>(); + + assert_eq!( + expected5, + query_rewarded_set_at_height(deps.as_ref().storage, 5, None, 1000).unwrap() + ); + } + + #[test] + fn querying_for_rewarded_set() { + let mut deps = test_helpers::init_contract(); + + let current_height = 123; + let other_height = 456; + let different_height = 789; + + storage::CURRENT_REWARDED_SET_HEIGHT + .save(deps.as_mut().storage, ¤t_height) + .unwrap(); + + let identities1 = store_rewarded_nodes(deps.as_mut().storage, current_height, 50, 100); + let identities2 = store_rewarded_nodes(deps.as_mut().storage, other_height, 100, 200); + let identities3 = store_rewarded_nodes( + deps.as_mut().storage, + different_height, + storage::REWARDED_NODE_MAX_PAGE_LIMIT, + storage::REWARDED_NODE_MAX_PAGE_LIMIT * 2, + ); + + // if height is not set, current height is used, else it's just passed + let expected1 = PagedRewardedSetResponse { + identities: identities1 + .into_iter() + .enumerate() + .map(|(i, identity)| { + if i < 50 { + (identity, RewardedSetNodeStatus::Active) + } else { + (identity, RewardedSetNodeStatus::Standby) + } + }) + .collect::>(), + start_next_after: None, + at_height: current_height, + }; + let expected2 = PagedRewardedSetResponse { + identities: identities2 + .into_iter() + .enumerate() + .map(|(i, identity)| { + if i < 100 { + (identity, RewardedSetNodeStatus::Active) + } else { + (identity, RewardedSetNodeStatus::Standby) + } + }) + .collect::>(), + start_next_after: None, + at_height: other_height, + }; + + assert_eq!( + Ok(expected1), + query_rewarded_set(deps.as_ref().storage, None, None, None) + ); + assert_eq!( + Ok(expected2), + query_rewarded_set(deps.as_ref().storage, Some(other_height), None, None) + ); + + // if limit is not set, a default one is used instead + let expected3 = PagedRewardedSetResponse { + identities: identities3 + .iter() + .take(storage::REWARDED_NODE_DEFAULT_PAGE_LIMIT as usize) + .cloned() + .map(|identity| (identity, RewardedSetNodeStatus::Active)) + .collect::>(), + start_next_after: Some(format!( + "identity{:04}", + storage::REWARDED_NODE_DEFAULT_PAGE_LIMIT - 1 + )), + at_height: different_height, + }; + assert_eq!( + Ok(expected3), + query_rewarded_set(deps.as_ref().storage, Some(different_height), None, None) + ); + + // limit cannot be larger that pre-defined maximum + let expected4 = PagedRewardedSetResponse { + identities: identities3 + .iter() + .take(storage::REWARDED_NODE_MAX_PAGE_LIMIT as usize) + .cloned() + .map(|identity| (identity, RewardedSetNodeStatus::Active)) + .collect::>(), + start_next_after: Some(format!( + "identity{:04}", + storage::REWARDED_NODE_MAX_PAGE_LIMIT - 1 + )), + at_height: different_height, + }; + assert_eq!( + Ok(expected4), + query_rewarded_set( + deps.as_ref().storage, + Some(different_height), + None, + Some(REWARDED_NODE_MAX_PAGE_LIMIT * 100) + ) + ); + } + + #[test] + fn querying_for_rewarded_set_update_details() { + let env = mock_env(); + let mut deps = test_helpers::init_contract(); + + let current_height = 123; + storage::CURRENT_REWARDED_SET_HEIGHT + .save(deps.as_mut().storage, ¤t_height) + .unwrap(); + + // returns whatever is in the correct environment + assert_eq!( + RewardedSetUpdateDetails { + refresh_rate_blocks: crate::contract::REWARDED_SET_REFRESH_BLOCKS, + last_refreshed_block: current_height, + current_height: env.block.height + }, + query_rewarded_set_update_details(env, deps.as_ref().storage).unwrap() + ) + } +} diff --git a/contracts/mixnet/src/interval/storage.rs b/contracts/mixnet/src/interval/storage.rs new file mode 100644 index 0000000000..214c5277df --- /dev/null +++ b/contracts/mixnet/src/interval/storage.rs @@ -0,0 +1,86 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_std::{StdResult, Storage}; +use cw_storage_plus::{Item, Map}; +use mixnet_contract_common::{IdentityKey, Interval, RewardedSetNodeStatus}; + +// type aliases for better reasoning for storage keys +// (I found it helpful) +type BlockHeight = u64; +type IntervalId = u32; + +// TODO: those values need to be verified +pub(crate) const REWARDED_NODE_DEFAULT_PAGE_LIMIT: u32 = 1000; +pub(crate) const REWARDED_NODE_MAX_PAGE_LIMIT: u32 = 1500; + +pub(crate) const CURRENT_INTERVAL: Item = Item::new("cep"); +pub(crate) const CURRENT_REWARDED_SET_HEIGHT: Item = Item::new("crh"); + +// I've changed the `()` data to an `u8` as after serializing `()` is represented as "null", +// taking more space than a single digit u8. If we don't care about what's there, why not go with more efficient approach? : ) +pub(crate) const REWARDED_SET_HEIGHTS_FOR_INTERVAL: Map<(IntervalId, BlockHeight), u8> = + Map::new("rsh"); + +// pub(crate) const REWARDED_SET: Map<(u64, IdentityKey), NodeStatus> = Map::new("rs"); +pub(crate) const REWARDED_SET: Map<(BlockHeight, IdentityKey), RewardedSetNodeStatus> = + Map::new("rs"); + +pub(crate) fn save_rewarded_set( + storage: &mut dyn Storage, + height: BlockHeight, + active_set_size: u32, + entries: Vec, +) -> StdResult<()> { + for (i, identity) in entries.into_iter().enumerate() { + // first k nodes are active + let set_status = if i < active_set_size as usize { + RewardedSetNodeStatus::Active + } else { + RewardedSetNodeStatus::Standby + }; + + REWARDED_SET.save(storage, (height, identity), &set_status)?; + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::support::tests::test_helpers; + + #[test] + fn saving_rewarded_set() { + let mut deps = test_helpers::init_contract(); + + let active_set_size = 100; + let mut nodes = Vec::new(); + for i in 0..1000 { + nodes.push(format!("identity{:04}", i)) + } + + save_rewarded_set(deps.as_mut().storage, 1234, active_set_size, nodes).unwrap(); + + // first k nodes MUST BE active + for i in 0..1000 { + let identity = format!("identity{:04}", i); + if i < active_set_size { + assert_eq!( + RewardedSetNodeStatus::Active, + REWARDED_SET + .load(deps.as_ref().storage, (1234, identity)) + .unwrap() + ) + } else { + assert_eq!( + RewardedSetNodeStatus::Standby, + REWARDED_SET + .load(deps.as_ref().storage, (1234, identity)) + .unwrap() + ) + } + } + } +} diff --git a/contracts/mixnet/src/interval/transactions.rs b/contracts/mixnet/src/interval/transactions.rs new file mode 100644 index 0000000000..132fc711c0 --- /dev/null +++ b/contracts/mixnet/src/interval/transactions.rs @@ -0,0 +1,308 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use super::storage; +use crate::error::ContractError; +use crate::error::ContractError::IntervalNotInProgress; +use crate::mixnet_contract_settings::storage as mixnet_params_storage; +use cosmwasm_std::{DepsMut, Env, MessageInfo, Response, Storage}; +use mixnet_contract_common::events::{new_advance_interval_event, new_change_rewarded_set_event}; +use mixnet_contract_common::IdentityKey; + +pub fn try_write_rewarded_set( + deps: DepsMut, + env: Env, + info: MessageInfo, + rewarded_set: Vec, + active_set_size: u32, +) -> Result { + let state = mixnet_params_storage::CONTRACT_STATE.load(deps.storage)?; + + // check if this is executed by the permitted validator, if not reject the transaction + if info.sender != state.rewarding_validator_address { + return Err(ContractError::Unauthorized); + } + + // sanity check to make sure the sending validator is in sync with the contract state + // (i.e. so that we'd known that top k nodes are actually expected to be active) + if active_set_size != state.params.mixnode_active_set_size { + return Err(ContractError::UnexpectedActiveSetSize { + received: active_set_size, + expected: state.params.mixnode_active_set_size, + }); + } + + if rewarded_set.len() as u32 > state.params.mixnode_rewarded_set_size { + return Err(ContractError::UnexpectedRewardedSetSize { + received: rewarded_set.len() as u32, + expected: state.params.mixnode_rewarded_set_size, + }); + } + + let last_update = storage::CURRENT_REWARDED_SET_HEIGHT.load(deps.storage)?; + let block_height = env.block.height; + + if last_update + crate::contract::REWARDED_SET_REFRESH_BLOCKS > block_height { + return Err(ContractError::TooFrequentRewardedSetUpdate { + last_update, + minimum_delay: crate::contract::REWARDED_SET_REFRESH_BLOCKS, + current_height: block_height, + }); + } + + let current_interval = storage::CURRENT_INTERVAL.load(deps.storage)?.id(); + let num_nodes = rewarded_set.len(); + + storage::save_rewarded_set(deps.storage, block_height, active_set_size, rewarded_set)?; + storage::REWARDED_SET_HEIGHTS_FOR_INTERVAL.save( + deps.storage, + (current_interval, block_height), + &0u8, + )?; + storage::CURRENT_REWARDED_SET_HEIGHT.save(deps.storage, &block_height)?; + + Ok(Response::new().add_event(new_change_rewarded_set_event( + state.params.mixnode_active_set_size, + state.params.mixnode_rewarded_set_size, + num_nodes as u32, + current_interval, + ))) +} + +pub fn try_advance_interval( + env: Env, + storage: &mut dyn Storage, +) -> Result { + // in theory, we could have just changed the state and relied on its reversal upon failed + // execution, but better safe than sorry and do not modify the state at all unless we know + // all checks have succeeded. + let current_interval = storage::CURRENT_INTERVAL.load(storage)?; + let next_interval = current_interval.next_interval(); + + if next_interval.start_unix_timestamp() > env.block.time.seconds() as i64 { + // the reason for this check is as follows: + // nobody, even trusted validators, should be able to continuously keep advancing intervals, + // because otherwise it would be possible for them to continuously keep rewarding nodes. + // + // Therefore, even if "trusted" validator, responsible for rewarding, is malicious, + // they can't send rewards more often than every `REWARDED_SET_REFRESH_BLOCKS` + // and changing this value requires going through governance and having agreement of + // the super-majority of the validators (by stake) + return Err(IntervalNotInProgress { + current_block_time: env.block.time.seconds(), + interval_start: next_interval.start_unix_timestamp(), + interval_end: next_interval.end_unix_timestamp(), + }); + } + + storage::CURRENT_INTERVAL.save(storage, &next_interval)?; + + Ok(Response::new().add_event(new_advance_interval_event(next_interval))) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::support::tests::test_helpers; + use cosmwasm_std::testing::{mock_env, mock_info}; + use cosmwasm_std::Timestamp; + use mixnet_contract_common::{Interval, RewardedSetNodeStatus}; + use std::time::Duration; + use time::OffsetDateTime; + + #[test] + fn writing_rewarded_set() { + let mut env = mock_env(); + let mut deps = test_helpers::init_contract(); + let current_state = mixnet_params_storage::CONTRACT_STATE + .load(deps.as_mut().storage) + .unwrap(); + let authorised_sender = mock_info(current_state.rewarding_validator_address.as_str(), &[]); + let full_rewarded_set = (0..current_state.params.mixnode_rewarded_set_size) + .map(|i| format!("identity{:04}", i)) + .collect::>(); + let last_update = 123; + storage::CURRENT_REWARDED_SET_HEIGHT + .save(deps.as_mut().storage, &last_update) + .unwrap(); + + // can only be performed by the permitted validator + let dummy_sender = mock_info("dummy_sender", &[]); + assert_eq!( + Err(ContractError::Unauthorized), + try_write_rewarded_set( + deps.as_mut(), + env.clone(), + dummy_sender, + full_rewarded_set.clone(), + current_state.params.mixnode_active_set_size + ) + ); + + // the sender must use the same active set size as the one defined in the contract + assert_eq!( + Err(ContractError::UnexpectedActiveSetSize { + received: 123, + expected: current_state.params.mixnode_active_set_size + }), + try_write_rewarded_set( + deps.as_mut(), + env.clone(), + authorised_sender.clone(), + full_rewarded_set.clone(), + 123 + ) + ); + + // the sender cannot provide more nodes than the rewarded set size + let mut bigger_set = full_rewarded_set.clone(); + bigger_set.push("another_node".to_string()); + assert_eq!( + Err(ContractError::UnexpectedRewardedSetSize { + received: current_state.params.mixnode_rewarded_set_size + 1, + expected: current_state.params.mixnode_rewarded_set_size + }), + try_write_rewarded_set( + deps.as_mut(), + env.clone(), + authorised_sender.clone(), + bigger_set, + current_state.params.mixnode_active_set_size + ) + ); + + // cannot be performed too soon after a previous update + env.block.height = last_update + 1; + assert_eq!( + Err(ContractError::TooFrequentRewardedSetUpdate { + last_update, + minimum_delay: crate::contract::REWARDED_SET_REFRESH_BLOCKS, + current_height: last_update + 1, + }), + try_write_rewarded_set( + deps.as_mut(), + env.clone(), + authorised_sender.clone(), + full_rewarded_set.clone(), + current_state.params.mixnode_active_set_size + ) + ); + + // after successful rewarded set write, all internal storage structures are updated appropriately + env.block.height = last_update + crate::contract::REWARDED_SET_REFRESH_BLOCKS; + let expected_response = Response::new().add_event(new_change_rewarded_set_event( + current_state.params.mixnode_active_set_size, + current_state.params.mixnode_rewarded_set_size, + full_rewarded_set.len() as u32, + 0, + )); + + assert_eq!( + Ok(expected_response), + try_write_rewarded_set( + deps.as_mut(), + env.clone(), + authorised_sender, + full_rewarded_set.clone(), + current_state.params.mixnode_active_set_size + ) + ); + + for (i, rewarded_node) in full_rewarded_set.into_iter().enumerate() { + if (i as u32) < current_state.params.mixnode_active_set_size { + assert_eq!( + RewardedSetNodeStatus::Active, + storage::REWARDED_SET + .load(deps.as_ref().storage, (env.block.height, rewarded_node)) + .unwrap() + ) + } else { + assert_eq!( + RewardedSetNodeStatus::Standby, + storage::REWARDED_SET + .load(deps.as_ref().storage, (env.block.height, rewarded_node)) + .unwrap() + ) + } + } + assert!(storage::REWARDED_SET_HEIGHTS_FOR_INTERVAL + .has(deps.as_ref().storage, (0, env.block.height))); + assert_eq!( + env.block.height, + storage::CURRENT_REWARDED_SET_HEIGHT + .load(deps.as_ref().storage) + .unwrap() + ); + } + + #[test] + fn advancing_interval() { + let mut env = mock_env(); + let mut deps = test_helpers::init_contract(); + + // 1609459200 = 2021-01-01 + // 1640995200 = 2022-01-01 + // 1641081600 = 2022-01-02 + // 1643673600 = 2022-02-01 + // 1672531200 = 2023-01-01 + + let current_interval = Interval::new( + 0, + OffsetDateTime::from_unix_timestamp(1640995200).unwrap(), + Duration::from_secs(60 * 60 * 720), + ); + let next_interval = current_interval.next_interval(); + storage::CURRENT_INTERVAL + .save(deps.as_mut().storage, ¤t_interval) + .unwrap(); + + // fails if the current interval hasn't finished yet i.e. the new interval hasn't begun + env.block.time = Timestamp::from_seconds(1641081600); + assert_eq!( + Err(ContractError::IntervalNotInProgress { + current_block_time: 1641081600, + interval_start: next_interval.start_unix_timestamp(), + interval_end: next_interval.end_unix_timestamp() + }), + try_advance_interval(env.clone(), deps.as_mut().storage) + ); + + // same if the current blocktime is set to BEFORE the first interval has even begun + // (say we decided to set the first interval to be some time in the future at initialisation) + env.block.time = Timestamp::from_seconds(1609459200); + assert_eq!( + Err(ContractError::IntervalNotInProgress { + current_block_time: 1609459200, + interval_start: next_interval.start_unix_timestamp(), + interval_end: next_interval.end_unix_timestamp() + }), + try_advance_interval(env.clone(), deps.as_mut().storage) + ); + + // works otherwise + + // interval that has just finished + env.block.time = + Timestamp::from_seconds(next_interval.start_unix_timestamp() as u64 + 10000); + let expected_new_interval = current_interval.next_interval(); + let expected_response = + Response::new().add_event(new_advance_interval_event(expected_new_interval)); + assert_eq!( + Ok(expected_response), + try_advance_interval(env.clone(), deps.as_mut().storage) + ); + + // interval way back in the past (i.e. 'somebody' failed to advance it for a long time) + env.block.time = Timestamp::from_seconds(1672531200); + storage::CURRENT_INTERVAL + .save(deps.as_mut().storage, ¤t_interval) + .unwrap(); + let expected_new_interval = current_interval.next_interval(); + let expected_response = + Response::new().add_event(new_advance_interval_event(expected_new_interval)); + assert_eq!( + Ok(expected_response), + try_advance_interval(env.clone(), deps.as_mut().storage) + ); + } +} diff --git a/contracts/mixnet/src/lib.rs b/contracts/mixnet/src/lib.rs index 133da53313..5065505b10 100644 --- a/contracts/mixnet/src/lib.rs +++ b/contracts/mixnet/src/lib.rs @@ -5,6 +5,7 @@ pub mod contract; mod delegations; mod error; mod gateways; +mod interval; mod mixnet_contract_settings; mod mixnodes; mod rewards; diff --git a/contracts/mixnet/src/mixnet_contract_settings/models.rs b/contracts/mixnet/src/mixnet_contract_settings/models.rs index 6b9d5d73f9..dcf9dcbbab 100644 --- a/contracts/mixnet/src/mixnet_contract_settings/models.rs +++ b/contracts/mixnet/src/mixnet_contract_settings/models.rs @@ -11,11 +11,4 @@ pub struct ContractState { pub owner: Addr, // only the owner account can update state pub rewarding_validator_address: Addr, pub params: ContractStateParams, - - // keep track of the changes to the current rewarding interval, - // i.e. at which block has the latest rewarding occurred - // and whether another run is already in progress - pub rewarding_interval_starting_block: u64, - pub latest_rewarding_interval_nonce: u32, - pub rewarding_in_progress: bool, } diff --git a/contracts/mixnet/src/mixnet_contract_settings/queries.rs b/contracts/mixnet/src/mixnet_contract_settings/queries.rs index 68427a8173..59454f14a5 100644 --- a/contracts/mixnet/src/mixnet_contract_settings/queries.rs +++ b/contracts/mixnet/src/mixnet_contract_settings/queries.rs @@ -3,9 +3,7 @@ use super::storage; use cosmwasm_std::{Deps, StdResult}; -use mixnet_contract_common::{ - ContractStateParams, MixnetContractVersion, RewardingIntervalResponse, -}; +use mixnet_contract_common::{ContractStateParams, MixnetContractVersion}; pub(crate) fn query_contract_settings_params(deps: Deps) -> StdResult { storage::CONTRACT_STATE @@ -13,16 +11,6 @@ pub(crate) fn query_contract_settings_params(deps: Deps) -> StdResult StdResult { - let state = storage::CONTRACT_STATE.load(deps.storage)?; - - Ok(RewardingIntervalResponse { - current_rewarding_interval_starting_block: state.rewarding_interval_starting_block, - current_rewarding_interval_nonce: state.latest_rewarding_interval_nonce, - rewarding_in_progress: state.rewarding_in_progress, - }) -} - pub(crate) fn query_contract_version() -> MixnetContractVersion { // as per docs // env! macro will expand to the value of the named environment variable at @@ -59,9 +47,6 @@ pub(crate) mod tests { mixnode_active_set_size: 500, active_set_work_factor: 10, }, - rewarding_interval_starting_block: 123, - latest_rewarding_interval_nonce: 0, - rewarding_in_progress: false, }; storage::CONTRACT_STATE diff --git a/contracts/mixnet/src/rewards/helpers.rs b/contracts/mixnet/src/rewards/helpers.rs index e532c948f5..535dd01be2 100644 --- a/contracts/mixnet/src/rewards/helpers.rs +++ b/contracts/mixnet/src/rewards/helpers.rs @@ -55,7 +55,7 @@ pub(crate) fn update_post_rewarding_storage( pub(crate) fn update_rewarding_status( storage: &mut dyn Storage, - rewarding_interval_nonce: u32, + interval_id: u32, mix_identity: IdentityKey, rewarding_results: RewardingResult, next_start: Option, @@ -64,7 +64,7 @@ pub(crate) fn update_rewarding_status( if let Some(next_start) = next_start { storage::REWARDING_STATUS.save( storage, - (rewarding_interval_nonce, mix_identity), + (interval_id, mix_identity), &RewardingStatus::PendingNextDelegatorPage(PendingDelegatorRewarding { running_results: rewarding_results, next_start, @@ -74,7 +74,7 @@ pub(crate) fn update_rewarding_status( } else { storage::REWARDING_STATUS.save( storage, - (rewarding_interval_nonce, mix_identity), + (interval_id, mix_identity), &RewardingStatus::Complete(rewarding_results), )?; } diff --git a/contracts/mixnet/src/rewards/queries.rs b/contracts/mixnet/src/rewards/queries.rs index d9f82ef7b8..68761345dd 100644 --- a/contracts/mixnet/src/rewards/queries.rs +++ b/contracts/mixnet/src/rewards/queries.rs @@ -17,10 +17,9 @@ pub(crate) fn query_circulating_supply(deps: Deps) -> StdResult { pub(crate) fn query_rewarding_status( deps: Deps, mix_identity: IdentityKey, - rewarding_interval_nonce: u32, + interval_id: u32, ) -> StdResult { - let status = storage::REWARDING_STATUS - .may_load(deps.storage, (rewarding_interval_nonce, mix_identity))?; + let status = storage::REWARDING_STATUS.may_load(deps.storage, (interval_id, mix_identity))?; Ok(MixnodeRewardingStatusResponse { status }) } @@ -39,8 +38,7 @@ pub(crate) mod tests { use super::*; use crate::delegations::transactions::try_delegate_to_mixnode; use crate::rewards::transactions::{ - try_begin_mixnode_rewarding, try_finish_mixnode_rewarding, try_reward_mixnode, - try_reward_next_mixnode_delegators, + try_reward_mixnode, try_reward_next_mixnode_delegators, }; use config::defaults::DENOM; use cosmwasm_std::{coin, Addr}; @@ -70,19 +68,17 @@ pub(crate) mod tests { .is_none() ); - // node was rewarded but for different epoch + // node was rewarded but for different interval let info = mock_info(rewarding_validator_address.as_ref(), &[]); - try_begin_mixnode_rewarding(deps.as_mut(), env.clone(), info.clone(), 1).unwrap(); try_reward_mixnode( deps.as_mut(), env, - info.clone(), + info, node_identity.clone(), tests::fixtures::node_rewarding_params_fixture(100), - 1, + 0, ) .unwrap(); - try_finish_mixnode_rewarding(deps.as_mut(), info, 1).unwrap(); assert!(query_rewarding_status(deps.as_ref(), node_identity, 2) .unwrap() @@ -110,19 +106,17 @@ pub(crate) mod tests { env.block.height += storage::MINIMUM_BLOCK_AGE_FOR_REWARDING; let info = mock_info(rewarding_validator_address.as_ref(), &[]); - try_begin_mixnode_rewarding(deps.as_mut(), env.clone(), info.clone(), 1).unwrap(); try_reward_mixnode( deps.as_mut(), env.clone(), - info.clone(), + info, node_identity.clone(), tests::fixtures::node_rewarding_params_fixture(100), - 1, + 0, ) .unwrap(); - try_finish_mixnode_rewarding(deps.as_mut(), info, 1).unwrap(); - let res = query_rewarding_status(deps.as_ref(), node_identity, 1).unwrap(); + let res = query_rewarding_status(deps.as_ref(), node_identity, 0).unwrap(); assert!(matches!(res.status, Some(RewardingStatus::Complete(..)))); match res.status.unwrap() { @@ -158,9 +152,9 @@ pub(crate) mod tests { } env.block.height += storage::MINIMUM_BLOCK_AGE_FOR_REWARDING; + test_helpers::update_env_and_progress_interval(&mut env, deps.as_mut().storage); let info = mock_info(rewarding_validator_address.as_ref(), &[]); - try_begin_mixnode_rewarding(deps.as_mut(), env.clone(), info.clone(), 2).unwrap(); try_reward_mixnode( deps.as_mut(), @@ -168,15 +162,15 @@ pub(crate) mod tests { info.clone(), node_identity.clone(), tests::fixtures::node_rewarding_params_fixture(100), - 2, + 1, ) .unwrap(); // rewards all pending - try_reward_next_mixnode_delegators(deps.as_mut(), info, node_identity.to_string(), 2) + try_reward_next_mixnode_delegators(deps.as_mut(), info, node_identity.to_string(), 1) .unwrap(); - let res = query_rewarding_status(deps.as_ref(), node_identity, 2).unwrap(); + let res = query_rewarding_status(deps.as_ref(), node_identity, 1).unwrap(); assert!(matches!(res.status, Some(RewardingStatus::Complete(..)))); match res.status.unwrap() { @@ -223,7 +217,6 @@ pub(crate) mod tests { env.block.height += storage::MINIMUM_BLOCK_AGE_FOR_REWARDING; let info = mock_info(rewarding_validator_address.as_ref(), &[]); - try_begin_mixnode_rewarding(deps.as_mut(), env.clone(), info.clone(), 1).unwrap(); try_reward_mixnode( deps.as_mut(), @@ -231,11 +224,11 @@ pub(crate) mod tests { info, node_identity.clone(), tests::fixtures::node_rewarding_params_fixture(100), - 1, + 0, ) .unwrap(); - let res = query_rewarding_status(deps.as_ref(), node_identity, 1).unwrap(); + let res = query_rewarding_status(deps.as_ref(), node_identity, 0).unwrap(); assert!(matches!( res.status, Some(RewardingStatus::PendingNextDelegatorPage(..)) diff --git a/contracts/mixnet/src/rewards/storage.rs b/contracts/mixnet/src/rewards/storage.rs index 838b68f092..a56e787a91 100644 --- a/contracts/mixnet/src/rewards/storage.rs +++ b/contracts/mixnet/src/rewards/storage.rs @@ -11,11 +11,9 @@ pub(crate) const REWARD_POOL: Item = Item::new("pool"); // TODO: Do we need a migration for this? pub(crate) const REWARDING_STATUS: Map<(u32, IdentityKey), RewardingStatus> = Map::new("rm"); -// approximately 1 day (assuming 5s per block) -pub(crate) const MINIMUM_BLOCK_AGE_FOR_REWARDING: u64 = 17280; - -// approximately 30min (assuming 5s per block) -pub(crate) const MAX_REWARDING_DURATION_IN_BLOCKS: u64 = 360; +// approximately 1 week (assuming 5s per block) +// i.e. approximately quarter of the interval (there are 3600 * 60 * 7 = 604800 seconds in a week, i.e. ~604800 / 5 = 120960 blocks) +pub(crate) const MINIMUM_BLOCK_AGE_FOR_REWARDING: u64 = 120960; #[allow(dead_code)] pub fn incr_reward_pool( diff --git a/contracts/mixnet/src/rewards/transactions.rs b/contracts/mixnet/src/rewards/transactions.rs index 5a838239d0..1f641fdfaa 100644 --- a/contracts/mixnet/src/rewards/transactions.rs +++ b/contracts/mixnet/src/rewards/transactions.rs @@ -4,15 +4,16 @@ use super::storage; use crate::delegations::storage as delegations_storage; use crate::error::ContractError; +use crate::interval::storage as interval_storage; use crate::mixnet_contract_settings::storage as mixnet_params_storage; use crate::mixnodes::storage as mixnodes_storage; use crate::rewards::helpers; use cosmwasm_std::{Addr, DepsMut, Env, MessageInfo, Response, StdResult, Storage, Uint128}; use cw_storage_plus::{Bound, PrimaryKey}; use mixnet_contract_common::events::{ - new_begin_rewarding_event, new_finish_rewarding_event, new_mix_delegators_rewarding_event, - new_mix_operator_rewarding_event, new_not_found_mix_operator_rewarding_event, - new_too_fresh_bond_mix_operator_rewarding_event, new_zero_uptime_mix_operator_rewarding_event, + new_mix_delegators_rewarding_event, new_mix_operator_rewarding_event, + new_not_found_mix_operator_rewarding_event, new_too_fresh_bond_mix_operator_rewarding_event, + new_zero_uptime_mix_operator_rewarding_event, }; use mixnet_contract_common::mixnode::{DelegatorRewardParams, NodeRewardParams}; use mixnet_contract_common::{ @@ -28,18 +29,17 @@ struct MixDelegationRewardingResult { /// Checks whether under the current context, any rewarding-related functionalities can be called. /// The following must be true: /// - the call has originated from the address of the authorised rewarding validator, -/// - the rewarding procedure has been initialised and has not concluded yet, /// - the call has been made with the nonce corresponding to the current rewarding procedure, /// /// # Arguments /// /// * `storage`: reference (kinda) to the underlying storage pool of the contract used to read the current state /// * `info`: contains the essential info for authorization, such as identity of the call -/// * `rewarding_interval_nonce`: nonce of the rewarding procedure sent alongside the call +/// * `interval_id`: expected id of the current interval sent alongside the call fn verify_rewarding_state( storage: &dyn Storage, info: MessageInfo, - rewarding_interval_nonce: u32, + interval_id: u32, ) -> Result<(), ContractError> { let state = mixnet_params_storage::CONTRACT_STATE.load(storage)?; @@ -48,72 +48,21 @@ fn verify_rewarding_state( return Err(ContractError::Unauthorized); } - // check if rewarding is currently in progress, if not reject the transaction - if !state.rewarding_in_progress { - return Err(ContractError::RewardingNotInProgress); - } + let current_interval = interval_storage::CURRENT_INTERVAL.load(storage)?; - // make sure the transaction is sent for the correct rewarding interval + // make sure the transaction is sent for the correct interval // (guard ourselves against somebody trying to send stale results; // realistically it's never going to happen in a single rewarding validator case - // but this check is not expensive (since we already had to read the state), - // so we might as well) - if rewarding_interval_nonce != state.latest_rewarding_interval_nonce { - Err(ContractError::InvalidRewardingIntervalNonce { - received: rewarding_interval_nonce, - expected: state.latest_rewarding_interval_nonce, + if interval_id != current_interval.id() { + Err(ContractError::InvalidIntervalId { + received: interval_id, + expected: current_interval.id(), }) } else { Ok(()) } } -// Note: this function is designed to work with only a single validator entity distributing rewards -// The main purpose of this function is to update `latest_rewarding_interval_nonce` which -// will trigger a different seed selection for the pseudorandom generation of the "demanded" set of mixnodes. -pub(crate) fn try_begin_mixnode_rewarding( - deps: DepsMut, - env: Env, - info: MessageInfo, - rewarding_interval_nonce: u32, -) -> Result { - let mut state = mixnet_params_storage::CONTRACT_STATE.load(deps.storage)?; - - // check if this is executed by the permitted validator, if not reject the transaction - if info.sender != state.rewarding_validator_address { - return Err(ContractError::Unauthorized); - } - - // check whether sufficient number of blocks already elapsed since the previous rewarding happened - // (this implies the validator responsible for rewarding in the previous interval did not call - // `try_finish_mixnode_rewarding` - perhaps they crashed or something. Regardless of the reason - // it shouldn't prevent anyone from distributing rewards in the following interval) - // Do note, however, that calling `try_finish_mixnode_rewarding` is crucial as otherwise the - // "demanded" set won't get updated on the validator API side - if state.rewarding_in_progress - && state.rewarding_interval_starting_block + storage::MAX_REWARDING_DURATION_IN_BLOCKS - > env.block.height - { - return Err(ContractError::RewardingInProgress); - } - - // make sure the validator is in sync with the contract state - if rewarding_interval_nonce != state.latest_rewarding_interval_nonce + 1 { - return Err(ContractError::InvalidRewardingIntervalNonce { - received: rewarding_interval_nonce, - expected: state.latest_rewarding_interval_nonce + 1, - }); - } - - state.rewarding_interval_starting_block = env.block.height; - state.latest_rewarding_interval_nonce = rewarding_interval_nonce; - state.rewarding_in_progress = true; - - mixnet_params_storage::CONTRACT_STATE.save(deps.storage, &state)?; - - Ok(Response::new().add_event(new_begin_rewarding_event(rewarding_interval_nonce))) -} - fn reward_mix_delegators( storage: &mut dyn Storage, mix_identity: IdentityKey, @@ -195,14 +144,11 @@ pub(crate) fn try_reward_next_mixnode_delegators( deps: DepsMut, info: MessageInfo, mix_identity: IdentityKey, - rewarding_interval_nonce: u32, + interval_id: u32, ) -> Result { - verify_rewarding_state(deps.storage, info, rewarding_interval_nonce)?; + verify_rewarding_state(deps.storage, info, interval_id)?; - match storage::REWARDING_STATUS.may_load( - deps.storage, - (rewarding_interval_nonce, mix_identity.clone()), - )? { + match storage::REWARDING_STATUS.may_load(deps.storage, (interval_id, mix_identity.clone()))? { None => { // we haven't called 'regular' try_reward_mixnode, i.e. the operator itself // was not rewarded yet @@ -239,7 +185,7 @@ pub(crate) fn try_reward_next_mixnode_delegators( helpers::update_rewarding_status( deps.storage, - rewarding_interval_nonce, + interval_id, mix_identity.clone(), rewarding_results, delegation_rewarding_result.start_next, @@ -248,7 +194,7 @@ pub(crate) fn try_reward_next_mixnode_delegators( Ok( Response::new().add_event(new_mix_delegators_rewarding_event( - rewarding_interval_nonce, + interval_id, &mix_identity, round_increase, more_delegators, @@ -264,15 +210,12 @@ pub(crate) fn try_reward_mixnode( info: MessageInfo, mix_identity: IdentityKey, params: NodeRewardParams, - rewarding_interval_nonce: u32, + interval_id: u32, ) -> Result { - verify_rewarding_state(deps.storage, info, rewarding_interval_nonce)?; + verify_rewarding_state(deps.storage, info, interval_id)?; // check if the mixnode hasn't been rewarded in this rewarding interval already - match storage::REWARDING_STATUS.may_load( - deps.storage, - (rewarding_interval_nonce, mix_identity.clone()), - )? { + match storage::REWARDING_STATUS.may_load(deps.storage, (interval_id, mix_identity.clone()))? { None => (), Some(RewardingStatus::Complete(_)) => { return Err(ContractError::MixnodeAlreadyRewarded { @@ -293,7 +236,7 @@ pub(crate) fn try_reward_mixnode( None => { return Ok( Response::new().add_event(new_not_found_mix_operator_rewarding_event( - rewarding_interval_nonce, + interval_id, &mix_identity, )), ) @@ -304,13 +247,13 @@ pub(crate) fn try_reward_mixnode( if current_bond.block_height + storage::MINIMUM_BLOCK_AGE_FOR_REWARDING > env.block.height { storage::REWARDING_STATUS.save( deps.storage, - (rewarding_interval_nonce, mix_identity.clone()), + (interval_id, mix_identity.clone()), &RewardingStatus::Complete(Default::default()), )?; return Ok( Response::new().add_event(new_too_fresh_bond_mix_operator_rewarding_event( - rewarding_interval_nonce, + interval_id, &mix_identity, )), ); @@ -320,13 +263,13 @@ pub(crate) fn try_reward_mixnode( if params.uptime() == 0 { storage::REWARDING_STATUS.save( deps.storage, - (rewarding_interval_nonce, mix_identity.clone()), + (interval_id, mix_identity.clone()), &RewardingStatus::Complete(Default::default()), )?; return Ok( Response::new().add_event(new_zero_uptime_mix_operator_rewarding_event( - rewarding_interval_nonce, + interval_id, &mix_identity, )), ); @@ -360,7 +303,7 @@ pub(crate) fn try_reward_mixnode( helpers::update_rewarding_status( deps.storage, - rewarding_interval_nonce, + interval_id, mix_identity.clone(), rewarding_results, delegation_rewarding_result.start_next, @@ -368,7 +311,7 @@ pub(crate) fn try_reward_mixnode( )?; Ok(Response::new().add_event(new_mix_operator_rewarding_event( - rewarding_interval_nonce, + interval_id, &mix_identity, node_reward_result, operator_reward, @@ -377,36 +320,6 @@ pub(crate) fn try_reward_mixnode( ))) } -pub(crate) fn try_finish_mixnode_rewarding( - deps: DepsMut, - info: MessageInfo, - rewarding_interval_nonce: u32, -) -> Result { - let mut state = mixnet_params_storage::CONTRACT_STATE.load(deps.storage)?; - - // check if this is executed by the permitted validator, if not reject the transaction - if info.sender != state.rewarding_validator_address { - return Err(ContractError::Unauthorized); - } - - if !state.rewarding_in_progress { - return Err(ContractError::RewardingNotInProgress); - } - - // make sure the validator is in sync with the contract state - if rewarding_interval_nonce != state.latest_rewarding_interval_nonce { - return Err(ContractError::InvalidRewardingIntervalNonce { - received: rewarding_interval_nonce, - expected: state.latest_rewarding_interval_nonce, - }); - } - - state.rewarding_in_progress = false; - mixnet_params_storage::CONTRACT_STATE.save(deps.storage, &state)?; - - Ok(Response::new().add_event(new_finish_rewarding_event(rewarding_interval_nonce))) -} - #[cfg(test)] pub mod tests { use super::*; @@ -416,9 +329,7 @@ pub mod tests { use crate::mixnet_contract_settings::storage as mixnet_params_storage; use crate::mixnodes::storage as mixnodes_storage; use crate::mixnodes::storage::StoredMixnodeBond; - use crate::rewards::transactions::{ - try_begin_mixnode_rewarding, try_finish_mixnode_rewarding, try_reward_mixnode, - }; + use crate::rewards::transactions::try_reward_mixnode; use crate::support::tests; use crate::support::tests::test_helpers; use config::defaults::DENOM; @@ -434,352 +345,10 @@ pub mod tests { use mixnet_contract_common::mixnode::NodeRewardParams; use mixnet_contract_common::{Delegation, IdentityKey, Layer, MixNode}; - #[cfg(test)] - mod beginning_mixnode_rewarding { - use super::*; - use crate::rewards::transactions::try_begin_mixnode_rewarding; - use crate::support::tests::test_helpers; - use cosmwasm_std::testing::mock_env; - - #[test] - fn can_only_be_called_by_specified_validator_address() { - let mut deps = test_helpers::init_contract(); - let env = mock_env(); - let current_state = mixnet_params_storage::CONTRACT_STATE - .load(deps.as_mut().storage) - .unwrap(); - let rewarding_validator_address = current_state.rewarding_validator_address; - - let res = try_begin_mixnode_rewarding( - deps.as_mut(), - env.clone(), - mock_info("not-the-approved-validator", &[]), - 1, - ); - assert_eq!(Err(ContractError::Unauthorized), res); - - let res = try_begin_mixnode_rewarding( - deps.as_mut(), - env, - mock_info(rewarding_validator_address.as_ref(), &[]), - 1, - ); - assert!(res.is_ok()) - } - - #[test] - fn cannot_be_called_if_rewarding_is_already_in_progress_with_little_day() { - let mut deps = test_helpers::init_contract(); - let env = mock_env(); - let current_state = mixnet_params_storage::CONTRACT_STATE - .load(deps.as_mut().storage) - .unwrap(); - let rewarding_validator_address = current_state.rewarding_validator_address; - - try_begin_mixnode_rewarding( - deps.as_mut(), - env.clone(), - mock_info(rewarding_validator_address.as_ref(), &[]), - 1, - ) - .unwrap(); - - let res = try_begin_mixnode_rewarding( - deps.as_mut(), - env, - mock_info(rewarding_validator_address.as_ref(), &[]), - 2, - ); - assert_eq!(Err(ContractError::RewardingInProgress), res); - } - - #[test] - fn can_be_called_if_rewarding_is_in_progress_if_sufficient_number_of_blocks_elapsed() { - let mut deps = test_helpers::init_contract(); - let env = mock_env(); - let current_state = mixnet_params_storage::CONTRACT_STATE - .load(deps.as_mut().storage) - .unwrap(); - let rewarding_validator_address = current_state.rewarding_validator_address; - - try_begin_mixnode_rewarding( - deps.as_mut(), - env.clone(), - mock_info(rewarding_validator_address.as_ref(), &[]), - 1, - ) - .unwrap(); - - let mut new_env = env.clone(); - - new_env.block.height = env.block.height + storage::MAX_REWARDING_DURATION_IN_BLOCKS; - - let res = try_begin_mixnode_rewarding( - deps.as_mut(), - new_env, - mock_info(rewarding_validator_address.as_ref(), &[]), - 2, - ); - assert!(res.is_ok()); - } - - #[test] - fn provided_nonce_must_be_equal_the_current_plus_one() { - let mut deps = test_helpers::init_contract(); - let env = mock_env(); - let mut current_state = mixnet_params_storage::CONTRACT_STATE - .load(deps.as_mut().storage) - .unwrap(); - current_state.latest_rewarding_interval_nonce = 42; - mixnet_params_storage::CONTRACT_STATE - .save(deps.as_mut().storage, ¤t_state) - .unwrap(); - - let rewarding_validator_address = current_state.rewarding_validator_address; - - let res = try_begin_mixnode_rewarding( - deps.as_mut(), - env.clone(), - mock_info(rewarding_validator_address.as_ref(), &[]), - 11, - ); - assert_eq!( - Err(ContractError::InvalidRewardingIntervalNonce { - received: 11, - expected: 43 - }), - res - ); - - let res = try_begin_mixnode_rewarding( - deps.as_mut(), - env.clone(), - mock_info(rewarding_validator_address.as_ref(), &[]), - 44, - ); - assert_eq!( - Err(ContractError::InvalidRewardingIntervalNonce { - received: 44, - expected: 43 - }), - res - ); - - let res = try_begin_mixnode_rewarding( - deps.as_mut(), - env.clone(), - mock_info(rewarding_validator_address.as_ref(), &[]), - 42, - ); - assert_eq!( - Err(ContractError::InvalidRewardingIntervalNonce { - received: 42, - expected: 43 - }), - res - ); - - let res = try_begin_mixnode_rewarding( - deps.as_mut(), - env, - mock_info(rewarding_validator_address.as_ref(), &[]), - 43, - ); - assert!(res.is_ok()) - } - - #[test] - fn updates_contract_state() { - let mut deps = test_helpers::init_contract(); - let env = mock_env(); - let start_state = mixnet_params_storage::CONTRACT_STATE - .load(deps.as_mut().storage) - .unwrap(); - let rewarding_validator_address = start_state.rewarding_validator_address; - - try_begin_mixnode_rewarding( - deps.as_mut(), - env.clone(), - mock_info(rewarding_validator_address.as_ref(), &[]), - 1, - ) - .unwrap(); - - let new_state = mixnet_params_storage::CONTRACT_STATE - .load(deps.as_mut().storage) - .unwrap(); - assert!(new_state.rewarding_in_progress); - assert_eq!( - new_state.rewarding_interval_starting_block, - env.block.height - ); - assert_eq!( - start_state.latest_rewarding_interval_nonce + 1, - new_state.latest_rewarding_interval_nonce - ); - } - } - - #[cfg(test)] - mod finishing_mixnode_rewarding { - use super::*; - use crate::rewards::transactions::{ - try_begin_mixnode_rewarding, try_finish_mixnode_rewarding, - }; - use crate::support::tests::test_helpers; - - #[test] - fn can_only_be_called_by_specified_validator_address() { - let mut deps = test_helpers::init_contract(); - let env = mock_env(); - let current_state = mixnet_params_storage::CONTRACT_STATE - .load(deps.as_mut().storage) - .unwrap(); - let rewarding_validator_address = current_state.rewarding_validator_address; - - try_begin_mixnode_rewarding( - deps.as_mut(), - env, - mock_info(rewarding_validator_address.as_ref(), &[]), - 1, - ) - .unwrap(); - - let res = try_finish_mixnode_rewarding( - deps.as_mut(), - mock_info("not-the-approved-validator", &[]), - 1, - ); - assert_eq!(Err(ContractError::Unauthorized), res); - - let res = try_finish_mixnode_rewarding( - deps.as_mut(), - mock_info(rewarding_validator_address.as_ref(), &[]), - 1, - ); - assert!(res.is_ok()) - } - - #[test] - fn cannot_be_called_if_rewarding_is_not_in_progress() { - let mut deps = test_helpers::init_contract(); - let current_state = mixnet_params_storage::CONTRACT_STATE - .load(deps.as_mut().storage) - .unwrap(); - let rewarding_validator_address = current_state.rewarding_validator_address; - - let res = try_finish_mixnode_rewarding( - deps.as_mut(), - mock_info(rewarding_validator_address.as_ref(), &[]), - 0, - ); - assert_eq!(Err(ContractError::RewardingNotInProgress), res); - } - - #[test] - fn provided_nonce_must_be_equal_the_current_one() { - let mut deps = test_helpers::init_contract(); - let env = mock_env(); - let mut current_state = mixnet_params_storage::CONTRACT_STATE - .load(deps.as_mut().storage) - .unwrap(); - current_state.latest_rewarding_interval_nonce = 42; - mixnet_params_storage::CONTRACT_STATE - .save(deps.as_mut().storage, ¤t_state) - .unwrap(); - - let rewarding_validator_address = current_state.rewarding_validator_address; - - try_begin_mixnode_rewarding( - deps.as_mut(), - env, - mock_info(rewarding_validator_address.as_ref(), &[]), - 43, - ) - .unwrap(); - - let res = try_finish_mixnode_rewarding( - deps.as_mut(), - mock_info(rewarding_validator_address.as_ref(), &[]), - 11, - ); - assert_eq!( - Err(ContractError::InvalidRewardingIntervalNonce { - received: 11, - expected: 43 - }), - res - ); - - let res = try_finish_mixnode_rewarding( - deps.as_mut(), - mock_info(rewarding_validator_address.as_ref(), &[]), - 44, - ); - assert_eq!( - Err(ContractError::InvalidRewardingIntervalNonce { - received: 44, - expected: 43 - }), - res - ); - - let res = try_finish_mixnode_rewarding( - deps.as_mut(), - mock_info(rewarding_validator_address.as_ref(), &[]), - 42, - ); - assert_eq!( - Err(ContractError::InvalidRewardingIntervalNonce { - received: 42, - expected: 43 - }), - res - ); - - let res = try_finish_mixnode_rewarding( - deps.as_mut(), - mock_info(rewarding_validator_address.as_ref(), &[]), - 43, - ); - assert!(res.is_ok()) - } - - #[test] - fn updates_contract_state() { - let mut deps = test_helpers::init_contract(); - let env = mock_env(); - let current_state = mixnet_params_storage::CONTRACT_STATE - .load(deps.as_mut().storage) - .unwrap(); - let rewarding_validator_address = current_state.rewarding_validator_address; - - try_begin_mixnode_rewarding( - deps.as_mut(), - env, - mock_info(rewarding_validator_address.as_ref(), &[]), - 1, - ) - .unwrap(); - - try_finish_mixnode_rewarding( - deps.as_mut(), - mock_info(rewarding_validator_address.as_ref(), &[]), - 1, - ) - .unwrap(); - - let new_state = mixnet_params_storage::CONTRACT_STATE - .load(deps.as_mut().storage) - .unwrap(); - assert!(!new_state.rewarding_in_progress); - } - } - #[test] - fn rewarding_mixnodes_outside_rewarding_period() { + fn rewarding_mixnodes_with_incorrect_interval_id() { let mut deps = test_helpers::init_contract(); - let env = mock_env(); + let mut env = mock_env(); let current_state = mixnet_params_storage::CONTRACT_STATE .load(deps.as_mut().storage) .unwrap(); @@ -794,6 +363,7 @@ pub mod tests { ); let info = mock_info(rewarding_validator_address.as_ref(), &[]); + let res = try_reward_mixnode( deps.as_mut(), env.clone(), @@ -802,52 +372,10 @@ pub mod tests { tests::fixtures::node_rewarding_params_fixture(100), 1, ); - assert_eq!(Err(ContractError::RewardingNotInProgress), res); - - try_begin_mixnode_rewarding(deps.as_mut(), env.clone(), info.clone(), 1).unwrap(); - - let res = try_reward_mixnode( - deps.as_mut(), - env, - info, - node_identity, - tests::fixtures::node_rewarding_params_fixture(100), - 1, - ); - assert!(res.is_ok()) - } - - #[test] - fn rewarding_mixnodes_with_incorrect_rewarding_nonce() { - let mut deps = test_helpers::init_contract(); - let env = mock_env(); - let current_state = mixnet_params_storage::CONTRACT_STATE - .load(deps.as_mut().storage) - .unwrap(); - let rewarding_validator_address = current_state.rewarding_validator_address; - - // bond the node - let node_owner: Addr = Addr::unchecked("node-owner"); - let node_identity = test_helpers::add_mixnode( - node_owner.as_str(), - tests::fixtures::good_mixnode_pledge(), - deps.as_mut(), - ); - - let info = mock_info(rewarding_validator_address.as_ref(), &[]); - try_begin_mixnode_rewarding(deps.as_mut(), env.clone(), info.clone(), 1).unwrap(); - let res = try_reward_mixnode( - deps.as_mut(), - env.clone(), - info.clone(), - node_identity.clone(), - tests::fixtures::node_rewarding_params_fixture(100), - 0, - ); assert_eq!( - Err(ContractError::InvalidRewardingIntervalNonce { - received: 0, - expected: 1 + Err(ContractError::InvalidIntervalId { + received: 1, + expected: 0 }), res ); @@ -861,28 +389,40 @@ pub mod tests { 2, ); assert_eq!( - Err(ContractError::InvalidRewardingIntervalNonce { + Err(ContractError::InvalidIntervalId { received: 2, - expected: 1 + expected: 0 }), res ); let res = try_reward_mixnode( deps.as_mut(), - env, + env.clone(), + info.clone(), + node_identity.clone(), + tests::fixtures::node_rewarding_params_fixture(100), + 0, + ); + assert!(res.is_ok()); + + test_helpers::update_env_and_progress_interval(&mut env, deps.as_mut().storage); + + let res = try_reward_mixnode( + deps.as_mut(), + env.clone(), info, node_identity, tests::fixtures::node_rewarding_params_fixture(100), 1, ); - assert!(res.is_ok()) + assert!(res.is_ok()); } #[test] fn attempting_rewarding_mixnode_multiple_times_per_interval() { let mut deps = test_helpers::init_contract(); - let env = mock_env(); + let mut env = mock_env(); let current_state = mixnet_params_storage::CONTRACT_STATE .load(deps.as_mut().storage) .unwrap(); @@ -897,7 +437,6 @@ pub mod tests { ); let info = mock_info(rewarding_validator_address.as_ref(), &[]); - try_begin_mixnode_rewarding(deps.as_mut(), env.clone(), info.clone(), 1).unwrap(); // first reward goes through just fine let res = try_reward_mixnode( @@ -906,7 +445,7 @@ pub mod tests { info.clone(), node_identity.clone(), tests::fixtures::node_rewarding_params_fixture(100), - 1, + 0, ); assert!(res.is_ok()); @@ -917,7 +456,7 @@ pub mod tests { info.clone(), node_identity.clone(), tests::fixtures::node_rewarding_params_fixture(100), - 1, + 0, ); assert_eq!( Err(ContractError::MixnodeAlreadyRewarded { @@ -927,8 +466,7 @@ pub mod tests { ); // but rewarding the same node in the following interval is fine again - try_finish_mixnode_rewarding(deps.as_mut(), info.clone(), 1).unwrap(); - try_begin_mixnode_rewarding(deps.as_mut(), env.clone(), info.clone(), 2).unwrap(); + test_helpers::update_env_and_progress_interval(&mut env, deps.as_mut().storage); let res = try_reward_mixnode( deps.as_mut(), @@ -936,7 +474,7 @@ pub mod tests { info, node_identity, tests::fixtures::node_rewarding_params_fixture(100), - 2, + 1, ); assert!(res.is_ok()); } @@ -996,17 +534,16 @@ pub mod tests { .unwrap(); let info = mock_info(rewarding_validator_address.as_ref(), &[]); - try_begin_mixnode_rewarding(deps.as_mut(), env.clone(), info.clone(), 1).unwrap(); + let res = try_reward_mixnode( deps.as_mut(), env.clone(), info.clone(), node_identity.clone(), tests::fixtures::node_rewarding_params_fixture(100), - 1, + 0, ) .unwrap(); - try_finish_mixnode_rewarding(deps.as_mut(), info, 1).unwrap(); assert_eq!( initial_bond, @@ -1030,19 +567,18 @@ pub mod tests { // reward can happen now, but only for bonded node env.block.height += 1; + test_helpers::update_env_and_progress_interval(&mut env, deps.as_mut().storage); let info = mock_info(rewarding_validator_address.as_ref(), &[]); - try_begin_mixnode_rewarding(deps.as_mut(), env.clone(), info.clone(), 2).unwrap(); let res = try_reward_mixnode( deps.as_mut(), env.clone(), info.clone(), node_identity.clone(), tests::fixtures::node_rewarding_params_fixture(100), - 2, + 1, ) .unwrap(); - try_finish_mixnode_rewarding(deps.as_mut(), info, 2).unwrap(); assert!( test_helpers::read_mixnode_pledge_amount(deps.as_ref().storage, &node_identity) @@ -1074,6 +610,7 @@ pub mod tests { // reward happens now, both for node owner and delegators env.block.height += storage::MINIMUM_BLOCK_AGE_FOR_REWARDING - 1; + test_helpers::update_env_and_progress_interval(&mut env, deps.as_mut().storage); let pledge_before_rewarding = test_helpers::read_mixnode_pledge_amount(deps.as_ref().storage, &node_identity) @@ -1081,17 +618,15 @@ pub mod tests { .u128(); let info = mock_info(rewarding_validator_address.as_ref(), &[]); - try_begin_mixnode_rewarding(deps.as_mut(), env.clone(), info.clone(), 3).unwrap(); let res = try_reward_mixnode( deps.as_mut(), env, info.clone(), node_identity.clone(), tests::fixtures::node_rewarding_params_fixture(100), - 3, + 2, ) .unwrap(); - try_finish_mixnode_rewarding(deps.as_mut(), info, 3).unwrap(); assert!( test_helpers::read_mixnode_pledge_amount(deps.as_ref().storage, &node_identity) @@ -1124,7 +659,7 @@ pub mod tests { #[test] fn test_tokenomics_rewarding() { - use crate::contract::{EPOCH_REWARD_PERCENT, INITIAL_REWARD_POOL}; + use crate::contract::{INITIAL_REWARD_POOL, INTERVAL_REWARD_PERCENT}; type U128 = fixed::types::U75F53; @@ -1134,7 +669,7 @@ pub mod tests { .load(deps.as_ref().storage) .unwrap(); let rewarding_validator_address = current_state.rewarding_validator_address; - let period_reward_pool = (INITIAL_REWARD_POOL / 100) * EPOCH_REWARD_PERCENT as u128; + let period_reward_pool = (INITIAL_REWARD_POOL / 100) * INTERVAL_REWARD_PERCENT as u128; assert_eq!(period_reward_pool, 5_000_000_000_000); let rewarded_set_size = 200; // Imagining our reward set size is 200 let active_set_size = 100; @@ -1166,14 +701,6 @@ pub mod tests { .unwrap(); let info = mock_info(rewarding_validator_address.as_ref(), &[]); - try_begin_mixnode_rewarding( - deps.as_mut(), - env.clone(), - mock_info(rewarding_validator_address.as_ref(), &[]), - 1, - ) - .unwrap(); - env.block.height += 2 * storage::MINIMUM_BLOCK_AGE_FOR_REWARDING; let mix_1 = mixnodes_storage::read_full_mixnode_bond(&deps.storage, &node_identity) @@ -1231,7 +758,7 @@ pub mod tests { .u128(); assert_eq!(pre_reward_delegation, 10_000_000_000); - try_reward_mixnode(deps.as_mut(), env, info, node_identity.clone(), params, 1).unwrap(); + try_reward_mixnode(deps.as_mut(), env, info, node_identity.clone(), params, 0).unwrap(); assert_eq!( test_helpers::read_mixnode_pledge_amount(&deps.storage, &node_identity) @@ -1257,7 +784,7 @@ pub mod tests { // it's all correctly saved match storage::REWARDING_STATUS - .load(deps.as_ref().storage, (1u32, node_identity)) + .load(deps.as_ref().storage, (0u32, node_identity)) .unwrap() { RewardingStatus::Complete(result) => assert_eq!( @@ -1317,21 +844,13 @@ pub mod tests { env.block.height += storage::MINIMUM_BLOCK_AGE_FOR_REWARDING; let info = mock_info(rewarding_validator_address.as_ref(), &[]); - try_begin_mixnode_rewarding( - deps.as_mut(), - env.clone(), - mock_info(rewarding_validator_address.as_ref(), &[]), - 1, - ) - .unwrap(); - let res = try_reward_mixnode( deps.as_mut(), env, info, node_identity.clone(), tests::fixtures::node_rewarding_params_fixture(100), - 1, + 0, ) .unwrap(); assert_eq!( @@ -1339,13 +858,6 @@ pub mod tests { must_find_attribute(&res.events[0], FURTHER_DELEGATIONS_TO_REWARD_KEY) ); - try_finish_mixnode_rewarding( - deps.as_mut(), - mock_info(rewarding_validator_address.as_ref(), &[]), - 1, - ) - .unwrap(); - for i in 0..10 { let delegation = test_helpers::read_delegation( &deps.storage, @@ -1399,21 +911,13 @@ pub mod tests { env.block.height += storage::MINIMUM_BLOCK_AGE_FOR_REWARDING; let info = mock_info(rewarding_validator_address.as_ref(), &[]); - try_begin_mixnode_rewarding( - deps.as_mut(), - env.clone(), - mock_info(rewarding_validator_address.as_ref(), &[]), - 1, - ) - .unwrap(); - let res = try_reward_mixnode( deps.as_mut(), env, info, node_identity.clone(), tests::fixtures::node_rewarding_params_fixture(100), - 1, + 0, ) .unwrap(); assert_eq!( @@ -1421,13 +925,6 @@ pub mod tests { must_find_attribute(&res.events[0], FURTHER_DELEGATIONS_TO_REWARD_KEY) ); - try_finish_mixnode_rewarding( - deps.as_mut(), - mock_info(rewarding_validator_address.as_ref(), &[]), - 1, - ) - .unwrap(); - for i in 0..MIXNODE_DELEGATORS_PAGE_LIMIT { let delegation = test_helpers::read_delegation( &deps.storage, @@ -1481,21 +978,13 @@ pub mod tests { env.block.height += storage::MINIMUM_BLOCK_AGE_FOR_REWARDING; let info = mock_info(rewarding_validator_address.as_ref(), &[]); - try_begin_mixnode_rewarding( - deps.as_mut(), - env.clone(), - mock_info(rewarding_validator_address.as_ref(), &[]), - 1, - ) - .unwrap(); - let res = try_reward_mixnode( deps.as_mut(), env, info, node_identity.clone(), tests::fixtures::node_rewarding_params_fixture(100), - 1, + 0, ) .unwrap(); assert_eq!( @@ -1503,13 +992,6 @@ pub mod tests { must_find_attribute(&res.events[0], FURTHER_DELEGATIONS_TO_REWARD_KEY) ); - try_finish_mixnode_rewarding( - deps.as_mut(), - mock_info(rewarding_validator_address.as_ref(), &[]), - 1, - ) - .unwrap(); - for i in 0..MIXNODE_DELEGATORS_PAGE_LIMIT { let delegation = test_helpers::read_delegation( &deps.storage, @@ -1707,46 +1189,19 @@ pub mod tests { assert_eq!(Err(ContractError::Unauthorized), res); } - #[test] - fn cannot_be_called_if_rewarding_is_not_in_progress() { - let mut deps = test_helpers::init_contract(); - let current_state = mixnet_params_storage::CONTRACT_STATE - .load(deps.as_mut().storage) - .unwrap(); - let rewarding_validator_address = current_state.rewarding_validator_address; - - let res = try_reward_next_mixnode_delegators( - deps.as_mut(), - mock_info(rewarding_validator_address.as_ref(), &[]), - "alice's mixnode".to_string(), - 1, - ); - - assert_eq!(Err(ContractError::RewardingNotInProgress), res); - } - #[test] fn cannot_be_called_if_mixnodes_operator_wasnt_rewarded() { let mut deps = test_helpers::init_contract(); - let env = mock_env(); let current_state = mixnet_params_storage::CONTRACT_STATE .load(deps.as_mut().storage) .unwrap(); let rewarding_validator_address = current_state.rewarding_validator_address; - try_begin_mixnode_rewarding( - deps.as_mut(), - env, - mock_info(rewarding_validator_address.as_ref(), &[]), - 1, - ) - .unwrap(); - let res = try_reward_next_mixnode_delegators( deps.as_mut(), mock_info(rewarding_validator_address.as_ref(), &[]), "alice's mixnode".to_string(), - 1, + 0, ); assert_eq!( @@ -1778,21 +1233,13 @@ pub mod tests { env.block.height += storage::MINIMUM_BLOCK_AGE_FOR_REWARDING; - try_begin_mixnode_rewarding( - deps.as_mut(), - env.clone(), - mock_info(rewarding_validator_address.as_ref(), &[]), - 1, - ) - .unwrap(); - try_reward_mixnode( deps.as_mut(), env.clone(), mock_info(rewarding_validator_address.as_ref(), &[]), node_identity.clone(), tests::fixtures::node_rewarding_params_fixture(100), - 1, + 0, ) .unwrap(); @@ -1800,7 +1247,7 @@ pub mod tests { deps.as_mut(), mock_info(rewarding_validator_address.as_ref(), &[]), node_identity.clone(), - 1, + 0, ); assert_eq!( @@ -1810,13 +1257,6 @@ pub mod tests { res ); - try_finish_mixnode_rewarding( - deps.as_mut(), - mock_info(rewarding_validator_address.as_ref(), &[]), - 1, - ) - .unwrap(); - // there was another page of delegators, but they were already dealt with let node_owner: Addr = Addr::unchecked("bob"); @@ -1838,23 +1278,16 @@ pub mod tests { } env.block.height += storage::MINIMUM_BLOCK_AGE_FOR_REWARDING; + test_helpers::update_env_and_progress_interval(&mut env, deps.as_mut().storage); let info = mock_info(rewarding_validator_address.as_ref(), &[]); - try_begin_mixnode_rewarding( - deps.as_mut(), - env.clone(), - mock_info(rewarding_validator_address.as_ref(), &[]), - 2, - ) - .unwrap(); - try_reward_mixnode( deps.as_mut(), env, info, node_identity.clone(), tests::fixtures::node_rewarding_params_fixture(100), - 2, + 1, ) .unwrap(); @@ -1863,7 +1296,7 @@ pub mod tests { deps.as_mut(), mock_info(rewarding_validator_address.as_ref(), &[]), node_identity.clone(), - 2, + 1, ) .unwrap(); @@ -1871,7 +1304,7 @@ pub mod tests { deps.as_mut(), mock_info(rewarding_validator_address.as_ref(), &[]), node_identity.clone(), - 2, + 1, ); assert_eq!( @@ -1880,13 +1313,6 @@ pub mod tests { }), res ); - - try_finish_mixnode_rewarding( - deps.as_mut(), - mock_info(rewarding_validator_address.as_ref(), &[]), - 2, - ) - .unwrap(); } #[test] @@ -1931,21 +1357,13 @@ pub mod tests { env.block.height += storage::MINIMUM_BLOCK_AGE_FOR_REWARDING; let info = mock_info(rewarding_validator_address.as_ref(), &[]); - try_begin_mixnode_rewarding( - deps.as_mut(), - env.clone(), - mock_info(rewarding_validator_address.as_ref(), &[]), - 1, - ) - .unwrap(); - try_reward_mixnode( deps.as_mut(), env, info, node_identity.clone(), tests::fixtures::node_rewarding_params_fixture(100), - 1, + 0, ) .unwrap(); @@ -1954,14 +1372,14 @@ pub mod tests { deps.as_mut(), mock_info(rewarding_validator_address.as_ref(), &[]), node_identity.clone(), - 1, + 0, ) .unwrap(); try_reward_next_mixnode_delegators( deps.as_mut(), mock_info(rewarding_validator_address.as_ref(), &[]), node_identity.clone(), - 1, + 0, ) .unwrap(); @@ -2051,21 +1469,13 @@ pub mod tests { env.block.height += 123; let info = mock_info(rewarding_validator_address.as_ref(), &[]); - try_begin_mixnode_rewarding( - deps.as_mut(), - env.clone(), - mock_info(rewarding_validator_address.as_ref(), &[]), - 1, - ) - .unwrap(); - try_reward_mixnode( deps.as_mut(), env, info, node_identity.clone(), tests::fixtures::node_rewarding_params_fixture(100), - 1, + 0, ) .unwrap(); @@ -2074,7 +1484,7 @@ pub mod tests { deps.as_mut(), mock_info(rewarding_validator_address.as_ref(), &[]), node_identity.clone(), - 1, + 0, ) .unwrap(); diff --git a/contracts/mixnet/src/support/tests/fixtures.rs b/contracts/mixnet/src/support/tests/fixtures.rs index cd1861e344..b2fd0f2ed3 100644 --- a/contracts/mixnet/src/support/tests/fixtures.rs +++ b/contracts/mixnet/src/support/tests/fixtures.rs @@ -1,6 +1,6 @@ use crate::contract::{ - DEFAULT_SYBIL_RESISTANCE_PERCENT, EPOCH_REWARD_PERCENT, INITIAL_MIXNODE_PLEDGE, - INITIAL_REWARD_POOL, + DEFAULT_SYBIL_RESISTANCE_PERCENT, INITIAL_MIXNODE_PLEDGE, INITIAL_REWARD_POOL, + INTERVAL_REWARD_PERCENT, }; use crate::mixnodes::storage as mixnodes_storage; use crate::{mixnodes::storage::StoredMixnodeBond, support::tests}; @@ -79,7 +79,7 @@ pub fn good_gateway_pledge() -> Vec { // when exact values are irrelevant and what matters is the action of rewarding pub fn node_rewarding_params_fixture(uptime: u128) -> NodeRewardParams { NodeRewardParams::new( - (INITIAL_REWARD_POOL / 100) * EPOCH_REWARD_PERCENT as u128, + (INITIAL_REWARD_POOL / 100) * INTERVAL_REWARD_PERCENT as u128, 50 as u128, 25 as u128, 0, diff --git a/contracts/mixnet/src/support/tests/mod.rs b/contracts/mixnet/src/support/tests/mod.rs index 547c68ffdd..f4113c88b2 100644 --- a/contracts/mixnet/src/support/tests/mod.rs +++ b/contracts/mixnet/src/support/tests/mod.rs @@ -12,11 +12,12 @@ pub mod test_helpers { use crate::contract::instantiate; use crate::delegations::storage as delegations_storage; use crate::gateways::transactions::try_add_gateway; + use crate::interval; + use crate::interval::storage as interval_storage; use crate::mixnodes::storage as mixnodes_storage; use crate::mixnodes::transactions::try_add_mixnode; use crate::support::tests; use config::defaults::DENOM; - use cosmwasm_std::coin; use cosmwasm_std::testing::mock_dependencies; use cosmwasm_std::testing::mock_env; use cosmwasm_std::testing::mock_info; @@ -25,6 +26,7 @@ pub mod test_helpers { use cosmwasm_std::Coin; use cosmwasm_std::DepsMut; use cosmwasm_std::OwnedDeps; + use cosmwasm_std::{coin, Env, Timestamp}; use cosmwasm_std::{Addr, StdResult, Storage}; use cosmwasm_std::{Empty, MemoryStorage}; use cw_storage_plus::PrimaryKey; @@ -85,7 +87,7 @@ pub mod test_helpers { }; let env = mock_env(); let info = mock_info("creator", &[]); - instantiate(deps.as_mut(), env.clone(), info, msg).unwrap(); + instantiate(deps.as_mut(), env, info, msg).unwrap(); deps } @@ -125,4 +127,18 @@ pub mod test_helpers { .may_load(storage, (mix.into(), owner.into()).joined_key()) .unwrap() } + + pub(crate) fn update_env_and_progress_interval(env: &mut Env, storage: &mut dyn Storage) { + // make sure current block time is within the expected next interval + env.block.time = Timestamp::from_seconds( + (interval_storage::CURRENT_INTERVAL + .load(storage) + .unwrap() + .next_interval() + .start_unix_timestamp() + + 123) as u64, + ); + + interval::transactions::try_advance_interval(env.clone(), storage).unwrap(); + } } diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index 0eb700103b..1bbc007c31 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -2609,6 +2609,7 @@ dependencies = [ "serde", "serde_repr", "thiserror", + "time 0.3.5", "ts-rs", ] @@ -5027,6 +5028,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41effe7cfa8af36f439fac33861b66b049edc6f9a32331e2312660529c1c24ad" dependencies = [ "libc", + "serde", "time-macros", ] diff --git a/nym-wallet/src-tauri/src/main.rs b/nym-wallet/src-tauri/src/main.rs index da5619524d..3e4f8883c9 100644 --- a/nym-wallet/src-tauri/src/main.rs +++ b/nym-wallet/src-tauri/src/main.rs @@ -71,6 +71,7 @@ fn main() { validator_api::status::mixnode_status, validator_api::status::mixnode_reward_estimation, validator_api::status::mixnode_stake_saturation, + validator_api::status::mixnode_inclusion_probability, ]) .menu(Menu::new().add_default_app_submenu_if_macos()) .run(tauri::generate_context!()) @@ -97,5 +98,6 @@ mod test { validator_client::models::MixnodeStatusResponse => "../src/types/rust/mixnodestatusresponse.ts", validator_client::models::RewardEstimationResponse => "../src/types/rust/rewardestimationresponse.ts", validator_client::models::StakeSaturationResponse => "../src/types/rust/stakesaturaionresponse.ts", + validator_client::models::InclusionProbabilityResponse => "../src/types/rust/inclusionprobabilityresponse.ts", } } diff --git a/nym-wallet/src-tauri/src/operations/validator_api/status.rs b/nym-wallet/src-tauri/src/operations/validator_api/status.rs index cb2fd9d17a..6b559b4acd 100644 --- a/nym-wallet/src-tauri/src/operations/validator_api/status.rs +++ b/nym-wallet/src-tauri/src/operations/validator_api/status.rs @@ -7,7 +7,8 @@ use crate::state::State; use std::sync::Arc; use tokio::sync::RwLock; use validator_client::models::{ - CoreNodeStatusResponse, MixnodeStatusResponse, RewardEstimationResponse, StakeSaturationResponse, + CoreNodeStatusResponse, InclusionProbabilityResponse, MixnodeStatusResponse, + RewardEstimationResponse, StakeSaturationResponse, }; #[tauri::command] @@ -67,3 +68,15 @@ pub async fn mixnode_stake_saturation( .await?, ) } + +#[tauri::command] +pub async fn mixnode_inclusion_probability( + identity: &str, + state: tauri::State<'_, Arc>>, +) -> Result { + Ok( + api_client!(state) + .get_mixnode_inclusion_probability(identity) + .await?, + ) +} diff --git a/nym-wallet/src/types/rust/inclusionprobabilityresponse.ts b/nym-wallet/src/types/rust/inclusionprobabilityresponse.ts new file mode 100644 index 0000000000..461d0a12f7 --- /dev/null +++ b/nym-wallet/src/types/rust/inclusionprobabilityresponse.ts @@ -0,0 +1,4 @@ +export interface InclusionProbabilityResponse { + in_active: number; + in_reserve: number; +} \ No newline at end of file diff --git a/nym-wallet/src/types/rust/operation.ts b/nym-wallet/src/types/rust/operation.ts index 6c8d38c221..ddb0cfb82f 100644 --- a/nym-wallet/src/types/rust/operation.ts +++ b/nym-wallet/src/types/rust/operation.ts @@ -24,4 +24,7 @@ export type Operation = | "TrackUnbondMixnode" | "WithdrawVestedCoins" | "TrackUndelegation" - | "CreatePeriodicVestingAccount"; \ No newline at end of file + | "CreatePeriodicVestingAccount" + | "AdvanceCurrentInterval" + | "WriteRewardedSet" + | "ClearRewardedSet"; \ No newline at end of file diff --git a/nym-wallet/src/types/rust/rewardestimationresponse.ts b/nym-wallet/src/types/rust/rewardestimationresponse.ts index 973641f97c..af42c9f21f 100644 --- a/nym-wallet/src/types/rust/rewardestimationresponse.ts +++ b/nym-wallet/src/types/rust/rewardestimationresponse.ts @@ -2,8 +2,8 @@ export interface RewardEstimationResponse { estimated_total_node_reward: bigint; estimated_operator_reward: bigint; estimated_delegators_reward: bigint; - current_epoch_start: bigint; - current_epoch_end: bigint; - current_epoch_uptime: number; + current_interval_start: bigint; + current_interval_end: bigint; + current_interval_uptime: number; as_at: bigint; } \ No newline at end of file diff --git a/validator-api/Cargo.toml b/validator-api/Cargo.toml index 77224e1816..1d818518ea 100644 --- a/validator-api/Cargo.toml +++ b/validator-api/Cargo.toml @@ -25,7 +25,6 @@ pin-project = "1.0" pretty_env_logger = "0.4" rand-07 = { package = "rand", version = "0.7" } # required for compatibility rand = "0.8" -rand_chacha = "0.3.1" reqwest = { version = "0.11", features = ["json"] } rocket = { version = "0.5.0-rc.1", features = ["json"] } serde = "1.0" diff --git a/validator-api/migrations/20220121120000_epoch_interval_renaming.sql b/validator-api/migrations/20220121120000_epoch_interval_renaming.sql new file mode 100644 index 0000000000..5832984022 --- /dev/null +++ b/validator-api/migrations/20220121120000_epoch_interval_renaming.sql @@ -0,0 +1,73 @@ +/* + * Copyright 2022 - Nym Technologies SA + * SPDX-License-Identifier: Apache-2.0 + */ + +ALTER TABLE epoch_rewarding + RENAME TO interval_rewarding; +ALTER TABLE interval_rewarding + RENAME COLUMN epoch_timestamp TO interval_start_timestamp; + +-- default exists here since otherwise a column couldn't have been added +ALTER TABLE interval_rewarding + ADD COLUMN interval_end_timestamp INTEGER NOT NULL default -1; + + +ALTER TABLE rewarding_report + RENAME TO _rewarding_report_old; +CREATE TABLE rewarding_report +( + interval_rewarding_id INTEGER NOT NULL, + + eligible_mixnodes INTEGER NOT NULL, + + possibly_unrewarded_mixnodes INTEGER NOT NULL, + + FOREIGN KEY (interval_rewarding_id) REFERENCES interval_rewarding (id) +); + +INSERT INTO rewarding_report (interval_rewarding_id, eligible_mixnodes, possibly_unrewarded_mixnodes) +SELECT epoch_rewarding_id, eligible_mixnodes, possibly_unrewarded_mixnodes +FROM _rewarding_report_old; + + +-- I'm not 100% sure whether this is actually required, but better safe than sorry +ALTER TABLE failed_mixnode_reward_chunk + RENAME TO _failed_mixnode_reward_chunk_old; +CREATE TABLE failed_mixnode_reward_chunk +( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + error_message VARCHAR NOT NULL, + + reward_summary_id INTEGER NOT NULL, + + FOREIGN KEY (reward_summary_id) REFERENCES interval_rewarding (id) +); + +INSERT INTO failed_mixnode_reward_chunk (id, error_message, reward_summary_id) +SELECT id, error_message, reward_summary_id +FROM _failed_mixnode_reward_chunk_old; + + +-- yay for SQLite not having option to change foreign key in alter table + +ALTER TABLE possibly_unrewarded_mixnode RENAME TO _possibly_unrewarded_mixnode_old; + +CREATE TABLE possibly_unrewarded_mixnode +( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + identity VARCHAR NOT NULL, + uptime INTEGER NOT NULL, + + failed_mixnode_reward_chunk_id INTEGER NOT NULL, + + FOREIGN KEY (failed_mixnode_reward_chunk_id) REFERENCES failed_mixnode_reward_chunk (id) +); + +INSERT INTO possibly_unrewarded_mixnode (id, identity, uptime, failed_mixnode_reward_chunk_id) +SELECT id, identity, uptime, failed_mixnode_reward_chunk_id +FROM _possibly_unrewarded_mixnode_old; + +DROP TABLE _rewarding_report_old; +DROP TABLE _failed_mixnode_reward_chunk_old; +DROP TABLE _possibly_unrewarded_mixnode_old; \ No newline at end of file diff --git a/validator-api/src/cache/mod.rs b/validator-api/src/cache/mod.rs deleted file mode 100644 index 5efc838025..0000000000 --- a/validator-api/src/cache/mod.rs +++ /dev/null @@ -1,601 +0,0 @@ -// Copyright 2021 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::nymd_client::Client; -use crate::rewarding::EpochRewardParams; -use ::time::OffsetDateTime; -use anyhow::Result; -use config::defaults::VALIDATOR_API_VERSION; -use mixnet_contract_common::{ - ContractStateParams, GatewayBond, IdentityKey, IdentityKeyRef, MixNodeBond, - RewardingIntervalResponse, -}; -use rand::prelude::SliceRandom; -use rand_chacha::rand_core::SeedableRng; -use rand_chacha::ChaCha20Rng; -use rocket::fairing::AdHoc; -use serde::{Deserialize, Serialize}; -use std::collections::HashSet; -use std::fmt; -use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering}; -use std::sync::Arc; -use std::time::Duration; -use tokio::sync::RwLock; -use tokio::time; -use validator_api_requests::models::MixnodeStatus; -use validator_client::nymd::hash::SHA256_HASH_SIZE; -use validator_client::nymd::CosmWasmClient; - -pub(crate) mod routes; - -#[derive(Debug, Serialize, Deserialize)] -pub struct InclusionProbabilityResponse { - in_active: f32, - in_reserve: f32, -} - -impl fmt::Display for InclusionProbabilityResponse { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "in_active: {:.5}, in_reserve: {:.5}", - self.in_active, self.in_reserve - ) - } -} - -pub struct ValidatorCacheRefresher { - nymd_client: Client, - cache: ValidatorCache, - caching_interval: Duration, -} - -#[derive(Clone)] -pub struct ValidatorCache { - inner: Arc, -} - -struct ValidatorCacheInner { - initialised: AtomicBool, - latest_known_rewarding_block: AtomicU64, - - mixnodes: RwLock>>, - gateways: RwLock>>, - - rewarded_mixnodes: RwLock>>, - - current_mixnode_rewarded_set_size: AtomicU32, - current_mixnode_active_set_size: AtomicU32, - - current_reward_params: RwLock>, -} - -fn current_unix_timestamp() -> i64 { - let now = OffsetDateTime::now_utc(); - now.unix_timestamp() -} - -#[derive(Default, Serialize, Clone)] -pub struct Cache { - value: T, - as_at: i64, -} - -impl Cache { - fn new(value: T) -> Self { - Cache { - value, - as_at: current_unix_timestamp(), - } - } - - fn set(&mut self, value: T) { - self.value = value; - self.as_at = current_unix_timestamp() - } - - fn renew(&mut self) { - self.as_at = current_unix_timestamp() - } - - pub fn timestamp(&self) -> i64 { - self.as_at - } - - pub fn into_inner(self) -> T { - self.value - } -} - -impl ValidatorCacheRefresher { - pub(crate) fn new( - nymd_client: Client, - caching_interval: Duration, - cache: ValidatorCache, - ) -> Self { - ValidatorCacheRefresher { - nymd_client, - cache, - caching_interval, - } - } - - async fn refresh_cache(&self) -> Result<()> - where - C: CosmWasmClient + Sync, - { - let (mixnodes, gateways) = tokio::try_join!( - self.nymd_client.get_mixnodes(), - self.nymd_client.get_gateways(), - )?; - - let contract_settings = self.nymd_client.get_contract_settings().await?; - let current_rewarding_interval = self.nymd_client.get_current_rewarding_interval().await?; - let rewarding_block_hash = self - .nymd_client - .get_block_hash( - current_rewarding_interval.current_rewarding_interval_starting_block as u32, - ) - .await?; - - let epoch_rewarding_params = self.nymd_client.get_current_epoch_reward_params().await?; - - info!( - "Updating validator cache. There are {} mixnodes and {} gateways", - mixnodes.len(), - gateways.len(), - ); - - self.cache - .update_cache( - mixnodes, - gateways, - contract_settings, - current_rewarding_interval, - rewarding_block_hash, - epoch_rewarding_params, - ) - .await; - - Ok(()) - } - - pub(crate) async fn run(&self) - where - C: CosmWasmClient + Sync, - { - let mut interval = time::interval(self.caching_interval); - loop { - interval.tick().await; - if let Err(err) = self.refresh_cache().await { - error!("Failed to refresh validator cache - {}", err); - } else { - // relaxed memory ordering is fine here. worst case scenario network monitor - // will just have to wait for an additional backoff to see the change. - // And so this will not really incur any performance penalties by setting it every loop iteration - self.cache.inner.initialised.store(true, Ordering::Relaxed) - } - } - } -} - -impl ValidatorCache { - fn new() -> Self { - ValidatorCache { - inner: Arc::new(ValidatorCacheInner::new()), - } - } - - pub fn stage() -> AdHoc { - AdHoc::on_ignite("Validator Cache Stage", |rocket| async { - rocket.manage(Self::new()).mount( - // this format! is so ugly... - format!("/{}", VALIDATOR_API_VERSION), - routes![ - routes::get_mixnodes, - routes::get_gateways, - routes::get_active_mixnodes, - routes::get_rewarded_mixnodes, - routes::get_probs_mixnode_rewarded - ], - ) - }) - } - - // NOTE: this does not guarantee consistent results between multiple validator APIs, because - // currently we do not guarantee the list of mixnodes (i.e. `mixnodes: &[MixNodeBond]`) will be the same - - // somebody might bond/unbond a node or change delegation between different cache refreshes. - // - // I guess that's not a problem right now and we can resolve it later. My idea for that would be as follows: - // since the demanded set changes only monthly, just write the identities of those nodes to the smart - // contract upon finished rewarding (this works under assumption of rewards being distributed by a single validator) - // - // alternatively we could have some state locking mechanism for the duration of determining the demanded set - // this could work with multiple validators via some multisig mechanism - fn determine_rewarded_set( - &self, - mixnodes: &[MixNodeBond], - nodes_to_select: u32, - block_hash: Option<[u8; SHA256_HASH_SIZE]>, - ) -> Vec { - if mixnodes.is_empty() { - return Vec::new(); - } - - if block_hash.is_none() { - // I'm not entirely sure under what condition can hash of a block be empty - // (note that we know the block exists otherwise we would have gotten an error - // when attempting to retrieve the hash) - error!("The hash of the block of the rewarding interval is None - we're not going to update the set"); - return Vec::new(); - } - - // seed our rng with the hash of the block of the most recent rewarding interval - let mut rng = ChaCha20Rng::from_seed(block_hash.unwrap()); - - self.stake_weighted_choice(mixnodes, nodes_to_select as usize, &mut rng) - } - - fn stake_weighted_choice( - &self, - mixnodes: &[MixNodeBond], - nodes_to_select: usize, - rng: &mut ChaCha20Rng, - ) -> Vec { - // generate list of mixnodes and their relatively weight (by total stake) - let choices = self.generate_mixnode_stake_tuples(mixnodes); - // the unwrap here is fine as an error can only be thrown under one of the following conditions: - // - our mixnode list is empty - we have already checked for that - // - we have invalid weights, i.e. less than zero or NaNs - it shouldn't happen in our case as we safely cast down from u128 - // - all weights are zero - it's impossible in our case as the list of nodes is not empty and weight is proportional to stake. You must have non-zero stake in order to bond - // - we have more than u32::MAX values (which is incredibly unrealistic to have 4B mixnodes bonded... literally every other person on the planet would need one) - choices - .choose_multiple_weighted(rng, nodes_to_select, |item| item.1) - .unwrap() - .map(|(bond, _weight)| bond) - .cloned() - .collect() - } - - fn generate_mixnode_stake_tuples(&self, mixnodes: &[MixNodeBond]) -> Vec<(MixNodeBond, f64)> { - mixnodes - .iter() - .map(|mix| { - // note that the theoretical maximum possible stake is equal to the total - // supply of all tokens, i.e. 1B (which is 1 quadrillion of native tokens, i.e. 10^15 ~ 2^50) - // which is way below maximum value of f64, so the cast is fine - let total_stake = mix.total_stake().unwrap_or_default() as f64; - (mix.clone(), total_stake) - }) // if for some reason node is invalid, treat it as 0 stake/weight - .collect() - } - - // Estimate probability that a node will end up in the rewarded set, by running the selection process 100 times and aggregating the results. - // If a node is in the active set it is not counted as being in the reserve set, the probabilities are exclusive. Cumulative probabilitiy - // can be obtained by summing the two probabilities returned. - async fn probs_mixnode_rewarded_calculate( - &self, - target_mixnode_id: IdentityKey, - mixnodes: Option>, - ) -> Option { - let mixnodes = if let Some(nodes) = mixnodes { - nodes - } else { - self.inner.mixnodes.read().await.value.clone() - }; - let total_bonded_tokens = mixnodes - .iter() - .fold(0u128, |acc, x| acc + x.total_stake().unwrap_or_default()) - as f64; - let target_mixnode = mixnodes - .iter() - .find(|x| x.identity() == &target_mixnode_id)?; - let rewarded_set_size = self - .inner - .current_mixnode_rewarded_set_size - .load(Ordering::SeqCst) as f64; - let active_set_size = self - .inner - .current_mixnode_active_set_size - .load(Ordering::SeqCst) as f64; - - // For running comparison tests below, needs improvement - // let rewarded_set_size = 720.; - // let active_set_size = 300.; - - let prob_one_draw = - target_mixnode.total_stake().unwrap_or_default() as f64 / total_bonded_tokens; - // Chance to be selected in any draw for active set - let prob_active_set = active_set_size * prob_one_draw; - // This is likely slightly too high, as we're not correcting form them not being selected in active, should be chance to be selected, minus the chance for being not selected in reserve - let prob_reserve_set = (rewarded_set_size - active_set_size) * prob_one_draw; - // (rewarded_set_size - active_set_size) * prob_one_draw * (1. - prob_active_set); - - Some(InclusionProbabilityResponse { - in_active: if prob_active_set > 1. { - 1. - } else { - prob_active_set - } as f32, - in_reserve: if prob_reserve_set > 1. { - 1. - } else { - prob_reserve_set - } as f32, - }) - } - - #[allow(dead_code)] - async fn probs_mixnode_rewarded_simulate( - &self, - target_mixnode_id: IdentityKey, - mixnodes: Option>, - ) -> Option { - let mut in_active = 0; - let mut in_reserve = 0; - let mixnodes = if let Some(nodes) = mixnodes { - nodes - } else { - self.inner.mixnodes.read().await.value.clone() - }; - let rewarded_set_size = self - .inner - .current_mixnode_rewarded_set_size - .load(Ordering::SeqCst) as usize; - let active_set_size = self - .inner - .current_mixnode_active_set_size - .load(Ordering::SeqCst) as usize; - let mut rng = ChaCha20Rng::from_entropy(); - - // For running comparison tests below, needs improvement - // let rewarded_set_size = 720.; - // let active_set_size = 300.; - - let it = 100; - - for _ in 0..it { - let mut rewarded_set = - self.stake_weighted_choice(&mixnodes, rewarded_set_size, &mut rng); - let reserve_set = rewarded_set - .split_off(active_set_size) - .iter() - .map(|bond| bond.identity().clone()) - .collect::>(); - let active_set = rewarded_set - .iter() - .map(|bond| bond.identity().clone()) - .collect::>(); - - if active_set.contains(&target_mixnode_id) { - in_active += 1; - } else if reserve_set.contains(&target_mixnode_id) { - in_reserve += 1; - } - } - Some(InclusionProbabilityResponse { - in_active: in_active as f32 / it as f32, - in_reserve: in_reserve as f32 / it as f32, - }) - } - - async fn update_cache( - &self, - mixnodes: Vec, - gateways: Vec, - state: ContractStateParams, - rewarding_interval: RewardingIntervalResponse, - rewarding_block_hash: Option<[u8; SHA256_HASH_SIZE]>, - epoch_rewarding_params: EpochRewardParams, - ) { - // if the rewarding is currently in progress, don't mess with the rewarded/active sets - // as most likely will be changed next time this function is called - // - // if our data is valid, it means the active sets are available, - // otherwise we must explicitly indicate nobody can read this data - if !rewarding_interval.rewarding_in_progress { - // if we're still in the same rewarding interval, i.e. the latest block is the same, - // there's nothing we have to do - if rewarding_interval.current_rewarding_interval_starting_block - > self - .inner - .latest_known_rewarding_block - .load(Ordering::SeqCst) - { - let rewarded_nodes = self.determine_rewarded_set( - &mixnodes, - state.mixnode_rewarded_set_size, - rewarding_block_hash, - ); - - self.inner - .current_mixnode_rewarded_set_size - .store(state.mixnode_rewarded_set_size, Ordering::SeqCst); - self.inner - .current_mixnode_active_set_size - .store(state.mixnode_active_set_size, Ordering::SeqCst); - self.inner.latest_known_rewarding_block.store( - rewarding_interval.current_rewarding_interval_starting_block, - Ordering::SeqCst, - ); - - self.inner - .rewarded_mixnodes - .write() - .await - .set(rewarded_nodes); - } else { - // however, update the timestamp on the cache - self.inner.rewarded_mixnodes.write().await.renew() - } - } - - self.inner.mixnodes.write().await.set(mixnodes); - self.inner.gateways.write().await.set(gateways); - self.inner - .current_reward_params - .write() - .await - .set(epoch_rewarding_params); - } - - pub async fn mixnodes(&self) -> Cache> { - self.inner.mixnodes.read().await.clone() - } - - pub async fn gateways(&self) -> Cache> { - self.inner.gateways.read().await.clone() - } - - pub async fn rewarded_mixnodes(&self) -> Cache> { - self.inner.rewarded_mixnodes.read().await.clone() - } - - pub async fn active_mixnodes(&self) -> Cache> { - // rewarded set is already "sorted" by pseudo-randomly choosing mixnodes from - // all bonded nodes, weighted by stake. For the active set choose first k nodes. - let cache = self.inner.rewarded_mixnodes.read().await; - let timestamp = cache.as_at; - let nodes = cache - .value - .iter() - .take( - self.inner - .current_mixnode_active_set_size - .load(Ordering::SeqCst) as usize, - ) - .cloned() - .collect(); - Cache { - value: nodes, - as_at: timestamp, - } - } - - pub(crate) async fn epoch_reward_params(&self) -> Cache { - self.inner.current_reward_params.read().await.clone() - } - - pub async fn mixnode_details( - &self, - identity: IdentityKeyRef<'_>, - ) -> (Option, MixnodeStatus) { - // it might not be the most optimal to possibly iterate the entire vector to find (or not) - // the relevant value. However, the vectors are relatively small (< 10_000 elements) and - // the implementation for active/rewarded sets might change soon so there's no point in premature optimisation - // with HashSets - let rewarded_mixnodes = &self.inner.rewarded_mixnodes.read().await.value; - let active_set_size = self - .inner - .current_mixnode_active_set_size - .load(Ordering::SeqCst) as usize; - - // see if node is in the top active_set_size of rewarded nodes, i.e. it's active - if let Some(bond) = rewarded_mixnodes - .iter() - .take(active_set_size) - .find(|mix| mix.mix_node.identity_key == identity) - { - (Some(bond.clone()), MixnodeStatus::Active) - // see if it's in the bottom part of the rewarded set, i.e. it's in standby - } else if let Some(bond) = rewarded_mixnodes - .iter() - .skip(active_set_size) - .find(|mix| mix.mix_node.identity_key == identity) - { - (Some(bond.clone()), MixnodeStatus::Standby) - // if it's not in the rewarded set see if its bonded at all - } else if let Some(bond) = self - .inner - .mixnodes - .read() - .await - .value - .iter() - .find(|mix| mix.mix_node.identity_key == identity) - { - (Some(bond.clone()), MixnodeStatus::Inactive) - } else { - (None, MixnodeStatus::NotFound) - } - } - - pub async fn mixnode_status(&self, identity: IdentityKey) -> MixnodeStatus { - self.mixnode_details(&identity).await.1 - } - - pub fn initialised(&self) -> bool { - self.inner.initialised.load(Ordering::Relaxed) - } - - pub(crate) async fn wait_for_initial_values(&self) { - let initialisation_backoff = Duration::from_secs(5); - loop { - if self.initialised() { - break; - } else { - debug!("Validator cache hasn't been initialised yet - waiting for {:?} before trying again", initialisation_backoff); - tokio::time::sleep(initialisation_backoff).await; - } - } - } -} - -impl ValidatorCacheInner { - fn new() -> Self { - ValidatorCacheInner { - initialised: AtomicBool::new(false), - latest_known_rewarding_block: Default::default(), - mixnodes: RwLock::new(Cache::default()), - gateways: RwLock::new(Cache::default()), - rewarded_mixnodes: RwLock::new(Cache::default()), - current_mixnode_rewarded_set_size: Default::default(), - current_mixnode_active_set_size: Default::default(), - current_reward_params: RwLock::new(Cache::new(EpochRewardParams::new_empty())), - } - } -} - -// #[cfg(test)] -// mod test { -// use crate::cache::InclusionProbabilityResponse; - -// use super::ValidatorCache; -// use mixnet_contract_common::MixNodeBond; - -// #[tokio::test] -// async fn test_inclusion_probabilities() { -// let cache = ValidatorCache::new(); -// let response = attohttpc::get("https://sandbox-validator.nymtech.net/api/v1/mixnodes") -// .send() -// .unwrap(); -// let mixnodes: Vec = response.json().unwrap(); -// let calculated = cache -// .probs_mixnode_rewarded_calculate( -// "ysmgeYJQPBFzB2TgqBjN5BE3Rb79CgFANrnJaYd8woQ".to_string(), -// Some(mixnodes.clone()), -// ) -// .await -// .unwrap(); -// let mut simulated_avg = Vec::new(); -// for _ in 0..1000 { -// let simulated = cache -// .probs_mixnode_rewarded_simulate( -// "ysmgeYJQPBFzB2TgqBjN5BE3Rb79CgFANrnJaYd8woQ".to_string(), -// Some(mixnodes.clone()), -// ) -// .await -// .unwrap(); -// simulated_avg.push(simulated); -// } -// let simulated = simulated_avg.iter().fold((0., 0.), |acc, x| { -// (acc.0 + x.in_active, acc.1 + x.in_reserve) -// }); -// let simulated = InclusionProbabilityResponse { -// in_active: simulated.0 / 100., -// in_reserve: simulated.1 / 100., -// }; -// println!("calculted: {calculated}"); -// println!("simulated: {simulated}"); -// } -// } diff --git a/validator-api/src/cache/routes.rs b/validator-api/src/cache/routes.rs deleted file mode 100644 index c08c520b7f..0000000000 --- a/validator-api/src/cache/routes.rs +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright 2021 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::cache::InclusionProbabilityResponse; -use crate::cache::ValidatorCache; -use mixnet_contract_common::{GatewayBond, MixNodeBond}; -use rocket::serde::json::Json; -use rocket::State; - -#[get("/mixnodes")] -pub(crate) async fn get_mixnodes(cache: &State) -> Json> { - Json(cache.mixnodes().await.value) -} - -#[get("/gateways")] -pub(crate) async fn get_gateways(cache: &State) -> Json> { - Json(cache.gateways().await.value) -} - -#[get("/mixnodes/rewarded")] -pub(crate) async fn get_rewarded_mixnodes(cache: &State) -> Json> { - Json(cache.rewarded_mixnodes().await.value) -} - -#[get("/mixnodes/active")] -pub(crate) async fn get_active_mixnodes(cache: &State) -> Json> { - Json(cache.active_mixnodes().await.value) -} - -#[get("/mixnodes/rewarded/inclusion-probability/")] -pub(crate) async fn get_probs_mixnode_rewarded( - cache: &State, - mixnode_id: String, -) -> Json> { - Json( - cache - .probs_mixnode_rewarded_calculate(mixnode_id, None) - .await, - ) -} diff --git a/validator-api/src/config/mod.rs b/validator-api/src/config/mod.rs index 094a0f820e..4612c5ac18 100644 --- a/validator-api/src/config/mod.rs +++ b/validator-api/src/config/mod.rs @@ -3,7 +3,7 @@ use crate::config::template::config_template; use config::defaults::{ - default_api_endpoints, DEFAULT_EPOCH_LENGTH, DEFAULT_FIRST_EPOCH_START, + default_api_endpoints, DEFAULT_FIRST_INTERVAL_START, DEFAULT_INTERVAL_LENGTH, DEFAULT_MIXNET_CONTRACT_ADDRESS, }; use config::NymConfig; @@ -261,18 +261,18 @@ pub struct Rewarding { /// Mnemonic (currently of the network monitor) used for rewarding mnemonic: String, - /// Datetime of the first rewarding epoch of the current length used for referencing - /// starting time of any subsequent epoch. - first_rewarding_epoch: OffsetDateTime, + /// Datetime of the first rewarding interval of the current length used for referencing + /// starting time of any subsequent interval. + first_rewarding_interval: OffsetDateTime, - /// Current length of the epoch. If modified `first_rewarding_epoch` should also get changed. + /// Current length of the interval. If modified `first_rewarding_interval` should also get changed. #[serde(with = "humantime_serde")] - epoch_length: Duration, + interval_length: Duration, /// Specifies the minimum percentage of monitor test run data present in order to - /// distribute rewards for given epoch. + /// distribute rewards for given interval. /// Note, only values in range 0-100 are valid - minimum_epoch_monitor_threshold: u8, + minimum_interval_monitor_threshold: u8, } impl Default for Rewarding { @@ -280,9 +280,9 @@ impl Default for Rewarding { Rewarding { enabled: false, mnemonic: String::default(), - first_rewarding_epoch: DEFAULT_FIRST_EPOCH_START, - epoch_length: DEFAULT_EPOCH_LENGTH, - minimum_epoch_monitor_threshold: DEFAULT_MONITOR_THRESHOLD, + first_rewarding_interval: DEFAULT_FIRST_INTERVAL_START, + interval_length: DEFAULT_INTERVAL_LENGTH, + minimum_interval_monitor_threshold: DEFAULT_MONITOR_THRESHOLD, } } } @@ -338,18 +338,18 @@ impl Config { self } - pub fn with_first_rewarding_epoch(mut self, first_epoch: OffsetDateTime) -> Self { - self.rewarding.first_rewarding_epoch = first_epoch; + pub fn with_first_rewarding_interval(mut self, first_interval: OffsetDateTime) -> Self { + self.rewarding.first_rewarding_interval = first_interval; self } - pub fn with_epoch_length(mut self, epoch_length: Duration) -> Self { - self.rewarding.epoch_length = epoch_length; + pub fn with_interval_length(mut self, interval_length: Duration) -> Self { + self.rewarding.interval_length = interval_length; self } - pub fn with_minimum_epoch_monitor_threshold(mut self, threshold: u8) -> Self { - self.rewarding.minimum_epoch_monitor_threshold = threshold; + pub fn with_minimum_interval_monitor_threshold(mut self, threshold: u8) -> Self { + self.rewarding.minimum_interval_monitor_threshold = threshold; self } @@ -462,15 +462,17 @@ impl Config { self.network_monitor.all_validator_apis.clone() } - pub fn get_first_rewarding_epoch(&self) -> OffsetDateTime { - self.rewarding.first_rewarding_epoch + // TODO: Is this needed? + #[allow(dead_code)] + pub fn get_first_rewarding_interval(&self) -> OffsetDateTime { + self.rewarding.first_rewarding_interval } - pub fn get_epoch_length(&self) -> Duration { - self.rewarding.epoch_length + pub fn get_interval_length(&self) -> Duration { + self.rewarding.interval_length } - pub fn get_minimum_epoch_monitor_threshold(&self) -> u8 { - self.rewarding.minimum_epoch_monitor_threshold + pub fn get_minimum_interval_monitor_threshold(&self) -> u8 { + self.rewarding.minimum_interval_monitor_threshold } } diff --git a/validator-api/src/config/template.rs b/validator-api/src/config/template.rs index d3a2813f81..79c05fafb9 100644 --- a/validator-api/src/config/template.rs +++ b/validator-api/src/config/template.rs @@ -99,17 +99,17 @@ enabled = {{ rewarding.enabled }} # Mnemonic (currently of the network monitor) used for rewarding mnemonic = '{{ rewarding.mnemonic }}' -# Datetime of the first rewarding epoch of the current length used for referencing -# starting time of any subsequent epoch. -first_rewarding_epoch = '{{ rewarding.first_rewarding_epoch }}' +# Datetime of the first rewarding interval of the current length used for referencing +# starting time of any subsequent interval. +first_rewarding_interval = '{{ rewarding.first_rewarding_interval }}' -# Current length of the epoch. If modified `first_rewarding_epoch` should also get changed. -epoch_length = '{{ rewarding.epoch_length }}' +# Current length of the interval. If modified `first_rewarding_interval` should also get changed. +interval_length = '{{ rewarding.interval_length }}' # Specifies the minimum percentage of monitor test run data present in order to -# distribute rewards for given epoch. +# distribute rewards for given interval. # Note, only values in range 0-100 are valid -minimum_epoch_monitor_threshold = {{ rewarding.minimum_epoch_monitor_threshold }} +minimum_interval_monitor_threshold = {{ rewarding.minimum_interval_monitor_threshold }} "# } diff --git a/validator-api/src/contract_cache/mod.rs b/validator-api/src/contract_cache/mod.rs new file mode 100644 index 0000000000..94a0bc7bb2 --- /dev/null +++ b/validator-api/src/contract_cache/mod.rs @@ -0,0 +1,327 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::nymd_client::Client; +use crate::rewarding::IntervalRewardParams; +use ::time::OffsetDateTime; +use anyhow::Result; +use config::defaults::VALIDATOR_API_VERSION; +use mixnet_contract_common::{ + GatewayBond, IdentityKey, IdentityKeyRef, Interval, MixNodeBond, RewardedSetNodeStatus, +}; + +use rocket::fairing::AdHoc; +use serde::Serialize; +use std::collections::HashMap; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use std::time::Duration; +use tokio::sync::{Notify, RwLock}; +use tokio::time; +use validator_api_requests::models::MixnodeStatus; +use validator_client::nymd::CosmWasmClient; + +pub(crate) mod routes; + +pub struct ValidatorCacheRefresher { + nymd_client: Client, + cache: ValidatorCache, + caching_interval: Duration, + update_rewarded_set_notify: Option>, +} + +#[derive(Clone)] +pub struct ValidatorCache { + initialised: Arc, + inner: Arc>, +} + +struct ValidatorCacheInner { + mixnodes: Cache>, + gateways: Cache>, + + rewarded_set: Cache>, + active_set: Cache>, + + current_reward_params: Cache, + current_interval: Cache, +} + +fn current_unix_timestamp() -> i64 { + let now = OffsetDateTime::now_utc(); + now.unix_timestamp() +} + +#[derive(Default, Serialize, Clone)] +pub struct Cache { + value: T, + as_at: i64, +} + +impl Cache { + fn new(value: T) -> Self { + Cache { + value, + as_at: current_unix_timestamp(), + } + } + + fn update(&mut self, value: T) { + self.value = value; + self.as_at = current_unix_timestamp() + } + + pub fn timestamp(&self) -> i64 { + self.as_at + } + + pub fn into_inner(self) -> T { + self.value + } +} + +impl ValidatorCacheRefresher { + pub(crate) fn new( + nymd_client: Client, + caching_interval: Duration, + cache: ValidatorCache, + update_rewarded_set_notify: Option>, + ) -> Self { + ValidatorCacheRefresher { + nymd_client, + cache, + caching_interval, + update_rewarded_set_notify, + } + } + + fn collect_rewarded_and_active_set_details( + &self, + all_mixnodes: &[MixNodeBond], + rewarded_set_identities: Vec<(IdentityKey, RewardedSetNodeStatus)>, + ) -> (Vec, Vec) { + let mut active_set = Vec::new(); + let mut rewarded_set = Vec::new(); + let rewarded_set_identities = rewarded_set_identities + .into_iter() + .collect::>(); + + for mix in all_mixnodes { + if let Some(status) = rewarded_set_identities.get(mix.identity()) { + rewarded_set.push(mix.clone()); + if status.is_active() { + active_set.push(mix.clone()) + } + } + } + + (rewarded_set, active_set) + } + + async fn refresh_cache(&self) -> Result<()> + where + C: CosmWasmClient + Sync, + { + let (mixnodes, gateways) = tokio::try_join!( + self.nymd_client.get_mixnodes(), + self.nymd_client.get_gateways(), + )?; + + let rewarded_set_identities = self.nymd_client.get_rewarded_set_identities().await?; + let (rewarded_set, active_set) = + self.collect_rewarded_and_active_set_details(&mixnodes, rewarded_set_identities); + + let interval_rewarding_params = self + .nymd_client + .get_current_interval_reward_params() + .await?; + let current_interval = self.nymd_client.get_current_interval().await?; + + info!( + "Updating validator cache. There are {} mixnodes and {} gateways", + mixnodes.len(), + gateways.len(), + ); + + self.cache + .update_cache( + mixnodes, + gateways, + rewarded_set, + active_set, + interval_rewarding_params, + current_interval, + ) + .await; + + if let Some(notify) = &self.update_rewarded_set_notify { + let update_details = self + .nymd_client + .get_current_rewarded_set_update_details() + .await?; + + if update_details.last_refreshed_block + (update_details.refresh_rate_blocks as u64) + < update_details.current_height + { + // there's only ever a single waiter -> the set updater + notify.notify_one() + } + } + + Ok(()) + } + + pub(crate) async fn run(&self) + where + C: CosmWasmClient + Sync, + { + let mut interval = time::interval(self.caching_interval); + loop { + interval.tick().await; + if let Err(err) = self.refresh_cache().await { + error!("Failed to refresh validator cache - {}", err); + } else { + // relaxed memory ordering is fine here. worst case scenario network monitor + // will just have to wait for an additional backoff to see the change. + // And so this will not really incur any performance penalties by setting it every loop iteration + self.cache.initialised.store(true, Ordering::Relaxed) + } + } + } +} + +impl ValidatorCache { + fn new() -> Self { + ValidatorCache { + initialised: Arc::new(AtomicBool::new(false)), + inner: Arc::new(RwLock::new(ValidatorCacheInner::new())), + } + } + + pub fn stage() -> AdHoc { + AdHoc::on_ignite("Validator Cache Stage", |rocket| async { + rocket.manage(Self::new()).mount( + // this format! is so ugly... + format!("/{}", VALIDATOR_API_VERSION), + routes![ + routes::get_mixnodes, + routes::get_gateways, + routes::get_active_set, + routes::get_rewarded_set, + ], + ) + }) + } + + async fn update_cache( + &self, + mixnodes: Vec, + gateways: Vec, + rewarded_set: Vec, + active_set: Vec, + interval_rewarding_params: IntervalRewardParams, + current_interval: Interval, + ) { + let mut inner = self.inner.write().await; + + inner.mixnodes.update(mixnodes); + inner.gateways.update(gateways); + inner.rewarded_set.update(rewarded_set); + inner.active_set.update(active_set); + inner + .current_reward_params + .update(interval_rewarding_params); + inner.current_interval.update(current_interval); + } + + pub async fn mixnodes(&self) -> Cache> { + self.inner.read().await.mixnodes.clone() + } + + pub async fn gateways(&self) -> Cache> { + self.inner.read().await.gateways.clone() + } + + pub async fn rewarded_set(&self) -> Cache> { + self.inner.read().await.rewarded_set.clone() + } + + pub async fn active_set(&self) -> Cache> { + self.inner.read().await.active_set.clone() + } + + pub(crate) async fn interval_reward_params(&self) -> Cache { + self.inner.read().await.current_reward_params.clone() + } + + pub(crate) async fn current_interval(&self) -> Cache { + self.inner.read().await.current_interval.clone() + } + + pub async fn mixnode_details( + &self, + identity: IdentityKeyRef<'_>, + ) -> (Option, MixnodeStatus) { + // it might not be the most optimal to possibly iterate the entire vector to find (or not) + // the relevant value. However, the vectors are relatively small (< 10_000 elements, < 1000 for active set) + + let active_set = &self.inner.read().await.active_set.value; + if let Some(bond) = active_set + .iter() + .find(|mix| mix.mix_node.identity_key == identity) + { + return (Some(bond.clone()), MixnodeStatus::Active); + } + + let rewarded_set = &self.inner.read().await.rewarded_set.value; + if let Some(bond) = rewarded_set + .iter() + .find(|mix| mix.mix_node.identity_key == identity) + { + return (Some(bond.clone()), MixnodeStatus::Standby); + } + + let all_bonded = &self.inner.read().await.mixnodes.value; + if let Some(bond) = all_bonded + .iter() + .find(|mix| mix.mix_node.identity_key == identity) + { + (Some(bond.clone()), MixnodeStatus::Inactive) + } else { + (None, MixnodeStatus::NotFound) + } + } + + pub async fn mixnode_status(&self, identity: IdentityKey) -> MixnodeStatus { + self.mixnode_details(&identity).await.1 + } + + pub fn initialised(&self) -> bool { + self.initialised.load(Ordering::Relaxed) + } + + pub(crate) async fn wait_for_initial_values(&self) { + let initialisation_backoff = Duration::from_secs(5); + loop { + if self.initialised() { + break; + } else { + debug!("Validator cache hasn't been initialised yet - waiting for {:?} before trying again", initialisation_backoff); + tokio::time::sleep(initialisation_backoff).await; + } + } + } +} + +impl ValidatorCacheInner { + fn new() -> Self { + ValidatorCacheInner { + mixnodes: Cache::default(), + gateways: Cache::default(), + rewarded_set: Cache::default(), + active_set: Cache::default(), + current_reward_params: Cache::new(IntervalRewardParams::new_empty()), + current_interval: Cache::default(), + } + } +} diff --git a/validator-api/src/contract_cache/routes.rs b/validator-api/src/contract_cache/routes.rs new file mode 100644 index 0000000000..ec54602e1f --- /dev/null +++ b/validator-api/src/contract_cache/routes.rs @@ -0,0 +1,27 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::contract_cache::ValidatorCache; +use mixnet_contract_common::{GatewayBond, MixNodeBond}; +use rocket::serde::json::Json; +use rocket::State; + +#[get("/mixnodes")] +pub(crate) async fn get_mixnodes(cache: &State) -> Json> { + Json(cache.mixnodes().await.value) +} + +#[get("/gateways")] +pub(crate) async fn get_gateways(cache: &State) -> Json> { + Json(cache.gateways().await.value) +} + +#[get("/mixnodes/rewarded")] +pub(crate) async fn get_rewarded_set(cache: &State) -> Json> { + Json(cache.rewarded_set().await.value) +} + +#[get("/mixnodes/active")] +pub(crate) async fn get_active_set(cache: &State) -> Json> { + Json(cache.active_set().await.value) +} diff --git a/validator-api/src/main.rs b/validator-api/src/main.rs index 698cbc970c..d147ce113b 100644 --- a/validator-api/src/main.rs +++ b/validator-api/src/main.rs @@ -4,18 +4,17 @@ #[macro_use] extern crate rocket; -use crate::cache::ValidatorCacheRefresher; use crate::config::Config; +use crate::contract_cache::ValidatorCacheRefresher; use crate::network_monitor::NetworkMonitorBuilder; use crate::node_status_api::uptime_updater::HistoricalUptimeUpdater; use crate::nymd_client::Client; -use crate::rewarding::epoch::Epoch; use crate::rewarding::Rewarder; use crate::storage::ValidatorApiStorage; use ::config::NymConfig; use anyhow::Result; -use cache::ValidatorCache; use clap::{crate_version, App, Arg, ArgMatches}; +use contract_cache::ValidatorCache; use log::{info, warn}; use rocket::fairing::AdHoc; use rocket::http::Method; @@ -29,15 +28,18 @@ use time::OffsetDateTime; use tokio::sync::Notify; use url::Url; use validator_client::nymd::SigningNymdClient; +use validator_client::ValidatorClientError; +use crate::rewarded_set_updater::RewardedSetUpdater; #[cfg(feature = "coconut")] use coconut::InternalSignRequest; -pub(crate) mod cache; pub(crate) mod config; +pub(crate) mod contract_cache; mod network_monitor; mod node_status_api; pub(crate) mod nymd_client; +mod rewarded_set_updater; mod rewarding; pub(crate) mod storage; @@ -64,8 +66,8 @@ const ETH_ENDPOINT: &str = "eth_endpoint"; #[cfg(not(feature = "coconut"))] const ETH_PRIVATE_KEY: &str = "eth_private_key"; -const EPOCH_LENGTH_ARG: &str = "epoch-length"; -const FIRST_REWARDING_EPOCH_ARG: &str = "first-epoch"; +const INTERVAL_LENGTH_ARG: &str = "interval-length"; +const FIRST_REWARDING_INTERVAL_ARG: &str = "first-interval"; const REWARDING_MONITOR_THRESHOLD_ARG: &str = "monitor-threshold"; fn parse_validators(raw: &str) -> Vec { @@ -164,22 +166,22 @@ fn parse_args<'a>() -> ArgMatches<'a> { .takes_value(true) ) .arg( - Arg::with_name(FIRST_REWARDING_EPOCH_ARG) - .help("Datetime specifying beginning of the first rewarding epoch of this length. It must be a valid rfc3339 datetime.") + Arg::with_name(FIRST_REWARDING_INTERVAL_ARG) + .help("Datetime specifying beginning of the first rewarding interval of this length. It must be a valid rfc3339 datetime.") .takes_value(true) - .long(FIRST_REWARDING_EPOCH_ARG) + .long(FIRST_REWARDING_INTERVAL_ARG) .requires(REWARDING_ENABLED) ) .arg( - Arg::with_name(EPOCH_LENGTH_ARG) - .help("Length of the current rewarding epoch in hours") + Arg::with_name(INTERVAL_LENGTH_ARG) + .help("Length of the current rewarding interval in hours") .takes_value(true) - .long(EPOCH_LENGTH_ARG) + .long(INTERVAL_LENGTH_ARG) .requires(REWARDING_ENABLED) ) .arg( Arg::with_name(REWARDING_MONITOR_THRESHOLD_ARG) - .help("Specifies the minimum percentage of monitor test run data present in order to distribute rewards for given epoch.") + .help("Specifies the minimum percentage of monitor test run data present in order to distribute rewards for given interval.") .takes_value(true) .long(REWARDING_MONITOR_THRESHOLD_ARG) .requires(REWARDING_ENABLED) @@ -281,18 +283,18 @@ fn override_config(mut config: Config, matches: &ArgMatches) -> Config { config = config.with_mnemonic(mnemonic) } - if let Some(rewarding_epoch_datetime) = matches.value_of(FIRST_REWARDING_EPOCH_ARG) { - let first_epoch = OffsetDateTime::parse(rewarding_epoch_datetime, &Rfc3339) - .expect("Provided first epoch is not a valid rfc3339 datetime!"); - config = config.with_first_rewarding_epoch(first_epoch) + if let Some(rewarding_interval_datetime) = matches.value_of(FIRST_REWARDING_INTERVAL_ARG) { + let first_interval = OffsetDateTime::parse(rewarding_interval_datetime, &Rfc3339) + .expect("Provided first interval is not a valid rfc3339 datetime!"); + config = config.with_first_rewarding_interval(first_interval) } - if let Some(epoch_length) = matches - .value_of(EPOCH_LENGTH_ARG) + if let Some(interval_length) = matches + .value_of(INTERVAL_LENGTH_ARG) .map(|len| len.parse::()) { - let epoch_length = epoch_length.expect("Provided epoch length is not a number!"); - config = config.with_epoch_length(Duration::from_secs(epoch_length * 60 * 60)); + let interval_length = interval_length.expect("Provided interval length is not a number!"); + config = config.with_interval_length(Duration::from_secs(interval_length * 60 * 60)); } if let Some(monitor_threshold) = matches @@ -305,7 +307,7 @@ fn override_config(mut config: Config, matches: &ArgMatches) -> Config { monitor_threshold <= 100, "Provided monitor threshold is greater than 100!" ); - config = config.with_minimum_epoch_monitor_threshold(monitor_threshold) + config = config.with_minimum_interval_monitor_threshold(monitor_threshold) } #[cfg(feature = "coconut")] @@ -389,51 +391,44 @@ fn setup_network_monitor<'a>( } fn expected_monitor_test_runs(config: &Config) -> usize { - let epoch_length = config.get_epoch_length(); + let interval_length = config.get_interval_length(); let test_delay = config.get_network_monitor_run_interval(); // this is just a rough estimate. In real world there will be slightly fewer test runs // as they are not instantaneous and hence do not happen exactly every test_delay - (epoch_length.as_secs() / test_delay.as_secs()) as usize + (interval_length.as_secs() / test_delay.as_secs()) as usize } -fn setup_rewarder( +async fn setup_rewarder( config: &Config, - first_epoch: Epoch, rocket: &Rocket, nymd_client: &Client, -) -> Option { +) -> Result, ValidatorClientError> { if config.get_rewarding_enabled() && config.get_network_monitor_enabled() { // get instances of managed states let node_status_storage = rocket.state::().unwrap().clone(); let validator_cache = rocket.state::().unwrap().clone(); - Some(Rewarder::new( + Ok(Some(Rewarder::new( nymd_client.clone(), validator_cache, node_status_storage, - first_epoch, expected_monitor_test_runs(config), - config.get_minimum_epoch_monitor_threshold(), - )) + config.get_minimum_interval_monitor_threshold(), + ))) } else if config.get_rewarding_enabled() { warn!("Cannot enable rewarding with the network monitor being disabled"); - None + Ok(None) } else { - None + Ok(None) } } -async fn setup_rocket( - config: &Config, - first_epoch: Epoch, - liftoff_notify: Arc, -) -> Result> { +async fn setup_rocket(config: &Config, liftoff_notify: Arc) -> Result> { // let's build our rocket! let rocket = rocket::build() .attach(setup_cors()?) .attach(setup_liftoff_notify(liftoff_notify)) - .manage(first_epoch) .attach(ValidatorCache::stage()); #[cfg(feature = "coconut")] @@ -492,15 +487,10 @@ async fn run_validator_api(matches: ArgMatches<'static>) -> Result<()> { .map_err(|err| err.into()); } - let first_epoch = Epoch::new( - config.get_first_rewarding_epoch(), - config.get_epoch_length(), - ); - let liftoff_notify = Arc::new(Notify::new()); // let's build our rocket! - let rocket = setup_rocket(&config, first_epoch, Arc::clone(&liftoff_notify)).await?; + let rocket = setup_rocket(&config, Arc::clone(&liftoff_notify)).await?; let monitor_builder = setup_network_monitor(&config, system_version, &rocket); let validator_cache = rocket.state::().unwrap().clone(); @@ -508,11 +498,14 @@ async fn run_validator_api(matches: ArgMatches<'static>) -> Result<()> { // if network monitor is disabled, we're not going to be sending any rewarding hence // we're not starting signing client if config.get_network_monitor_enabled() { + let rewarded_set_update_notify = Arc::new(Notify::new()); + let nymd_client = Client::new_signing(&config); let validator_cache_refresher = ValidatorCacheRefresher::new( nymd_client.clone(), config.get_caching_interval(), validator_cache.clone(), + Some(Arc::clone(&rewarded_set_update_notify)), ); // spawn our cacher @@ -524,8 +517,20 @@ async fn run_validator_api(matches: ArgMatches<'static>) -> Result<()> { let uptime_updater = HistoricalUptimeUpdater::new(storage); tokio::spawn(async move { uptime_updater.run().await }); - if let Some(rewarder) = setup_rewarder(&config, first_epoch, &rocket, &nymd_client) { + if let Some(rewarder) = setup_rewarder(&config, &rocket, &nymd_client).await? { info!("Periodic rewarding is starting..."); + + let rewarded_set_updater = RewardedSetUpdater::new( + nymd_client.clone(), + rewarded_set_update_notify, + validator_cache.clone(), + ); + + // spawn rewarded set updater + tokio::spawn(async move { rewarded_set_updater.run().await }); + + // only update rewarded set if we're also distributing rewards + tokio::spawn(async move { rewarder.run().await }); } else { info!("Periodic rewarding is disabled."); @@ -536,6 +541,7 @@ async fn run_validator_api(matches: ArgMatches<'static>) -> Result<()> { nymd_client, config.get_caching_interval(), validator_cache, + None, ); // spawn our cacher diff --git a/validator-api/src/network_monitor/mod.rs b/validator-api/src/network_monitor/mod.rs index be629f9a05..855e2a1012 100644 --- a/validator-api/src/network_monitor/mod.rs +++ b/validator-api/src/network_monitor/mod.rs @@ -6,8 +6,8 @@ use futures::channel::mpsc; use gateway_client::bandwidth::BandwidthController; use std::sync::Arc; -use crate::cache::ValidatorCache; use crate::config::Config; +use crate::contract_cache::ValidatorCache; use crate::network_monitor::monitor::preparer::PacketPreparer; use crate::network_monitor::monitor::processor::{ ReceivedProcessor, ReceivedProcessorReceiver, ReceivedProcessorSender, diff --git a/validator-api/src/network_monitor/monitor/preparer.rs b/validator-api/src/network_monitor/monitor/preparer.rs index 6fed3f643a..6c832721f2 100644 --- a/validator-api/src/network_monitor/monitor/preparer.rs +++ b/validator-api/src/network_monitor/monitor/preparer.rs @@ -1,7 +1,7 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::cache::ValidatorCache; +use crate::contract_cache::ValidatorCache; use crate::network_monitor::chunker::Chunker; use crate::network_monitor::monitor::sender::GatewayPackets; use crate::network_monitor::test_packet::{NodeType, TestPacket}; @@ -185,7 +185,7 @@ impl PacketPreparer { let initialisation_backoff = Duration::from_secs(30); loop { let gateways = self.validator_cache.gateways().await; - let mixnodes = self.validator_cache.rewarded_mixnodes().await; + let mixnodes = self.validator_cache.rewarded_set().await; if gateways.into_inner().len() < minimum_full_routes { info!( @@ -227,7 +227,7 @@ impl PacketPreparer { async fn get_rewarded_nodes(&self) -> (Vec, Vec) { info!(target: "Monitor", "Obtaining network topology..."); - let mixnodes = self.validator_cache.rewarded_mixnodes().await.into_inner(); + let mixnodes = self.validator_cache.rewarded_set().await.into_inner(); let gateways = self.validator_cache.gateways().await.into_inner(); (mixnodes, gateways) @@ -262,12 +262,12 @@ impl PacketPreparer { n: usize, blacklist: &mut HashSet, ) -> Option> { - let rewarded_mixnodes = self.validator_cache.rewarded_mixnodes().await.into_inner(); + let rewarded_set = self.validator_cache.rewarded_set().await.into_inner(); let gateways = self.validator_cache.gateways().await.into_inner(); // separate mixes into layers for easier selection let mut layered_mixes = HashMap::new(); - for rewarded_mix in rewarded_mixnodes { + for rewarded_mix in rewarded_set { // filter out mixes on the blacklist if blacklist.contains(&rewarded_mix.mix_node.identity_key) { continue; @@ -450,14 +450,13 @@ impl PacketPreparer { test_nonce: u64, test_routes: &[TestRoute], ) -> PreparedPackets { - // only test mixnodes that are rewarded, i.e. that will be rewarded in this epoch. + // only test mixnodes that are rewarded, i.e. that will be rewarded in this interval. // (remember that "idle" nodes are still part of that set) // we don't care about other nodes, i.e. nodes that are bonded but will not get // any reward during the current rewarding interval - let (rewarded_mixnodes, all_gateways) = self.get_rewarded_nodes().await; + let (rewarded_set, all_gateways) = self.get_rewarded_nodes().await; - let (mixes, invalid_mixnodes) = - self.filter_outdated_and_malformed_mixnodes(rewarded_mixnodes); + let (mixes, invalid_mixnodes) = self.filter_outdated_and_malformed_mixnodes(rewarded_set); let (gateways, invalid_gateways) = self.filter_outdated_and_malformed_gateways(all_gateways); diff --git a/validator-api/src/node_status_api/mod.rs b/validator-api/src/node_status_api/mod.rs index 697fca4403..166e4ad61d 100644 --- a/validator-api/src/node_status_api/mod.rs +++ b/validator-api/src/node_status_api/mod.rs @@ -28,6 +28,7 @@ pub(crate) fn stage_full() -> AdHoc { routes::get_mixnode_status, routes::get_mixnode_reward_estimation, routes::get_mixnode_stake_saturation, + routes::get_mixnode_inclusion_probability, ], ) }) @@ -42,6 +43,7 @@ pub(crate) fn stage_minimal() -> AdHoc { routes![ routes::get_mixnode_status, routes::get_mixnode_stake_saturation, + routes::get_mixnode_inclusion_probability, ], ) }) diff --git a/validator-api/src/node_status_api/routes.rs b/validator-api/src/node_status_api/routes.rs index 986c0d5919..3e7008a905 100644 --- a/validator-api/src/node_status_api/routes.rs +++ b/validator-api/src/node_status_api/routes.rs @@ -6,14 +6,13 @@ use crate::node_status_api::models::{ MixnodeUptimeHistory, }; use crate::storage::ValidatorApiStorage; -use crate::{Epoch, ValidatorCache}; +use crate::ValidatorCache; use rocket::http::Status; use rocket::serde::json::Json; use rocket::State; -use time::OffsetDateTime; use validator_api_requests::models::{ - CoreNodeStatusResponse, MixnodeStatusResponse, RewardEstimationResponse, - StakeSaturationResponse, + CoreNodeStatusResponse, InclusionProbabilityResponse, MixnodeStatusResponse, + RewardEstimationResponse, StakeSaturationResponse, }; #[get("/mixnode//report")] @@ -112,26 +111,25 @@ pub(crate) async fn get_mixnode_status( pub(crate) async fn get_mixnode_reward_estimation( cache: &State, storage: &State, - first_epoch: &State, identity: String, ) -> Result, ErrorResponse> { let (bond, status) = cache.mixnode_details(&identity).await; if let Some(bond) = bond { - let epoch_reward_params = cache.epoch_reward_params().await; - let as_at = epoch_reward_params.timestamp(); - let epoch_reward_params = epoch_reward_params.into_inner(); + let interval_reward_params = cache.interval_reward_params().await; + let as_at = interval_reward_params.timestamp(); + let interval_reward_params = interval_reward_params.into_inner(); - let current_epoch = first_epoch.current(OffsetDateTime::now_utc()); + let current_interval = cache.current_interval().await.into_inner(); let uptime = storage .get_average_mixnode_uptime_in_interval( &identity, - current_epoch.start_unix_timestamp(), - current_epoch.end_unix_timestamp(), + current_interval.start_unix_timestamp(), + current_interval.end_unix_timestamp(), ) .await .map_err(|err| ErrorResponse::new(err.to_string(), Status::NotFound))?; - match epoch_reward_params.estimate_reward(&bond, uptime.u8(), status.is_active()) { + match interval_reward_params.estimate_reward(&bond, uptime.u8(), status.is_active()) { Ok(( estimated_total_node_reward, estimated_operator_reward, @@ -141,9 +139,9 @@ pub(crate) async fn get_mixnode_reward_estimation( estimated_total_node_reward, estimated_operator_reward, estimated_delegators_reward, - current_epoch_start: current_epoch.start_unix_timestamp(), - current_epoch_end: current_epoch.end_unix_timestamp(), - current_epoch_uptime: uptime.u8(), + current_interval_start: current_interval.start_unix_timestamp(), + current_interval_end: current_interval.end_unix_timestamp(), + current_interval_uptime: uptime.u8(), as_at, }; Ok(Json(reponse)) @@ -168,13 +166,13 @@ pub(crate) async fn get_mixnode_stake_saturation( ) -> Result, ErrorResponse> { let (bond, _) = cache.mixnode_details(&identity).await; if let Some(bond) = bond { - let epoch_reward_params = cache.epoch_reward_params().await; - let as_at = epoch_reward_params.timestamp(); - let epoch_reward_params = epoch_reward_params.into_inner(); + let interval_reward_params = cache.interval_reward_params().await; + let as_at = interval_reward_params.timestamp(); + let interval_reward_params = interval_reward_params.into_inner(); let saturation = bond.stake_saturation( - epoch_reward_params.circulating_supply, - epoch_reward_params.rewarded_set_size, + interval_reward_params.circulating_supply, + interval_reward_params.rewarded_set_size, ); Ok(Json(StakeSaturationResponse { @@ -188,3 +186,45 @@ pub(crate) async fn get_mixnode_stake_saturation( )) } } + +#[get("/mixnode//inclusion-probability")] +pub(crate) async fn get_mixnode_inclusion_probability( + cache: &State, + identity: String, +) -> Json> { + let mixnodes = cache.mixnodes().await.into_inner(); + + if let Some(target_mixnode) = mixnodes.iter().find(|x| x.identity() == &identity) { + let total_bonded_tokens = mixnodes + .iter() + .fold(0u128, |acc, x| acc + x.total_bond().unwrap_or_default()) + as f64; + + let rewarding_params = cache.interval_reward_params().await.into_inner(); + let rewarded_set_size = rewarding_params.rewarded_set_size as f64; + let active_set_size = rewarding_params.active_set_size as f64; + + let prob_one_draw = + target_mixnode.total_bond().unwrap_or_default() as f64 / total_bonded_tokens; + // Chance to be selected in any draw for active set + let prob_active_set = active_set_size * prob_one_draw; + // This is likely slightly too high, as we're not correcting form them not being selected in active, should be chance to be selected, minus the chance for being not selected in reserve + let prob_reserve_set = (rewarded_set_size - active_set_size) * prob_one_draw; + // (rewarded_set_size - active_set_size) * prob_one_draw * (1. - prob_active_set); + + Json(Some(InclusionProbabilityResponse { + in_active: if prob_active_set > 1. { + 1. + } else { + prob_active_set + } as f32, + in_reserve: if prob_reserve_set > 1. { + 1. + } else { + prob_reserve_set + } as f32, + })) + } else { + Json(None) + } +} diff --git a/validator-api/src/nymd_client.rs b/validator-api/src/nymd_client.rs index 1481ad2682..5c5dcb8b79 100644 --- a/validator-api/src/nymd_client.rs +++ b/validator-api/src/nymd_client.rs @@ -2,11 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 use crate::config::Config; -use crate::rewarding::{error::RewardingError, EpochRewardParams, MixnodeToReward}; +use crate::rewarding::{error::RewardingError, IntervalRewardParams, MixnodeToReward}; use config::defaults::DEFAULT_VALIDATOR_API_PORT; use mixnet_contract_common::{ - ContractStateParams, Delegation, ExecuteMsg, GatewayBond, IdentityKey, MixNodeBond, - MixnodeRewardingStatusResponse, RewardingIntervalResponse, MIXNODE_DELEGATORS_PAGE_LIMIT, + ContractStateParams, Delegation, ExecuteMsg, GatewayBond, IdentityKey, Interval, MixNodeBond, + MixnodeRewardingStatusResponse, RewardedSetNodeStatus, RewardedSetUpdateDetails, + MIXNODE_DELEGATORS_PAGE_LIMIT, }; use serde::Serialize; use std::sync::Arc; @@ -80,7 +81,6 @@ impl Client { impl Client { // a helper function for the future to obtain the current block timestamp - #[allow(dead_code)] pub(crate) async fn current_block_timestamp( &self, ) -> Result @@ -98,6 +98,13 @@ impl Client { Ok(time) } + pub(crate) async fn get_current_interval(&self) -> Result + where + C: CosmWasmClient + Sync, + { + Ok(self.0.read().await.get_current_interval().await?) + } + pub(crate) async fn get_mixnodes(&self) -> Result, ValidatorClientError> where C: CosmWasmClient + Sync, @@ -112,6 +119,9 @@ impl Client { self.0.read().await.get_all_nymd_gateways().await } + #[allow(dead_code)] + // I've got a feeling we will need this again very soon, so I'd rather not remove this + // (and all subcalls in the various clients) just yet pub(crate) async fn get_contract_settings( &self, ) -> Result @@ -121,18 +131,9 @@ impl Client { self.0.read().await.get_contract_settings().await } - pub(crate) async fn get_current_rewarding_interval( + pub(crate) async fn get_current_interval_reward_params( &self, - ) -> Result - where - C: CosmWasmClient + Sync, - { - self.0.read().await.get_current_rewarding_interval().await - } - - pub(crate) async fn get_current_epoch_reward_params( - &self, - ) -> Result + ) -> Result where C: CosmWasmClient + Sync, { @@ -140,25 +141,25 @@ impl Client { let state = this.get_contract_settings().await?; let reward_pool = this.get_reward_pool().await?; - let epoch_reward_percent = this.get_epoch_reward_percent().await?; + let interval_reward_percent = this.get_interval_reward_percent().await?; - let epoch_reward_params = EpochRewardParams { + let interval_reward_params = IntervalRewardParams { reward_pool, circulating_supply: this.get_circulating_supply().await?, sybil_resistance_percent: this.get_sybil_resistance_percent().await?, rewarded_set_size: state.mixnode_rewarded_set_size, active_set_size: state.mixnode_active_set_size, - period_reward_pool: (reward_pool / 100) * epoch_reward_percent as u128, + period_reward_pool: (reward_pool / 100) * interval_reward_percent as u128, active_set_work_factor: state.active_set_work_factor, }; - Ok(epoch_reward_params) + Ok(interval_reward_params) } pub(crate) async fn get_rewarding_status( &self, mix_identity: mixnet_contract_common::IdentityKey, - rewarding_interval_nonce: u32, + interval_id: u32, ) -> Result where C: CosmWasmClient + Sync, @@ -166,7 +167,7 @@ impl Client { self.0 .read() .await - .get_rewarding_status(mix_identity, rewarding_interval_nonce) + .get_rewarding_status(mix_identity, interval_id) .await } @@ -176,7 +177,8 @@ impl Client { /// # Arguments /// /// * `height`: height of the block for which we want to obtain the hash. - pub async fn get_block_hash( + #[allow(dead_code)] + pub(crate) async fn get_block_hash( &self, height: u32, ) -> Result, ValidatorClientError> @@ -205,26 +207,45 @@ impl Client { .await } - pub(crate) async fn begin_mixnode_rewarding( + pub(crate) async fn get_rewarded_set_identities( &self, - rewarding_interval_nonce: u32, - ) -> Result<(), RewardingError> + ) -> Result, ValidatorClientError> + where + C: CosmWasmClient + Sync, + { + self.0 + .read() + .await + .get_all_nymd_rewarded_set_mixnode_identities() + .await + } + + pub(crate) async fn get_current_rewarded_set_update_details( + &self, + ) -> Result + where + C: CosmWasmClient + Sync, + { + self.0 + .read() + .await + .get_current_rewarded_set_update_details() + .await + } + + pub(crate) async fn advance_current_interval(&self) -> Result<(), ValidatorClientError> where C: SigningCosmWasmClient + Sync, { - self.0 - .write() - .await - .nymd - .begin_mixnode_rewarding(rewarding_interval_nonce) - .await?; + self.0.write().await.nymd.advance_current_interval().await?; Ok(()) } - pub(crate) async fn finish_mixnode_rewarding( + pub(crate) async fn write_rewarded_set( &self, - rewarding_interval_nonce: u32, - ) -> Result<(), RewardingError> + rewarded_set: Vec, + expected_active_set_size: u32, + ) -> Result<(), ValidatorClientError> where C: SigningCosmWasmClient + Sync, { @@ -232,7 +253,7 @@ impl Client { .write() .await .nymd - .finish_mixnode_rewarding(rewarding_interval_nonce) + .write_rewarded_set(rewarded_set, expected_active_set_size) .await?; Ok(()) } @@ -240,7 +261,7 @@ impl Client { pub(crate) async fn reward_mixnode_and_all_delegators( &self, node: &MixnodeToReward, - rewarding_interval_nonce: u32, + interval_id: u32, ) -> Result<(), RewardingError> where C: SigningCosmWasmClient + Sync, @@ -250,7 +271,7 @@ impl Client { let further_calls = node.total_delegations / MIXNODE_DELEGATORS_PAGE_LIMIT; // start with the base call to reward operator and first page of delegators - let msgs = vec![(node.to_reward_execute_msg(rewarding_interval_nonce), vec![])]; + let msgs = vec![(node.to_reward_execute_msg(interval_id), vec![])]; let memo = format!( "operator + {} delegators rewarding", MIXNODE_DELEGATORS_PAGE_LIMIT @@ -261,7 +282,7 @@ impl Client { // reward rest of delegators let mut remaining_delegators = node.total_delegations - MIXNODE_DELEGATORS_PAGE_LIMIT; let delegator_rewarding_msg = ( - node.to_next_delegator_reward_execute_msg(rewarding_interval_nonce), + node.to_next_delegator_reward_execute_msg(interval_id), vec![], ); for _ in 0..further_calls { @@ -280,7 +301,7 @@ impl Client { pub(crate) async fn reward_mix_delegators( &self, node: &MixnodeToReward, - rewarding_interval_nonce: u32, + interval_id: u32, ) -> Result<(), RewardingError> where C: SigningCosmWasmClient + Sync, @@ -288,7 +309,7 @@ impl Client { // the fee is a tricky subject here because we don't know exactly how many delegators we missed, // let's aim for the worst case scenario and assume it was the entire page let delegator_rewarding_msg = ( - node.to_next_delegator_reward_execute_msg(rewarding_interval_nonce), + node.to_next_delegator_reward_execute_msg(interval_id), vec![], ); @@ -300,14 +321,14 @@ impl Client { pub(crate) async fn reward_mixnodes_with_single_page_of_delegators( &self, nodes: &[MixnodeToReward], - rewarding_interval_nonce: u32, + interval_id: u32, ) -> Result<(), RewardingError> where C: SigningCosmWasmClient + Sync, { let msgs: Vec<(ExecuteMsg, _)> = nodes .iter() - .map(|node| node.to_reward_execute_msg(rewarding_interval_nonce)) + .map(|node| node.to_reward_execute_msg(interval_id)) .zip(std::iter::repeat(Vec::new())) .collect(); diff --git a/validator-api/src/rewarded_set_updater/mod.rs b/validator-api/src/rewarded_set_updater/mod.rs new file mode 100644 index 0000000000..0cc912b33a --- /dev/null +++ b/validator-api/src/rewarded_set_updater/mod.rs @@ -0,0 +1,112 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +// there is couple of reasons for putting this in a separate module: +// 1. I didn't feel it fit well in validator "cache". It seems like purpose of cache is to just keep updating local data +// rather than attempting to change global view (i.e. the active set) +// +// 2. However, even if it was to exist in the validator cache refresher, we'd have to create a different "run" +// method as it doesn't have access to the signing client which we need in the case of updating rewarded sets +// (because validator cache can be run by anyone regardless of whether, say, network monitor exists) +// +// 3. Eventually this whole procedure is going to get expanded to allow for distribution of rewarded set generation +// and hence this might be a good place for it. + +use crate::contract_cache::ValidatorCache; +use crate::nymd_client::Client; +use mixnet_contract_common::{IdentityKey, MixNodeBond}; +use rand::prelude::SliceRandom; +use rand::rngs::OsRng; +use std::sync::Arc; +use tokio::sync::Notify; +use validator_client::nymd::SigningNymdClient; + +pub struct RewardedSetUpdater { + nymd_client: Client, + update_rewarded_set_notify: Arc, + validator_cache: ValidatorCache, +} + +impl RewardedSetUpdater { + pub(crate) fn new( + nymd_client: Client, + update_rewarded_set_notify: Arc, + validator_cache: ValidatorCache, + ) -> Self { + RewardedSetUpdater { + nymd_client, + update_rewarded_set_notify, + validator_cache, + } + } + + fn determine_rewarded_set( + &self, + mixnodes: Vec, + nodes_to_select: u32, + ) -> Vec { + if mixnodes.is_empty() { + return Vec::new(); + } + + let mut rng = OsRng; + + // generate list of mixnodes and their relatively weight (by total stake) + let choices = mixnodes + .into_iter() + .map(|mix| { + // note that the theoretical maximum possible stake is equal to the total + // supply of all tokens, i.e. 1B (which is 1 quadrillion of native tokens, i.e. 10^15 ~ 2^50) + // which is way below maximum value of f64, so the cast is fine + let total_stake = mix.total_bond().unwrap_or_default() as f64; + (mix.mix_node.identity_key, total_stake) + }) // if for some reason node is invalid, treat it as 0 stake/weight + .collect::>(); + + // the unwrap here is fine as an error can only be thrown under one of the following conditions: + // - our mixnode list is empty - we have already checked for that + // - we have invalid weights, i.e. less than zero or NaNs - it shouldn't happen in our case as we safely cast down from u128 + // - all weights are zero - it's impossible in our case as the list of nodes is not empty and weight is proportional to stake. You must have non-zero stake in order to bond + // - we have more than u32::MAX values (which is incredibly unrealistic to have 4B mixnodes bonded... literally every other person on the planet would need one) + choices + .choose_multiple_weighted(&mut rng, nodes_to_select as usize, |item| item.1) + .unwrap() + .map(|(identity, _weight)| identity.clone()) + .collect() + } + + async fn update_rewarded_set(&self) { + // we know the entries are not stale, as a matter of fact they were JUST updated, since we got notified + let all_nodes = self.validator_cache.mixnodes().await.into_inner(); + let rewarding_params = self + .validator_cache + .interval_reward_params() + .await + .into_inner(); + + let rewarded_set_size = rewarding_params.rewarded_set_size; + let active_set_size = rewarding_params.active_set_size; + + // note that top k nodes are in the active set + let new_rewarded_set = self.determine_rewarded_set(all_nodes, rewarded_set_size); + if let Err(err) = self + .nymd_client + .write_rewarded_set(new_rewarded_set, active_set_size) + .await + { + log::error!("failed to update the rewarded set - {}", err) + // note that if the transaction failed to get executed because, I don't know, there was a networking hiccup + // the cache will notify the updater on its next round + } + } + + pub(crate) async fn run(&self) { + self.validator_cache.wait_for_initial_values().await; + + loop { + // wait until the cache refresher determined its time to update the rewarded/active sets + self.update_rewarded_set_notify.notified().await; + self.update_rewarded_set().await; + } + } +} diff --git a/validator-api/src/rewarding/epoch.rs b/validator-api/src/rewarding/epoch.rs deleted file mode 100644 index 5bba761647..0000000000 --- a/validator-api/src/rewarding/epoch.rs +++ /dev/null @@ -1,219 +0,0 @@ -// Copyright 2021 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::storage::UnixTimestamp; -use config::defaults::{DEFAULT_EPOCH_LENGTH, DEFAULT_FIRST_EPOCH_START}; -use std::fmt::{Display, Formatter}; -use std::time::Duration; -use time::OffsetDateTime; - -// TODO: perhaps this should be moved to a commons crate? -// And become representation of system epoch? -/// Representation of rewarding epoch. -#[derive(Debug, Clone, Copy, Eq, PartialEq)] -pub struct Epoch { - start: OffsetDateTime, - length: Duration, -} - -impl Epoch { - /// Creates new epoch instance. - pub const fn new(start: OffsetDateTime, length: Duration) -> Self { - Epoch { start, length } - } - - /// Returns the next epoch. - pub fn next_epoch(&self) -> Self { - Epoch { - start: self.end(), - length: self.length, - } - } - - /// Returns the last epoch. - pub fn previous_epoch(&self) -> Self { - Epoch { - start: self.start - self.length, - length: self.length, - } - } - - /// Determines whether the provided datetime is contained within the epoch - /// - /// # Arguments - /// - /// * `datetime`: specified datetime - pub fn contains(&self, datetime: OffsetDateTime) -> bool { - self.start <= datetime && datetime <= self.end() - } - - /// Returns new instance of [Epoch] such that the provided datetime would be within - /// its duration. - /// - /// # Arguments - /// - /// * `now`: current datetime - pub fn current(&self, now: OffsetDateTime) -> Self { - let mut candidate = *self; - - if now > self.start { - loop { - if candidate.contains(now) { - return candidate; - } - candidate = candidate.next_epoch(); - } - } else { - loop { - if candidate.contains(now) { - return candidate; - } - candidate = candidate.previous_epoch(); - } - } - } - - /// Returns the starting datetime of this epoch. - pub const fn start(&self) -> OffsetDateTime { - self.start - } - - /// Returns the length of this epoch. - pub const fn length(&self) -> Duration { - self.length - } - - /// Returns the ending datetime of this epoch. - pub fn end(&self) -> OffsetDateTime { - self.start + self.length - } - - /// Returns the unix timestamp of the start of this epoch. - pub const fn start_unix_timestamp(&self) -> UnixTimestamp { - self.start().unix_timestamp() - } - - /// Returns the unix timestamp of the end of this epoch. - pub fn end_unix_timestamp(&self) -> UnixTimestamp { - self.end().unix_timestamp() - } -} - -impl Display for Epoch { - fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { - let length = self.length(); - let hours = length.as_secs_f32() / 3600.0; - write!( - f, - "Epoch: {} - {} ({:.1} hours)", - self.start(), - self.end(), - hours - ) - } -} - -impl Default for Epoch { - fn default() -> Self { - Epoch { - start: DEFAULT_FIRST_EPOCH_START, - length: DEFAULT_EPOCH_LENGTH, - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn previous_epoch() { - let epoch = Epoch { - start: time::macros::datetime!(2021-08-23 12:00 UTC), - length: Duration::from_secs(24 * 60 * 60), - }; - let expected = Epoch { - start: time::macros::datetime!(2021-08-22 12:00 UTC), - length: Duration::from_secs(24 * 60 * 60), - }; - - assert_eq!(expected, epoch.previous_epoch()) - } - - #[test] - fn next_epoch() { - let epoch = Epoch { - start: time::macros::datetime!(2021-08-23 12:00 UTC), - length: Duration::from_secs(24 * 60 * 60), - }; - let expected = Epoch { - start: time::macros::datetime!(2021-08-24 12:00 UTC), - length: Duration::from_secs(24 * 60 * 60), - }; - - assert_eq!(expected, epoch.next_epoch()) - } - - #[test] - fn checking_for_datetime_inclusion() { - let epoch = Epoch { - start: time::macros::datetime!(2021-08-23 12:00 UTC), - length: Duration::from_secs(24 * 60 * 60), - }; - - // it must contain its own boundaries - assert!(epoch.contains(epoch.start)); - assert!(epoch.contains(epoch.end())); - - let in_the_midle = epoch.start + Duration::from_secs(epoch.length.as_secs() / 2); - assert!(epoch.contains(in_the_midle)); - - assert!(!epoch.contains(epoch.next_epoch().end())); - assert!(!epoch.contains(epoch.previous_epoch().start())); - } - - #[test] - fn determining_current_epoch() { - let first_epoch = Epoch { - start: time::macros::datetime!(2021-08-23 12:00 UTC), - length: Duration::from_secs(24 * 60 * 60), - }; - - // epoch just before - let fake_now = first_epoch.start - Duration::from_secs(123); - assert_eq!(first_epoch.previous_epoch(), first_epoch.current(fake_now)); - - // this epoch (start boundary) - assert_eq!(first_epoch, first_epoch.current(first_epoch.start)); - - // this epoch (in the middle) - let fake_now = first_epoch.start + Duration::from_secs(123); - assert_eq!(first_epoch, first_epoch.current(fake_now)); - - // this epoch (end boundary) - assert_eq!(first_epoch, first_epoch.current(first_epoch.end())); - - // next epoch - let fake_now = first_epoch.end() + Duration::from_secs(123); - assert_eq!(first_epoch.next_epoch(), first_epoch.current(fake_now)); - - // few epochs in the past - let fake_now = - first_epoch.start() - first_epoch.length - first_epoch.length - first_epoch.length; - assert_eq!( - first_epoch - .previous_epoch() - .previous_epoch() - .previous_epoch(), - first_epoch.current(fake_now) - ); - - // few epochs in the future - let fake_now = - first_epoch.end() + first_epoch.length + first_epoch.length + first_epoch.length; - assert_eq!( - first_epoch.next_epoch().next_epoch().next_epoch(), - first_epoch.current(fake_now) - ); - } -} diff --git a/validator-api/src/rewarding/mod.rs b/validator-api/src/rewarding/mod.rs index fc67a7bba8..f14432db56 100644 --- a/validator-api/src/rewarding/mod.rs +++ b/validator-api/src/rewarding/mod.rs @@ -1,10 +1,9 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::cache::ValidatorCache; +use crate::contract_cache::ValidatorCache; use crate::node_status_api::ONE_DAY; use crate::nymd_client::Client; -use crate::rewarding::epoch::Epoch; use crate::rewarding::error::RewardingError; use crate::storage::models::{ FailedMixnodeRewardChunk, PossiblyUnrewardedMixnode, RewardingReport, @@ -14,19 +13,20 @@ use config::defaults::DENOM; use log::{error, info}; use mixnet_contract_common::mixnode::NodeRewardParams; use mixnet_contract_common::{ - ExecuteMsg, IdentityKey, MixNodeBond, RewardingStatus, MIXNODE_DELEGATORS_PAGE_LIMIT, + ExecuteMsg, IdentityKey, Interval, MixNodeBond, RewardingStatus, MIXNODE_DELEGATORS_PAGE_LIMIT, }; +use std::collections::HashSet; use std::convert::TryInto; +use std::process; use std::time::Duration; use time::OffsetDateTime; use tokio::time::sleep; use validator_client::nymd::SigningNymdClient; -pub(crate) mod epoch; pub(crate) mod error; #[derive(Copy, Clone)] -pub(crate) struct EpochRewardParams { +pub(crate) struct IntervalRewardParams { pub(crate) reward_pool: u128, pub(crate) circulating_supply: u128, pub(crate) sybil_resistance_percent: u8, @@ -36,13 +36,13 @@ pub(crate) struct EpochRewardParams { pub(crate) active_set_work_factor: u8, } -impl EpochRewardParams { +impl IntervalRewardParams { // technically it's identical to what would have been derived with a Default implementation, // however, I prefer to be explicit about it, as a `Default::default` value makes no sense // apart from the `ValidatorCacheInner` context, where this value is not going to be touched anyway // (it's guarded behind an `initialised` flag) pub(crate) fn new_empty() -> Self { - EpochRewardParams { + IntervalRewardParams { reward_pool: 0, circulating_supply: 0, sybil_resistance_percent: 0, @@ -106,21 +106,18 @@ impl MixnodeToReward { } impl MixnodeToReward { - pub(crate) fn to_reward_execute_msg(&self, rewarding_interval_nonce: u32) -> ExecuteMsg { + pub(crate) fn to_reward_execute_msg(&self, interval_id: u32) -> ExecuteMsg { ExecuteMsg::RewardMixnode { identity: self.identity.clone(), params: self.params(), - rewarding_interval_nonce, + interval_id, } } - pub(crate) fn to_next_delegator_reward_execute_msg( - &self, - rewarding_interval_nonce: u32, - ) -> ExecuteMsg { + pub(crate) fn to_next_delegator_reward_execute_msg(&self, interval_id: u32) -> ExecuteMsg { ExecuteMsg::RewardNextMixDelegators { mix_identity: self.identity.clone(), - rewarding_interval_nonce, + interval_id, } } } @@ -140,17 +137,14 @@ pub(crate) struct Rewarder { validator_cache: ValidatorCache, storage: ValidatorApiStorage, - /// The first epoch of the current length. - first_epoch: Epoch, - - /// Ideal world, expected number of network monitor test runs per epoch. + /// Ideal world, expected number of network monitor test runs per interval. /// In reality it will be slightly lower due to network delays, but it's good enough /// for estimations regarding percentage of available data for reward distribution. - expected_epoch_monitor_runs: usize, + expected_interval_monitor_runs: usize, /// Minimum percentage of network monitor test runs reports required in order to distribute /// rewards. - minimum_epoch_monitor_threshold: u8, + minimum_interval_monitor_threshold: u8, } impl Rewarder { @@ -158,17 +152,15 @@ impl Rewarder { nymd_client: Client, validator_cache: ValidatorCache, storage: ValidatorApiStorage, - first_epoch: Epoch, - expected_epoch_monitor_runs: usize, - minimum_epoch_monitor_threshold: u8, + expected_interval_monitor_runs: usize, + minimum_interval_monitor_threshold: u8, ) -> Self { Rewarder { nymd_client, validator_cache, storage, - first_epoch, - expected_epoch_monitor_runs, - minimum_epoch_monitor_threshold, + expected_interval_monitor_runs, + minimum_interval_monitor_threshold, } } @@ -188,7 +180,7 @@ impl Rewarder { .len()) } - /// Obtain the list of current 'rewarded' set, determine their uptime in the provided epoch + /// Obtain the list of current 'rewarded' set, determine their uptime in the provided interval /// and attach information required for rewarding. /// /// The method also obtains the number of delegators towards the node in order to more accurately @@ -196,72 +188,78 @@ impl Rewarder { /// /// # Arguments /// - /// * `epoch`: current rewarding epoch + /// * `interval`: current rewarding interval async fn determine_eligible_mixnodes( &self, - epoch: Epoch, + interval: Interval, ) -> Result, RewardingError> { - // Currently we don't have as many 'features' as in the typescript reward script, - // such as we don't check ports or verloc data anymore. However, that's fine as - // it's a good price to pay for being able to move rewarding to rust - // 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 epoch_reward_params = self.nymd_client.get_current_epoch_reward_params().await?; + let interval_reward_params = self + .nymd_client + .get_current_interval_reward_params() + .await?; info!("Rewarding pool stats"); info!( "-- Reward pool: {} {}", - epoch_reward_params.reward_pool, DENOM + interval_reward_params.reward_pool, DENOM ); info!( - "---- Epoch reward pool: {} {}", - epoch_reward_params.period_reward_pool, DENOM + "---- Interval reward pool: {} {}", + interval_reward_params.period_reward_pool, DENOM ); info!( "-- Circulating supply: {} {}", - epoch_reward_params.circulating_supply, DENOM + interval_reward_params.circulating_supply, DENOM ); - // 1. get list of 'rewarded' nodes + // 1. get list of all currently bonded nodes // 2. for each of them determine their delegator count - // 3. for each of them determine their uptime for the epoch - let rewarded_nodes = self.validator_cache.rewarded_mixnodes().await.into_inner(); - let mut nodes_with_delegations = Vec::with_capacity(rewarded_nodes.len()); - for rewarded_node in rewarded_nodes { + // 3. for each of them determine their uptime for the interval + // 4. for each of them determine if they're currently in the active set + // TODO: step 4 will definitely need to change. + let all_nodes = self.validator_cache.mixnodes().await.into_inner(); + let active_set = self + .validator_cache + .active_set() + .await + .into_inner() + .into_iter() + .map(|bond| bond.mix_node.identity_key) + .collect::>(); + + let mut nodes_with_delegations = Vec::with_capacity(all_nodes.len()); + for node in all_nodes { let delegator_count = self - .get_mixnode_delegators_count(rewarded_node.mix_node.identity_key.clone()) + .get_mixnode_delegators_count(node.mix_node.identity_key.clone()) .await?; - nodes_with_delegations.push((rewarded_node, delegator_count)); + nodes_with_delegations.push((node, delegator_count)); } let mut eligible_nodes = Vec::with_capacity(nodes_with_delegations.len()); - for (i, (rewarded_node, total_delegations)) in - nodes_with_delegations.into_iter().enumerate() - { + for (rewarded_node, total_delegations) in nodes_with_delegations.into_iter() { let uptime = self .storage .get_average_mixnode_uptime_in_interval( rewarded_node.identity(), - epoch.start_unix_timestamp(), - epoch.end_unix_timestamp(), + interval.start_unix_timestamp(), + interval.end_unix_timestamp(), ) .await?; eligible_nodes.push(MixnodeToReward { - identity: rewarded_node.mix_node.identity_key, + identity: rewarded_node.identity().clone(), total_delegations, params: NodeRewardParams::new( - epoch_reward_params.period_reward_pool, - epoch_reward_params.rewarded_set_size.into(), - epoch_reward_params.active_set_size.into(), + interval_reward_params.period_reward_pool, + interval_reward_params.rewarded_set_size.into(), + interval_reward_params.active_set_size.into(), // Reward blockstamp gets set in the contract call 0, - epoch_reward_params.circulating_supply, + interval_reward_params.circulating_supply, uptime.u8().into(), - epoch_reward_params.sybil_resistance_percent, - i < epoch_reward_params.active_set_size as usize, - epoch_reward_params.active_set_work_factor, + interval_reward_params.sybil_resistance_percent, + active_set.contains(rewarded_node.identity()), + interval_reward_params.active_set_work_factor, ), }) } @@ -277,7 +275,7 @@ impl Rewarder { /// onto the next page that the validator API was not aware of. /// /// * `eligible_mixnodes`: list of the nodes that were eligible to receive rewards. - /// * `rewarding_interval_nonce`: nonce associated with the current rewarding interval + /// * `interval_id`: nonce associated with the current rewarding interval async fn verify_rewarding_completion( &self, eligible_mixnodes: &[MixnodeToReward], @@ -343,12 +341,12 @@ impl Rewarder { /// # Arguments /// /// * `eligible_mixnodes`: list of the nodes that are eligible to receive rewards. - /// * `rewarding_interval_nonce`: nonce associated with the current rewarding interval. + /// * `interval_id`: nonce associated with the current rewarding interval. /// * `retry`: flag to indicate whether this is a retry attempt for rewarding particular nodes. async fn distribute_rewards_to_mixnodes( &self, eligible_mixnodes: &[MixnodeToReward], - rewarding_interval_nonce: u32, + interval_id: u32, retry: bool, ) -> Option> { if retry { @@ -405,7 +403,7 @@ impl Rewarder { for mix in individually_rewarded { if let Err(err) = self .nymd_client - .reward_mixnode_and_all_delegators(mix, rewarding_interval_nonce) + .reward_mixnode_and_all_delegators(mix, interval_id) .await { // this is a super weird edge case that we didn't catch change to sequence and @@ -432,10 +430,7 @@ impl Rewarder { } if let Err(err) = self .nymd_client - .reward_mixnodes_with_single_page_of_delegators( - &mix_chunk, - rewarding_interval_nonce, - ) + .reward_mixnodes_with_single_page_of_delegators(&mix_chunk, interval_id) .await { // this is a super weird edge case that we didn't catch change to sequence and @@ -469,13 +464,9 @@ impl Rewarder { /// Realistically if this method is ever called, it will be only done once per node, so there's /// no need to determine the exact number of missed delegators. /// - /// * `nodes`: mixnodes which delegators did not receive all rewards in this epoch. - /// * `rewarding_interval_nonce`: nonce associated with the current rewarding interval. - async fn reward_missed_delegators( - &self, - nodes: &[MixnodeToReward], - rewarding_interval_nonce: u32, - ) { + /// * `nodes`: mixnodes which delegators did not receive all rewards in this interval. + /// * `interval_id`: nonce associated with the current rewarding interval. + async fn reward_missed_delegators(&self, nodes: &[MixnodeToReward], interval_id: u32) { let mut total_resent = 0; for missed_node in nodes { total_resent += 1; @@ -487,7 +478,7 @@ impl Rewarder { if let Err(err) = self .nymd_client - .reward_mix_delegators(missed_node, rewarding_interval_nonce) + .reward_mix_delegators(missed_node, interval_id) .await { warn!( @@ -503,31 +494,23 @@ impl Rewarder { /// /// # Arguments /// - /// * `epoch_rewarding_id`: id of the current epoch rewarding as stored in the database. - /// * `epoch`: current rewarding epoch + /// * `interval_rewarding_id`: id of the current interval rewarding as stored in the database. + /// * `interval`: current rewarding interval async fn distribute_rewards( &self, - epoch_rewarding_database_id: i64, - epoch: Epoch, + interval_rewarding_database_id: i64, + interval: Interval, ) -> Result<(RewardingReport, Option), RewardingError> { let mut failure_data = FailureData::default(); - let eligible_mixnodes = self.determine_eligible_mixnodes(epoch).await?; + let eligible_mixnodes = self.determine_eligible_mixnodes(interval).await?; if eligible_mixnodes.is_empty() { return Err(RewardingError::NoMixnodesToReward); } let total_eligible = eligible_mixnodes.len(); - let current_rewarding_nonce = self - .nymd_client - .get_current_rewarding_interval() - .await? - .current_rewarding_interval_nonce; - self.nymd_client - .begin_mixnode_rewarding(current_rewarding_nonce + 1) - .await?; failure_data.mixnodes = self - .distribute_rewards_to_mixnodes(&eligible_mixnodes, current_rewarding_nonce + 1, false) + .distribute_rewards_to_mixnodes(&eligible_mixnodes, interval.id(), false) .await; let mut nodes_to_verify = eligible_mixnodes; @@ -539,7 +522,7 @@ impl Rewarder { break; } let (unrewarded, mut pending_delegators) = self - .verify_rewarding_completion(&nodes_to_verify, current_rewarding_nonce + 1) + .verify_rewarding_completion(&nodes_to_verify, interval.id()) .await; if unrewarded.is_empty() && pending_delegators.is_empty() { // we're all good - everyone got their rewards @@ -548,12 +531,12 @@ impl Rewarder { if !unrewarded.is_empty() { // no need to save failure data as we already know about those from the very first run - self.distribute_rewards_to_mixnodes(&unrewarded, current_rewarding_nonce + 1, true) + self.distribute_rewards_to_mixnodes(&unrewarded, interval.id(), true) .await; } if !pending_delegators.is_empty() { - self.reward_missed_delegators(&pending_delegators, current_rewarding_nonce + 1) + self.reward_missed_delegators(&pending_delegators, interval.id()) .await; } @@ -565,7 +548,7 @@ impl Rewarder { } let report = RewardingReport { - epoch_rewarding_id: epoch_rewarding_database_id, + interval_rewarding_id: interval_rewarding_database_id, eligible_mixnodes: total_eligible as i64, possibly_unrewarded_mixnodes: failure_data .mixnodes @@ -579,9 +562,7 @@ impl Rewarder { .unwrap_or_default(), }; - self.nymd_client - .finish_mixnode_rewarding(current_rewarding_nonce + 1) - .await?; + self.nymd_client.advance_current_interval().await?; if failure_data.mixnodes.is_none() { Ok((report, None)) @@ -596,13 +577,13 @@ impl Rewarder { /// /// # Arguments /// - /// * `failure_data`: information regarding nodes that might have not received reward this epoch. + /// * `failure_data`: information regarding nodes that might have not received reward this interval. /// - /// * `epoch_rewarding_id`: id of the current epoch rewarding. + /// * `interval_rewarding_id`: id of the current interval rewarding. async fn save_failure_information( &self, failure_data: FailureData, - epoch_rewarding_id: i64, + interval_rewarding_id: i64, ) -> Result<(), RewardingError> { if let Some(failed_mixnode_chunks) = failure_data.mixnodes { for failed_chunk in failed_mixnode_chunks.into_iter() { @@ -610,7 +591,7 @@ impl Rewarder { let chunk_id = self .storage .insert_failed_mixnode_reward_chunk(FailedMixnodeRewardChunk { - epoch_rewarding_id, + interval_rewarding_id, error_message: failed_chunk.error_message, }) .await?; @@ -631,19 +612,19 @@ impl Rewarder { Ok(()) } - /// Determines whether this validator has already distributed rewards for the specified epoch + /// Determines whether this validator has already distributed rewards for the specified interval /// so that it wouldn't accidentally attempt to do it again. /// /// # Arguments /// - /// * `epoch`: epoch to check - async fn check_if_rewarding_happened_at_epoch( + /// * `interval`: interval to check + async fn check_if_rewarding_happened_at_interval( &self, - epoch: Epoch, + interval: Interval, ) -> Result { if let Some(entry) = self .storage - .get_epoch_rewarding_entry(epoch.start_unix_timestamp()) + .get_interval_rewarding_entry(interval.start_unix_timestamp()) .await? { // log error if the attempt wasn't finished. This error implies the process has crashed @@ -651,7 +632,7 @@ impl Rewarder { if !entry.finished { error!( "It seems that we haven't successfully finished distributing rewards at {}", - epoch + interval ) } @@ -661,109 +642,57 @@ impl Rewarder { } } - /// Determines whether the specified epoch is eligible for rewards, i.e. it was not rewarded + /// Determines whether the specified interval is eligible for rewards, i.e. it was not rewarded /// before and we have enough network monitor test data to distribute the rewards based on them. /// /// # Arguments /// - /// * `epoch`: epoch to check - async fn check_epoch_eligibility(&self, epoch: Epoch) -> Result { - if self.check_if_rewarding_happened_at_epoch(epoch).await? - || !self.check_for_monitor_data(epoch).await? + /// * `interval`: interval to check + async fn check_interval_eligibility(&self, interval: Interval) -> Result { + if self + .check_if_rewarding_happened_at_interval(interval) + .await? + || !self.check_for_monitor_data(interval).await? { Ok(false) } else { - // we haven't sent rewards during the epoch and we have enough monitor test data + // we haven't sent rewards during the interval and we have enough monitor test data Ok(true) } } - /// Determines the next epoch during which the rewards should get distributed. - /// - /// # Arguments - /// - /// * `now`: current datetime - async fn next_rewarding_epoch(&self, now: OffsetDateTime) -> Result { - // edge case handling for when we decide to change first epoch to be at some time in the future - // (i.e. epoch length transition) - // we don't have to perform checks here as it's impossible to distribute rewards for epochs - // in the future - if self.first_epoch.start() > now { - return Ok(self.first_epoch); - } - - let current_epoch = self.first_epoch.current(now); - // check previous epoch in case we had a tiny hiccup - // example: - // epochs start at 12:00pm and last for 24h (ignore variance) - // validator-api crashed at 11:59am before distributing rewards - // and restarted at 12:01 - it has all the data required to distribute the rewards - // for the previous epoch. - let previous_epoch = current_epoch.previous_epoch(); - if self.check_epoch_eligibility(previous_epoch).await? { - return Ok(previous_epoch); - } - - // check if rewards weren't already given out for the current epoch - // (it can happen for negative variance if the process crashed) - // note that if the epoch ends at say 12:00 and it's 11:59 and we just started, - // we might end up skipping this epoch regardless - if !self - .check_if_rewarding_happened_at_epoch(current_epoch) - .await? - { - return Ok(current_epoch); - } - - // if we have given rewards for the previous and the current epoch, - // wait until the next one - Ok(current_epoch.next_epoch()) - } - - /// Given datetime of the rewarding epoch datetime, determine duration until it ends. - /// - /// # Arguments - /// - /// * `rewarding_epoch`: the rewarding epoch - fn determine_delay_until_next_rewarding(&self, rewarding_epoch: Epoch) -> Option { - let now = OffsetDateTime::now_utc(); - if now > rewarding_epoch.end() { - return None; - } - - // we have a positive duration so we can't fail the conversion - let until_epoch_end: Duration = (rewarding_epoch.end() - now).try_into().unwrap(); - - Some(until_epoch_end) - } - /// Distribute rewards to all eligible mixnodes and gateways on the network. /// /// # Arguments /// - /// * `epoch`: current rewarding epoch - async fn perform_rewarding(&self, epoch: Epoch) -> Result<(), RewardingError> { + /// * `interval`: current rewarding interval + async fn perform_rewarding(&self, interval: Interval) -> Result<(), RewardingError> { info!( - "Starting mixnode and gateway rewarding for epoch {} ...", - epoch + "Starting mixnode and gateway rewarding for interval {} ...", + interval ); // insert information about beginning the procedure (so that if we crash during it, // we wouldn't attempt to possibly double reward operators) - let epoch_rewarding_id = self + let interval_rewarding_id = self .storage - .insert_started_epoch_rewarding(epoch.start_unix_timestamp()) + .insert_started_interval_rewarding( + interval.start_unix_timestamp(), + interval.end_unix_timestamp(), + ) .await?; - let (report, failure_data) = self.distribute_rewards(epoch_rewarding_id, epoch).await?; + let (report, failure_data) = self + .distribute_rewards(interval_rewarding_id, interval) + .await?; self.storage - .finish_rewarding_epoch_and_insert_report(report) + .finish_rewarding_interval_and_insert_report(report) .await?; if let Some(failure_data) = failure_data { if let Err(err) = self - .save_failure_information(failure_data, epoch_rewarding_id) + .save_failure_information(failure_data, interval_rewarding_id) .await { error!("failed to save information about rewarding failures!"); @@ -772,107 +701,131 @@ impl Rewarder { } } - // since we have already performed rewards, purge everything older than the end of this epoch + // since we have already performed rewards, purge everything older than the end of this interval // (+one day of buffer) as we're never going to need it again (famous last words...) - // note that usually end of epoch is equal to the current time - let cutoff = (epoch.end() - ONE_DAY).unix_timestamp(); + // note that usually end of interval is equal to the current time + let cutoff = (interval.end() - ONE_DAY).unix_timestamp(); self.storage.purge_old_statuses(cutoff).await?; Ok(()) } /// Checks whether there is enough network monitor test run data to distribute rewards - /// for the specified epoch. + /// for the specified interval. /// /// # Arguments /// - /// * `epoch`: epoch to check - async fn check_for_monitor_data(&self, epoch: Epoch) -> Result { - let since = epoch.start_unix_timestamp(); - let until = epoch.end_unix_timestamp(); + /// * `interval`: interval to check + async fn check_for_monitor_data(&self, interval: Interval) -> Result { + let since = interval.start_unix_timestamp(); + let until = interval.end_unix_timestamp(); let monitor_runs = self.storage.get_monitor_runs_count(since, until).await?; - // check if we have more than threshold percentage of monitor runs for the epoch - let available = monitor_runs as f32 * 100.0 / self.expected_epoch_monitor_runs as f32; - Ok(available >= self.minimum_epoch_monitor_threshold as f32) + // check if we have more than threshold percentage of monitor runs for the interval + let available = monitor_runs as f32 * 100.0 / self.expected_interval_monitor_runs as f32; + Ok(available >= self.minimum_interval_monitor_threshold as f32) } - /// Waits until the next epoch starts - /// - /// # Arguments - /// - /// * `current_epoch`: current epoch that we want to wait out - async fn wait_until_next_epoch(&self, current_epoch: Epoch) { - let now = OffsetDateTime::now_utc(); - let until_end = current_epoch.end() - now; + async fn sync_up_rewarding_intervals(&self) -> Result<(), RewardingError> { + let mut last_stored_interval = self.nymd_client.get_current_interval().await?; - // otherwise it means the epoch is already over and the next one has begun - if until_end.is_positive() { - // we know for sure that the duration here is positive so conversion can't fail - sleep(until_end.try_into().unwrap()).await; + let block_now: OffsetDateTime = self.nymd_client.current_block_timestamp().await?.into(); + let actual_current_interval = match last_stored_interval.current(block_now) { + None => return Ok(()), + Some(interval) => interval, + }; + + // we're waiting for the first interval to start... (same is true if the value was 'None') + if actual_current_interval.start() < last_stored_interval.start() { + return Ok(()); } + + // we're already synced up + if actual_current_interval == last_stored_interval { + return Ok(()); + } + + // actual_current_interval > last_stored_interval + loop { + // if we can perform rewarding, do it, otherwise just go straight into the next interval + if self + .check_interval_eligibility(last_stored_interval) + .await? + { + self.perform_rewarding(last_stored_interval).await?; + } else { + self.nymd_client.advance_current_interval().await?; + } + + last_stored_interval = self.nymd_client.get_current_interval().await?; + + // compare by start times in case the id didn't match (TODO: is it even possible?) + if last_stored_interval.start() == actual_current_interval.start() { + break; + } + } + + Ok(()) + } + + // pub(crate) async fn processing_loop_iteration(&self) -> Result, RewardingError> { + pub(crate) async fn processing_loop_iteration(&self) -> Result<(), RewardingError> { + let last_stored_interval = self.nymd_client.get_current_interval().await?; + let block_now: OffsetDateTime = self.nymd_client.current_block_timestamp().await?.into(); + + let actual_current_interval = match last_stored_interval.current(block_now) { + None => return Ok(()), + Some(interval) => interval, + }; + + // the [stored] interval has finished - we should distribute rewards now + if last_stored_interval.start() < actual_current_interval.start() { + // it's time to distribute rewards, however, first let's see if we have enough data to go through with it + // (consider the case of rewards being distributed every 24h at 12:00pm and validator-api + // starting for the very first time at 11:00am. It's not going to have enough data for + // rewards for the *current* interval, but we couldn't have known that at startup) + if self + .check_interval_eligibility(last_stored_interval) + .await? + { + self.perform_rewarding(last_stored_interval).await?; + } else { + warn!("We do not have sufficient monitor data to perform rewarding in this interval ({}). We're advancing it forward...", last_stored_interval); + self.nymd_client.advance_current_interval().await?; + } + } else { + info!( + "rewards will be distributed around {}. Approximately {:?} remaining", + last_stored_interval.end(), + last_stored_interval.until_end(block_now) + ); + } + + sleep(Duration::from_secs(15 * 60)).await; + + Ok(()) } pub(crate) async fn run(&self) { // whatever happens, we shouldn't do anything until the cache is initialised self.validator_cache.wait_for_initial_values().await; + if let Err(err) = self.sync_up_rewarding_intervals().await { + error!( + "Failed to sync up intervals with the contract state - {}", + err + ); + process::exit(1); + } + + // at this point the current block time < end of current[or first] interval, so to do anything, + // we have to wait for the interval to finish loop { - // Just a reference for anyone wanting to modify the code to use blockchain timestamps. - // This method is now available: - // let current_block_timestamp = self.nymd_client.current_block_timestamp().await.unwrap(); - // and if you look at the source of that, you can easily use block height instead if preferred. - - let now = OffsetDateTime::now_utc(); - // if we haven't rewarded anyone for the previous epoch, get the start of the previous epoch - // otherwise get the start of the current epoch - // (remember, we will be rewarding at the END of the selected epoch) - let next_rewarding_epoch = match self.next_rewarding_epoch(now).await { - Ok(next_rewarding_epoch) => next_rewarding_epoch, - Err(err) => { - // I'm not entirely sure whether this is recoverable, because failure implies database errors - error!("We failed to determine time until next reward cycle ({}). Going to wait for the epoch length until next attempt", err); - sleep(self.first_epoch.length()).await; - continue; - } - }; - - // wait's until the start of the *next* epoch, e.g. end of the current chosen epoch - // (it could be none, for example if we are distributing overdue rewards for the previous epoch) - if let Some(remaining_time) = - self.determine_delay_until_next_rewarding(next_rewarding_epoch) - { - info!("Next rewarding epoch is {}", next_rewarding_epoch); - info!( - "Rewards distribution will happen at {}. ({:?} left)", - now + remaining_time, - remaining_time - ); - sleep(remaining_time).await; - } else { - info!( - "Starting reward distribution for epoch {} immediately!", - next_rewarding_epoch - ); - } - - // it's time to distribute rewards, however, first let's see if we have enough data to go through with it - // (consider the case of rewards being distributed every 24h at 12:00pm and validator-api - // starting for the very first time at 11:00am. It's not going to have enough data for - // rewards for the *current* epoch, but we couldn't have known that at startup) - match self.check_for_monitor_data(next_rewarding_epoch).await { - Err(_) | Ok(false) => { - warn!("We do not have sufficient monitor data to perform rewarding in this epoch ({})", next_rewarding_epoch); - self.wait_until_next_epoch(next_rewarding_epoch).await; - continue; - } - _ => (), - } - - if let Err(err) = self.perform_rewarding(next_rewarding_epoch).await { - // TODO: should we just terminate the process here instead? - error!("Failed to distribute rewards! - {}", err) + if let Err(err) = self.processing_loop_iteration().await { + error!("failed to finish rewarding loop iteration - {}", err); + // should we go into backoff here or just exit the process? + sleep(Duration::from_secs(15 * 60)).await; } } } diff --git a/validator-api/src/storage/manager.rs b/validator-api/src/storage/manager.rs index d746c846bd..6220d8354f 100644 --- a/validator-api/src/storage/manager.rs +++ b/validator-api/src/storage/manager.rs @@ -5,10 +5,9 @@ use crate::network_monitor::monitor::summary_producer::NodeResult; use crate::node_status_api::models::{HistoricalUptime, Uptime}; use crate::node_status_api::utils::ActiveNodeStatuses; use crate::storage::models::{ - ActiveNode, EpochRewarding, FailedMixnodeRewardChunk, NodeStatus, PossiblyUnrewardedMixnode, + ActiveNode, FailedMixnodeRewardChunk, IntervalRewarding, NodeStatus, PossiblyUnrewardedMixnode, RewardingReport, TestingRoute, }; -use crate::storage::UnixTimestamp; use std::convert::TryFrom; #[derive(Clone)] @@ -102,7 +101,7 @@ impl StorageManager { pub(super) async fn get_mixnode_statuses_since( &self, identity: &str, - timestamp: UnixTimestamp, + timestamp: i64, ) -> Result, sqlx::Error> { sqlx::query_as!( NodeStatus, @@ -130,7 +129,7 @@ impl StorageManager { pub(super) async fn get_gateway_statuses_since( &self, identity: &str, - timestamp: UnixTimestamp, + timestamp: i64, ) -> Result, sqlx::Error> { sqlx::query_as!( NodeStatus, @@ -234,8 +233,8 @@ impl StorageManager { pub(super) async fn get_mixnode_statuses_by_id( &self, id: i64, - since: UnixTimestamp, - until: UnixTimestamp, + since: i64, + until: i64, ) -> Result, sqlx::Error> { sqlx::query_as!( NodeStatus, @@ -262,8 +261,8 @@ impl StorageManager { pub(super) async fn get_gateway_statuses_by_id( &self, id: i64, - since: UnixTimestamp, - until: UnixTimestamp, + since: i64, + until: i64, ) -> Result, sqlx::Error> { sqlx::query_as!( NodeStatus, @@ -288,7 +287,7 @@ impl StorageManager { /// * `mixnode_results`: reliability results of each node that got tested. pub(super) async fn submit_mixnode_statuses( &self, - timestamp: UnixTimestamp, + timestamp: i64, mixnode_results: Vec, ) -> Result<(), sqlx::Error> { // insert it all in a transaction to make sure all nodes are updated at the same time @@ -333,7 +332,7 @@ impl StorageManager { /// * `gateway_results`: reliability results of each node that got tested. pub(super) async fn submit_gateway_statuses( &self, - timestamp: UnixTimestamp, + timestamp: i64, gateway_results: Vec, ) -> Result<(), sqlx::Error> { // insert it all in a transaction to make sure all nodes are updated at the same time @@ -411,7 +410,7 @@ impl StorageManager { pub(super) async fn get_mixnode_testing_route_presence_count_since( &self, mixnode_id: i64, - since: UnixTimestamp, + since: i64, ) -> Result { let count = sqlx::query!( r#" @@ -450,7 +449,7 @@ impl StorageManager { pub(super) async fn get_gateway_testing_route_presence_count_since( &self, gateway_id: i64, - since: UnixTimestamp, + since: i64, ) -> Result { let count = sqlx::query!( r#" @@ -542,10 +541,7 @@ impl StorageManager { /// # Arguments /// /// * `timestamp`: unix timestamp at which the monitor test run has occurred - pub(super) async fn insert_monitor_run( - &self, - timestamp: UnixTimestamp, - ) -> Result { + pub(super) async fn insert_monitor_run(&self, timestamp: i64) -> Result { let res = sqlx::query!("INSERT INTO monitor_run(timestamp) VALUES (?)", timestamp) .execute(&self.connection_pool) .await?; @@ -560,8 +556,8 @@ impl StorageManager { /// * `until`: unix timestamp indicating the upper bound interval of the selection. pub(super) async fn get_monitor_runs_count( &self, - since: UnixTimestamp, - until: UnixTimestamp, + since: i64, + until: i64, ) -> Result { let count = sqlx::query!( "SELECT COUNT(*) as count FROM monitor_run WHERE timestamp > ? AND timestamp < ?", @@ -582,7 +578,7 @@ impl StorageManager { /// * `until`: timestamp specifying the purge cutoff. pub(super) async fn purge_old_mixnode_statuses( &self, - timestamp: UnixTimestamp, + timestamp: i64, ) -> Result<(), sqlx::Error> { sqlx::query!("DELETE FROM mixnode_status WHERE timestamp < ?", timestamp) .execute(&self.connection_pool) @@ -598,7 +594,7 @@ impl StorageManager { /// * `until`: timestamp specifying the purge cutoff. pub(super) async fn purge_old_gateway_statuses( &self, - timestamp: UnixTimestamp, + timestamp: i64, ) -> Result<(), sqlx::Error> { sqlx::query!("DELETE FROM gateway_status WHERE timestamp < ?", timestamp) .execute(&self.connection_pool) @@ -615,8 +611,8 @@ impl StorageManager { /// * `until`: indicates the upper bound timestamp for deciding whether given mixnode is active pub(super) async fn get_all_active_mixnodes_in_interval( &self, - since: UnixTimestamp, - until: UnixTimestamp, + since: i64, + until: i64, ) -> Result, sqlx::Error> { // find mixnode details of all nodes that have had at least 1 status information since the provided // timestamp @@ -649,8 +645,8 @@ impl StorageManager { /// * `until`: indicates the upper bound timestamp for deciding whether given gateway is active pub(super) async fn get_all_active_gateways_in_interval( &self, - since: UnixTimestamp, - until: UnixTimestamp, + since: i64, + until: i64, ) -> Result, sqlx::Error> { sqlx::query_as!( ActiveNode, @@ -670,22 +666,25 @@ impl StorageManager { .await } - /// Inserts information about starting new epoch rewarding into the database. + /// Inserts information about starting new interval rewarding into the database. /// Returns id of the newly created entry. /// /// # Arguments /// - /// * `epoch_timestamp`: Unix timestamp of this rewarding epoch. - pub(super) async fn insert_new_epoch_rewarding( + /// * `interval_start_timestamp`: Unix timestamp of start of this rewarding interval. + /// * `interval_end_timestamp`: Unix timestamp of end of this rewarding interval. + pub(super) async fn insert_new_interval_rewarding( &self, - epoch_timestamp: UnixTimestamp, + interval_start_timestamp: i64, + interval_end_timestamp: i64, ) -> Result { let res = sqlx::query!( r#" - INSERT INTO epoch_rewarding (epoch_timestamp, finished) - VALUES (?, 0) + INSERT INTO interval_rewarding (interval_start_timestamp, interval_end_timestamp, finished) + VALUES (?, ?, 0) "#, - epoch_timestamp + interval_start_timestamp, + interval_end_timestamp, ) .execute(&self.connection_pool) .await?; @@ -693,15 +692,18 @@ impl StorageManager { Ok(res.last_insert_rowid()) } - /// Sets the `finished` field on the epoch rewarding to true. + /// Sets the `finished` field on the interval rewarding to true. /// /// # Arguments /// /// * `id`: id of the entry we want to update. - pub(super) async fn update_finished_epoch_rewarding(&self, id: i64) -> Result<(), sqlx::Error> { + pub(super) async fn update_finished_interval_rewarding( + &self, + id: i64, + ) -> Result<(), sqlx::Error> { sqlx::query!( r#" - UPDATE epoch_rewarding + UPDATE interval_rewarding SET finished = 1 WHERE id = ? "#, @@ -713,17 +715,17 @@ impl StorageManager { Ok(()) } - // /// Tries to obtain the most recent epoch rewarding entry currently stored. + // /// Tries to obtain the most recent interval rewarding entry currently stored. // /// // /// Returns None if no data exists. - // pub(super) async fn get_most_recent_epoch_rewarding_entry( + // pub(super) async fn get_most_recent_interval_rewarding_entry( // &self, - // ) -> Result, sqlx::Error> { + // ) -> Result, sqlx::Error> { // sqlx::query_as!( - // EpochRewarding, + // IntervalRewarding, // r#" - // SELECT * FROM epoch_rewarding - // ORDER BY epoch_timestamp DESC + // SELECT * FROM interval_rewarding + // ORDER BY interval_timestamp DESC // LIMIT 1 // "#, // ) @@ -731,24 +733,24 @@ impl StorageManager { // .await // } - /// Tries to obtain the epoch rewarding entry that has the provided timestamp. + /// Tries to obtain the interval rewarding entry that has the provided timestamp. /// /// Returns None if no data exists. /// /// # Arguments /// - /// * `epoch_timestamp`: Unix timestamp of this rewarding epoch. - pub(super) async fn get_epoch_rewarding_entry( + /// * `interval_start_timestamp`: Unix timestamp of the start of this rewarding interval. + pub(super) async fn get_interval_rewarding_entry( &self, - epoch_timestamp: UnixTimestamp, - ) -> Result, sqlx::Error> { + interval_start_timestamp: i64, + ) -> Result, sqlx::Error> { sqlx::query_as!( - EpochRewarding, + IntervalRewarding, r#" - SELECT * FROM epoch_rewarding - WHERE epoch_timestamp = ? + SELECT * FROM interval_rewarding + WHERE interval_start_timestamp = ? "#, - epoch_timestamp + interval_start_timestamp ) .fetch_optional(&self.connection_pool) .await @@ -766,10 +768,10 @@ impl StorageManager { sqlx::query!( r#" INSERT INTO rewarding_report - (epoch_rewarding_id, eligible_mixnodes, possibly_unrewarded_mixnodes) + (interval_rewarding_id, eligible_mixnodes, possibly_unrewarded_mixnodes) VALUES (?, ?, ?); "#, - report.epoch_rewarding_id, + report.interval_rewarding_id, report.eligible_mixnodes, report.possibly_unrewarded_mixnodes, ) @@ -793,13 +795,13 @@ impl StorageManager { INSERT INTO failed_mixnode_reward_chunk (error_message, reward_summary_id) VALUES (?, ?) "#, failed_chunk.error_message, - failed_chunk.epoch_rewarding_id, + failed_chunk.interval_rewarding_id, ).execute(&self.connection_pool).await?; Ok(res.last_insert_rowid()) } - /// Inserts information into the database about a mixnode that might have been unfairly unrewarded this epoch. + /// Inserts information into the database about a mixnode that might have been unfairly unrewarded this interval. /// /// # Arguments /// @@ -827,8 +829,8 @@ impl StorageManager { /// * `until`: unix timestamp indicating the upper bound interval of the selection. pub(super) async fn get_all_active_mixnodes_statuses_in_interval( &self, - since: UnixTimestamp, - until: UnixTimestamp, + since: i64, + until: i64, ) -> Result, sqlx::Error> { let active_nodes = self .get_all_active_mixnodes_in_interval(since, until) @@ -860,8 +862,8 @@ impl StorageManager { /// * `until`: unix timestamp indicating the upper bound interval of the selection. pub(super) async fn get_all_active_gateways_statuses_in_interval( &self, - since: UnixTimestamp, - until: UnixTimestamp, + since: i64, + until: i64, ) -> Result, sqlx::Error> { let active_nodes = self .get_all_active_gateways_in_interval(since, until) diff --git a/validator-api/src/storage/mod.rs b/validator-api/src/storage/mod.rs index 449c205e3d..e538a8c071 100644 --- a/validator-api/src/storage/mod.rs +++ b/validator-api/src/storage/mod.rs @@ -10,7 +10,7 @@ use crate::node_status_api::models::{ use crate::node_status_api::{ONE_DAY, ONE_HOUR}; use crate::storage::manager::StorageManager; use crate::storage::models::{ - EpochRewarding, FailedMixnodeRewardChunk, NodeStatus, PossiblyUnrewardedMixnode, + FailedMixnodeRewardChunk, IntervalRewarding, NodeStatus, PossiblyUnrewardedMixnode, RewardingReport, TestingRoute, }; use rocket::fairing::{self, AdHoc}; @@ -22,9 +22,6 @@ use time::OffsetDateTime; pub(crate) mod manager; pub(crate) mod models; -// A type alias to be more explicit about type of timestamp used. -pub(crate) type UnixTimestamp = i64; - // note that clone here is fine as upon cloning the same underlying pool will be used #[derive(Clone)] pub(crate) struct ValidatorApiStorage { @@ -81,7 +78,7 @@ impl ValidatorApiStorage { async fn get_mixnode_statuses( &self, identity: &str, - since: UnixTimestamp, + since: i64, ) -> Result, ValidatorApiStorageError> { let statuses = self .manager @@ -102,7 +99,7 @@ impl ValidatorApiStorage { async fn get_gateway_statuses( &self, identity: &str, - since: UnixTimestamp, + since: i64, ) -> Result, ValidatorApiStorageError> { let statuses = self .manager @@ -275,8 +272,8 @@ impl ValidatorApiStorage { pub(crate) async fn get_average_mixnode_uptime_in_interval( &self, identity: &str, - start: UnixTimestamp, - end: UnixTimestamp, + start: i64, + end: i64, ) -> Result { let mixnode_database_id = match self .manager @@ -319,14 +316,14 @@ impl ValidatorApiStorage { /// * `since`: unix timestamp indicating the lower bound interval of the selection. /// * `end`: unix timestamp indicating the upper bound interval of the selection. // NOTE: even though the arguments would suggest this function is generic in regards to - // epoch length, the constructed reports still assume the epochs are 24h in length. + // interval length, the constructed reports still assume the intervals are 24h in length. pub(crate) async fn get_all_active_mixnode_reports_in_interval( &self, - start: UnixTimestamp, - end: UnixTimestamp, + start: i64, + end: i64, ) -> Result, ValidatorApiStorageError> { if (end - start) as u64 != ONE_DAY.as_secs() { - warn!("Our current epoch length breaks the 24h length assumption") + warn!("Our current interval length breaks the 24h length assumption") } let hour_ago = end - ONE_HOUR.as_secs() as i64; @@ -363,14 +360,14 @@ impl ValidatorApiStorage { /// * `since`: unix timestamp indicating the lower bound interval of the selection. /// * `end`: unix timestamp indicating the upper bound interval of the selection. // NOTE: even though the arguments would suggest this function is generic in regards to - // epoch length, the constructed reports still assume the epochs are 24h in length. + // interval length, the constructed reports still assume the intervals are 24h in length. pub(crate) async fn get_all_active_gateway_reports_in_interval( &self, - start: UnixTimestamp, - end: UnixTimestamp, + start: i64, + end: i64, ) -> Result, ValidatorApiStorageError> { if (end - start) as u64 != ONE_DAY.as_secs() { - warn!("Our current epoch length breaks the 24h length assumption") + warn!("Our current interval length breaks the 24h length assumption") } let hour_ago = end - ONE_HOUR.as_secs() as i64; @@ -465,7 +462,7 @@ impl ValidatorApiStorage { pub(crate) async fn get_core_mixnode_status_count( &self, identity: &str, - since: Option, + since: Option, ) -> Result { let node_id = self .manager @@ -497,7 +494,7 @@ impl ValidatorApiStorage { pub(crate) async fn get_core_gateway_status_count( &self, identity: &str, - since: Option, + since: Option, ) -> Result { let node_id = self .manager @@ -567,8 +564,8 @@ impl ValidatorApiStorage { /// * `until`: unix timestamp indicating the upper bound interval of the selection. pub(crate) async fn get_monitor_runs_count( &self, - since: UnixTimestamp, - until: UnixTimestamp, + since: i64, + until: i64, ) -> Result { let run_count = self .manager @@ -668,7 +665,7 @@ impl ValidatorApiStorage { /// * `until`: timestamp specifying the purge cutoff. pub(crate) async fn purge_old_statuses( &self, - until: UnixTimestamp, + until: i64, ) -> Result<(), ValidatorApiStorageError> { self.manager .purge_old_mixnode_statuses(until) @@ -684,63 +681,65 @@ impl ValidatorApiStorage { // TODO: Should all of the below really return a "ValidatorApiStorageError" Errors? //////////////////////////////////////////////////////////////////////// - /// Inserts information about starting new epoch rewarding into the database. + /// Inserts information about starting new interval rewarding into the database. /// Returns id of the newly created entry. /// /// # Arguments /// - /// * `epoch_timestamp`: Unix timestamp of this rewarding epoch. - pub(crate) async fn insert_started_epoch_rewarding( + /// * `interval_start_timestamp`: Unix timestamp of start of this rewarding interval. + /// * `interval_end_timestamp`: Unix timestamp of end of this rewarding interval. + pub(crate) async fn insert_started_interval_rewarding( &self, - epoch_timestamp: UnixTimestamp, + interval_start_timestamp: i64, + interval_end_timestamp: i64, ) -> Result { self.manager - .insert_new_epoch_rewarding(epoch_timestamp) + .insert_new_interval_rewarding(interval_start_timestamp, interval_end_timestamp) .await .map_err(|_| ValidatorApiStorageError::InternalDatabaseError) } - // /// Tries to obtain the most recent epoch rewarding entry currently stored. + // /// Tries to obtain the most recent interval rewarding entry currently stored. // /// // /// Returns None if no data exists. - // pub(crate) async fn get_most_recent_epoch_rewarding_entry( + // pub(crate) async fn get_most_recent_interval_rewarding_entry( // &self, - // ) -> Result, ValidatorApiStorageError> { + // ) -> Result, ValidatorApiStorageError> { // self.manager - // .get_most_recent_epoch_rewarding_entry() + // .get_most_recent_interval_rewarding_entry() // .await // .map_err(|_| ValidatorApiStorageError::InternalDatabaseError) // } - /// Tries to obtain the epoch rewarding entry that has the provided timestamp. + /// Tries to obtain the interval rewarding entry that has the provided timestamp. /// /// Returns None if no data exists. /// /// # Arguments /// - /// * `epoch_timestamp`: Unix timestamp of this rewarding epoch. - pub(super) async fn get_epoch_rewarding_entry( + /// * `interval_timestamp`: Unix timestamp of this rewarding interval. + pub(super) async fn get_interval_rewarding_entry( &self, - epoch_timestamp: UnixTimestamp, - ) -> Result, ValidatorApiStorageError> { + interval_timestamp: i64, + ) -> Result, ValidatorApiStorageError> { self.manager - .get_epoch_rewarding_entry(epoch_timestamp) + .get_interval_rewarding_entry(interval_timestamp) .await .map_err(|_| ValidatorApiStorageError::InternalDatabaseError) } - /// Sets the `finished` field on the epoch rewarding to true and inserts the rewarding report into + /// Sets the `finished` field on the interval rewarding to true and inserts the rewarding report into /// the database. /// /// # Arguments /// /// * `report`: report to insert into the database - pub(crate) async fn finish_rewarding_epoch_and_insert_report( + pub(crate) async fn finish_rewarding_interval_and_insert_report( &self, report: RewardingReport, ) -> Result<(), ValidatorApiStorageError> { self.manager - .update_finished_epoch_rewarding(report.epoch_rewarding_id) + .update_finished_interval_rewarding(report.interval_rewarding_id) .await .map_err(|_| ValidatorApiStorageError::InternalDatabaseError)?; @@ -766,7 +765,7 @@ impl ValidatorApiStorage { .map_err(|_| ValidatorApiStorageError::InternalDatabaseError) } - /// Inserts information into the database about a mixnode that might have been unfairly unrewarded this epoch. + /// Inserts information into the database about a mixnode that might have been unfairly unrewarded this interval. /// /// # Arguments /// diff --git a/validator-api/src/storage/models.rs b/validator-api/src/storage/models.rs index 65891524eb..f6c8875bf1 100644 --- a/validator-api/src/storage/models.rs +++ b/validator-api/src/storage/models.rs @@ -1,11 +1,9 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::storage::UnixTimestamp; - // Internally used struct to catch results from the database to calculate uptimes for given mixnode/gateway pub(crate) struct NodeStatus { - pub(crate) timestamp: UnixTimestamp, + pub(crate) timestamp: i64, pub(crate) reliability: u8, } @@ -24,17 +22,19 @@ pub(crate) struct TestingRoute { pub(crate) monitor_run_id: i64, } -pub(crate) struct EpochRewarding { +pub(crate) struct IntervalRewarding { #[allow(dead_code)] pub(crate) id: i64, #[allow(dead_code)] - pub(crate) epoch_timestamp: i64, + pub(crate) interval_start_timestamp: i64, + #[allow(dead_code)] + pub(crate) interval_end_timestamp: i64, pub(crate) finished: bool, } pub(crate) struct RewardingReport { - // references particular epoch_rewarding - pub(crate) epoch_rewarding_id: i64, + // references particular interval_rewarding + pub(crate) interval_rewarding_id: i64, pub(crate) eligible_mixnodes: i64, @@ -42,8 +42,8 @@ pub(crate) struct RewardingReport { } pub(crate) struct FailedMixnodeRewardChunk { - // references particular epoch_rewarding (there can be multiple chunks in a rewarding epoch) - pub(crate) epoch_rewarding_id: i64, + // references particular interval_rewarding (there can be multiple chunks in a rewarding interval) + pub(crate) interval_rewarding_id: i64, pub(crate) error_message: String, } diff --git a/validator-api/validator-api-requests/src/models.rs b/validator-api/validator-api-requests/src/models.rs index a7e5640837..fb240e61c0 100644 --- a/validator-api/validator-api-requests/src/models.rs +++ b/validator-api/validator-api-requests/src/models.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use serde::{Deserialize, Serialize}; +use std::fmt; #[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)] #[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))] @@ -39,9 +40,9 @@ pub struct RewardEstimationResponse { pub estimated_operator_reward: u64, pub estimated_delegators_reward: u64, - pub current_epoch_start: i64, - pub current_epoch_end: i64, - pub current_epoch_uptime: u8, + pub current_interval_start: i64, + pub current_interval_end: i64, + pub current_interval_uptime: u8, pub as_at: i64, } @@ -51,3 +52,20 @@ pub struct StakeSaturationResponse { pub saturation: f32, pub as_at: i64, } + +#[derive(Clone, Copy, Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))] +pub struct InclusionProbabilityResponse { + pub in_active: f32, + pub in_reserve: f32, +} + +impl fmt::Display for InclusionProbabilityResponse { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "in_active: {:.5}, in_reserve: {:.5}", + self.in_active, self.in_reserve + ) + } +}