Feature/hourly set updates (#1012)
* Rename function/variables mixnodes->set * Stub utility interface * Rewarded set contract interface * Move epoch to common, epoch to contract * Move epoch to the chain * Rewarded set validator-api * [ci skip] Generate TS types * Epoch queries * Moved new code to a new module * Restored cosmwasm dependencies to their beta.3 versions for better compatibility with the rest of the codebase * Rewarded set write reorganisation * Stub for validator api module responsible for rewarded set updates * Reorganised validator api cache * Pending contract changes * Relevant updates to the validator client * Updating rewarded set based on contract state * Advancing/Setting current epoch in the contract * Using blocktime as 'now' at startup * Adjusted validator-api side rewarding code * Contract cleanup + query for epoch rewarded set heights * [ci skip] Generate TS types * Simplified rewarder processing loop and initial sync * [ci skip] Generate TS types * Fixed EXISTING query-related unit tests * Fixed existing unit tests for rewarding-related transactions * Cargo fmt * Removed some dead code * Using cosmwasm 1.0.0-beta3 for compatibility [with cw-storage-plus and rest of codebase] * Missing TryInto import * Additional storage and query related unit tests + a bug fix * Transaction-related unit tests + bug fixes * Required migration code * Update common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs Co-authored-by: Drazen Urch <drazen@urch.eu> * Update common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs Co-authored-by: Drazen Urch <drazen@urch.eu> * Constant renaming * Changed determining previous epoch return type to Option<Epoch> if they would precede the genesis * Exposed the new endpoint to the wallet * Epoch-related unit tests fixes * Recommended #[must_use] on next_epoch method * Renamed all epoch occurences to interval As they refer to the 'rewarding interval' Co-authored-by: Jędrzej Stuczyński <jedrzej.stuczynski@gmail.com> Co-authored-by: jstuczyn <jstuczyn@users.noreply.github.com>
This commit is contained in:
Generated
+8
-8
@@ -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",
|
||||
|
||||
@@ -180,8 +180,8 @@ export default class ValidatorClient implements INymClient {
|
||||
return this.client.getSybilResistancePercent(this.mixnetContract);
|
||||
}
|
||||
|
||||
public async getEpochRewardPercent(): Promise<number> {
|
||||
return this.client.getEpochRewardPercent(this.mixnetContract);
|
||||
public async getIntervalRewardPercent(): Promise<number> {
|
||||
return this.client.getIntervalRewardPercent(this.mixnetContract);
|
||||
}
|
||||
|
||||
public async getAllNymdMixnodes(): Promise<MixNodeBond[]> {
|
||||
|
||||
@@ -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<RewardingIntervalResponse> {
|
||||
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<number> {
|
||||
getIntervalRewardPercent(mixnetContractAddress: string): Promise<number> {
|
||||
return this.client.queryContractSmart(mixnetContractAddress, {
|
||||
get_epoch_reward_percent: {},
|
||||
get_interval_reward_percent: {},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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<MixOwnershipResponse>;
|
||||
ownsGateway(mixnetContractAddress: string, address: string): Promise<GatewayOwnershipResponse>;
|
||||
getStateParams(mixnetContractAddress: string): Promise<ContractStateParams>;
|
||||
getCurrentRewardingInterval(mixnetContractAddress: string): Promise<RewardingIntervalResponse>;
|
||||
|
||||
getAllNetworkDelegationsPaged(
|
||||
mixnetContractAddress: string,
|
||||
@@ -87,7 +85,7 @@ export interface INymdQuery {
|
||||
getLayerDistribution(mixnetContractAddress: string): Promise<LayerDistribution>;
|
||||
getRewardPool(mixnetContractAddress: string): Promise<string>;
|
||||
getCirculatingSupply(mixnetContractAddress: string): Promise<string>;
|
||||
getEpochRewardPercent(mixnetContractAddress: string): Promise<number>;
|
||||
getIntervalRewardPercent(mixnetContractAddress: string): Promise<number>;
|
||||
getSybilResistancePercent(mixnetContractAddress: string): Promise<number>;
|
||||
getRewardingStatus(
|
||||
mixnetContractAddress: string,
|
||||
@@ -138,10 +136,6 @@ export default class QueryClient extends CosmWasmClient implements IQueryClient
|
||||
return this.nymdQuerier.getStateParams(mixnetContractAddress);
|
||||
}
|
||||
|
||||
getCurrentRewardingInterval(mixnetContractAddress: string): Promise<RewardingIntervalResponse> {
|
||||
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<number> {
|
||||
return this.nymdQuerier.getEpochRewardPercent(mixnetContractAddress);
|
||||
getIntervalRewardPercent(mixnetContractAddress: string): Promise<number> {
|
||||
return this.nymdQuerier.getIntervalRewardPercent(mixnetContractAddress);
|
||||
}
|
||||
|
||||
getSybilResistancePercent(mixnetContractAddress: string): Promise<number> {
|
||||
|
||||
@@ -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<RewardingIntervalResponse> {
|
||||
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<number> {
|
||||
return this.nymdQuerier.getEpochRewardPercent(mixnetContractAddress);
|
||||
getIntervalRewardPercent(mixnetContractAddress: string): Promise<number> {
|
||||
return this.nymdQuerier.getIntervalRewardPercent(mixnetContractAddress);
|
||||
}
|
||||
|
||||
getSybilResistancePercent(mixnetContractAddress: string): Promise<number> {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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<u32>,
|
||||
gateway_page_limit: Option<u32>,
|
||||
mixnode_delegations_page_limit: Option<u32>,
|
||||
rewarded_set_page_limit: Option<u32>,
|
||||
}
|
||||
|
||||
#[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<u32>) -> Config {
|
||||
self.rewarded_set_page_limit = limit;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "nymd-client")]
|
||||
@@ -81,6 +92,7 @@ pub struct Client<C> {
|
||||
mixnode_page_limit: Option<u32>,
|
||||
gateway_page_limit: Option<u32>,
|
||||
mixnode_delegations_page_limit: Option<u32>,
|
||||
rewarded_set_page_limit: Option<u32>,
|
||||
|
||||
// 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<SigningNymdClient> {
|
||||
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<QueryNymdClient> {
|
||||
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<C> Client<C> {
|
||||
Ok(self.nymd.get_mixnet_contract_version().await?)
|
||||
}
|
||||
|
||||
pub async fn get_current_rewarding_interval(
|
||||
&self,
|
||||
) -> Result<RewardingIntervalResponse, ValidatorClientError>
|
||||
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<C> Client<C> {
|
||||
Ok(self.nymd.get_reward_pool().await?.u128())
|
||||
}
|
||||
|
||||
pub async fn get_current_interval(&self) -> Result<Interval, ValidatorClientError>
|
||||
where
|
||||
C: CosmWasmClient + Sync,
|
||||
{
|
||||
Ok(self.nymd.get_current_interval().await?)
|
||||
}
|
||||
|
||||
pub async fn get_circulating_supply(&self) -> Result<u128, ValidatorClientError>
|
||||
where
|
||||
C: CosmWasmClient + Sync,
|
||||
@@ -258,14 +270,129 @@ impl<C> Client<C> {
|
||||
Ok(self.nymd.get_sybil_resistance_percent().await?)
|
||||
}
|
||||
|
||||
pub async fn get_epoch_reward_percent(&self) -> Result<u8, ValidatorClientError>
|
||||
pub async fn get_interval_reward_percent(&self) -> Result<u8, ValidatorClientError>
|
||||
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<RewardedSetUpdateDetails, ValidatorClientError>
|
||||
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<Vec<(IdentityKey, RewardedSetNodeStatus)>, 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<Vec<(MixNodeBond, RewardedSetNodeStatus)>, 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::<HashMap<_, _>>();
|
||||
|
||||
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<Vec<MixNodeBond>, 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::<HashSet<_>>();
|
||||
|
||||
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<Vec<MixNodeBond>, 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::<HashSet<_>>();
|
||||
|
||||
Ok(all_mixnodes
|
||||
.into_iter()
|
||||
.filter(|node| active_set_identities.contains(node.identity()))
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub async fn get_all_nymd_mixnodes(&self) -> Result<Vec<MixNodeBond>, ValidatorClientError>
|
||||
where
|
||||
C: CosmWasmClient + Sync,
|
||||
@@ -314,8 +441,8 @@ impl<C> Client<C> {
|
||||
|
||||
pub async fn get_all_nymd_single_mixnode_delegations(
|
||||
&self,
|
||||
identity: mixnet_contract_common::IdentityKey,
|
||||
) -> Result<Vec<mixnet_contract_common::Delegation>, ValidatorClientError>
|
||||
identity: IdentityKey,
|
||||
) -> Result<Vec<Delegation>, ValidatorClientError>
|
||||
where
|
||||
C: CosmWasmClient + Sync,
|
||||
{
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<C> NymdClient<C> {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_current_rewarding_interval(
|
||||
&self,
|
||||
) -> Result<RewardingIntervalResponse, NymdError>
|
||||
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<MixnodeRewardingStatusResponse, NymdError>
|
||||
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<u64, NymdError>
|
||||
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<RewardedSetUpdateDetails, NymdError>
|
||||
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<IdentityKey>,
|
||||
page_limit: Option<u32>,
|
||||
height: Option<u64>,
|
||||
) -> Result<PagedRewardedSetResponse, NymdError>
|
||||
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<LayerDistribution, NymdError>
|
||||
where
|
||||
C: CosmWasmClient + Sync,
|
||||
@@ -328,6 +358,16 @@ impl<C> NymdClient<C> {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_current_interval(&self) -> Result<Interval, NymdError>
|
||||
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<Uint128, NymdError>
|
||||
where
|
||||
C: CosmWasmClient + Sync,
|
||||
@@ -358,11 +398,11 @@ impl<C> NymdClient<C> {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_epoch_reward_percent(&self) -> Result<u8, NymdError>
|
||||
pub async fn get_interval_reward_percent(&self) -> Result<u8, NymdError>
|
||||
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<C> NymdClient<C> {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn begin_mixnode_rewarding(
|
||||
&self,
|
||||
rewarding_interval_nonce: u32,
|
||||
) -> Result<ExecuteResult, NymdError>
|
||||
pub async fn advance_current_interval(&self) -> Result<ExecuteResult, NymdError>
|
||||
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<IdentityKey>,
|
||||
expected_active_set_size: u32,
|
||||
) -> Result<ExecuteResult, NymdError>
|
||||
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<C> NymdClient<C> {
|
||||
self.mixnet_contract_address()?,
|
||||
&req,
|
||||
fee,
|
||||
"Finishing mixnode rewarding procedure",
|
||||
"Writing rewarded set",
|
||||
Vec::new(),
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -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<InclusionProbabilityResponse, ValidatorAPIError> {
|
||||
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,
|
||||
|
||||
@@ -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" }
|
||||
|
||||
|
||||
@@ -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<Addr>,
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,325 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// 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<Self> {
|
||||
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<Self> {
|
||||
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<Self> {
|
||||
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<Duration> {
|
||||
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()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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<u128> {
|
||||
pub fn total_bond(&self) -> Option<u128> {
|
||||
if self.pledge_amount.denom != self.total_delegation.denom {
|
||||
None
|
||||
} else {
|
||||
|
||||
@@ -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<IdentityKey>,
|
||||
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<u64>,
|
||||
start_after: Option<IdentityKey>,
|
||||
limit: Option<u32>,
|
||||
},
|
||||
GetRewardedSetHeightsForInterval {
|
||||
interval_id: u32,
|
||||
},
|
||||
GetRewardedSetUpdateDetails {},
|
||||
GetCurrentRewardedSetHeight {},
|
||||
GetCurrentInterval {},
|
||||
GetRewardedSetRefreshBlocks {},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
|
||||
@@ -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<IdentityKey>,
|
||||
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<u64>,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Generated
+76
-73
@@ -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"
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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<Response, ContractError> {
|
||||
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<QueryResponse, ContractError> {
|
||||
pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result<QueryResponse, ContractError> {
|
||||
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<QueryResponse, Cont
|
||||
}
|
||||
QueryMsg::OwnsGateway { address } => 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<QueryResponse, Cont
|
||||
} => 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<Response, ContractError> {
|
||||
pub fn migrate(deps: DepsMut, env: Env, _msg: MigrateMsg) -> Result<Response, ContractError> {
|
||||
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<OldContractState> = 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())
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub mod queries;
|
||||
pub mod storage;
|
||||
pub mod transactions;
|
||||
@@ -0,0 +1,425 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// 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<Interval, ContractError> {
|
||||
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<IntervalRewardedSetHeightsResponse, ContractError> {
|
||||
// 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::<StdResult<Vec<_>>>()?;
|
||||
|
||||
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<u64, ContractError> {
|
||||
Ok(storage::CURRENT_REWARDED_SET_HEIGHT.load(storage)?)
|
||||
}
|
||||
|
||||
fn query_rewarded_set_at_height(
|
||||
storage: &dyn Storage,
|
||||
height: u64,
|
||||
start_after: Option<IdentityKey>,
|
||||
limit: u32,
|
||||
) -> Result<Vec<(IdentityKey, RewardedSetNodeStatus)>, 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::<StdResult<_>>()?;
|
||||
Ok(rewarded_set)
|
||||
}
|
||||
|
||||
pub fn query_rewarded_set(
|
||||
storage: &dyn Storage,
|
||||
height: Option<u64>,
|
||||
start_after: Option<IdentityKey>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<PagedRewardedSetResponse, ContractError> {
|
||||
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<RewardedSetUpdateDetails, ContractError> {
|
||||
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<IdentityKey> {
|
||||
let identities = (0..rewarded_set)
|
||||
.map(|i| format!("identity{:04}", i))
|
||||
.collect::<Vec<_>>();
|
||||
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::<Vec<_>>();
|
||||
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::<Vec<_>>();
|
||||
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::<Vec<_>>();
|
||||
|
||||
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::<Vec<_>>();
|
||||
|
||||
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::<Vec<_>>();
|
||||
|
||||
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::<Vec<_>>();
|
||||
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::<Vec<_>>();
|
||||
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::<Vec<_>>();
|
||||
|
||||
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::<Vec<_>>(),
|
||||
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::<Vec<_>>(),
|
||||
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::<Vec<_>>(),
|
||||
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::<Vec<_>>(),
|
||||
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()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// 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<Interval> = Item::new("cep");
|
||||
pub(crate) const CURRENT_REWARDED_SET_HEIGHT: Item<BlockHeight> = 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<IdentityKey>,
|
||||
) -> 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()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,308 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// 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<IdentityKey>,
|
||||
active_set_size: u32,
|
||||
) -> Result<Response, ContractError> {
|
||||
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<Response, ContractError> {
|
||||
// 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::<Vec<_>>();
|
||||
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)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ pub mod contract;
|
||||
mod delegations;
|
||||
mod error;
|
||||
mod gateways;
|
||||
mod interval;
|
||||
mod mixnet_contract_settings;
|
||||
mod mixnodes;
|
||||
mod rewards;
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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<ContractStateParams> {
|
||||
storage::CONTRACT_STATE
|
||||
@@ -13,16 +11,6 @@ pub(crate) fn query_contract_settings_params(deps: Deps) -> StdResult<ContractSt
|
||||
.map(|settings| settings.params)
|
||||
}
|
||||
|
||||
pub(crate) fn query_rewarding_interval(deps: Deps) -> StdResult<RewardingIntervalResponse> {
|
||||
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
|
||||
|
||||
@@ -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<Addr>,
|
||||
@@ -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),
|
||||
)?;
|
||||
}
|
||||
|
||||
@@ -17,10 +17,9 @@ pub(crate) fn query_circulating_supply(deps: Deps) -> StdResult<Uint128> {
|
||||
pub(crate) fn query_rewarding_status(
|
||||
deps: Deps,
|
||||
mix_identity: IdentityKey,
|
||||
rewarding_interval_nonce: u32,
|
||||
interval_id: u32,
|
||||
) -> StdResult<MixnodeRewardingStatusResponse> {
|
||||
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(..))
|
||||
|
||||
@@ -11,11 +11,9 @@ pub(crate) const REWARD_POOL: Item<Uint128> = 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(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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<Coin> {
|
||||
// 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,
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+2
@@ -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",
|
||||
]
|
||||
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<RwLock<State>>>,
|
||||
) -> Result<InclusionProbabilityResponse, BackendError> {
|
||||
Ok(
|
||||
api_client!(state)
|
||||
.get_mixnode_inclusion_probability(identity)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface InclusionProbabilityResponse {
|
||||
in_active: number;
|
||||
in_reserve: number;
|
||||
}
|
||||
@@ -24,4 +24,7 @@ export type Operation =
|
||||
| "TrackUnbondMixnode"
|
||||
| "WithdrawVestedCoins"
|
||||
| "TrackUndelegation"
|
||||
| "CreatePeriodicVestingAccount";
|
||||
| "CreatePeriodicVestingAccount"
|
||||
| "AdvanceCurrentInterval"
|
||||
| "WriteRewardedSet"
|
||||
| "ClearRewardedSet";
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
* 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;
|
||||
Vendored
-601
@@ -1,601 +0,0 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// 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<C> {
|
||||
nymd_client: Client<C>,
|
||||
cache: ValidatorCache,
|
||||
caching_interval: Duration,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ValidatorCache {
|
||||
inner: Arc<ValidatorCacheInner>,
|
||||
}
|
||||
|
||||
struct ValidatorCacheInner {
|
||||
initialised: AtomicBool,
|
||||
latest_known_rewarding_block: AtomicU64,
|
||||
|
||||
mixnodes: RwLock<Cache<Vec<MixNodeBond>>>,
|
||||
gateways: RwLock<Cache<Vec<GatewayBond>>>,
|
||||
|
||||
rewarded_mixnodes: RwLock<Cache<Vec<MixNodeBond>>>,
|
||||
|
||||
current_mixnode_rewarded_set_size: AtomicU32,
|
||||
current_mixnode_active_set_size: AtomicU32,
|
||||
|
||||
current_reward_params: RwLock<Cache<EpochRewardParams>>,
|
||||
}
|
||||
|
||||
fn current_unix_timestamp() -> i64 {
|
||||
let now = OffsetDateTime::now_utc();
|
||||
now.unix_timestamp()
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize, Clone)]
|
||||
pub struct Cache<T> {
|
||||
value: T,
|
||||
as_at: i64,
|
||||
}
|
||||
|
||||
impl<T: Clone> Cache<T> {
|
||||
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<C> ValidatorCacheRefresher<C> {
|
||||
pub(crate) fn new(
|
||||
nymd_client: Client<C>,
|
||||
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<MixNodeBond> {
|
||||
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<MixNodeBond> {
|
||||
// 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<Vec<MixNodeBond>>,
|
||||
) -> Option<InclusionProbabilityResponse> {
|
||||
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<Vec<MixNodeBond>>,
|
||||
) -> Option<InclusionProbabilityResponse> {
|
||||
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::<HashSet<IdentityKey>>();
|
||||
let active_set = rewarded_set
|
||||
.iter()
|
||||
.map(|bond| bond.identity().clone())
|
||||
.collect::<HashSet<IdentityKey>>();
|
||||
|
||||
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<MixNodeBond>,
|
||||
gateways: Vec<GatewayBond>,
|
||||
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<Vec<MixNodeBond>> {
|
||||
self.inner.mixnodes.read().await.clone()
|
||||
}
|
||||
|
||||
pub async fn gateways(&self) -> Cache<Vec<GatewayBond>> {
|
||||
self.inner.gateways.read().await.clone()
|
||||
}
|
||||
|
||||
pub async fn rewarded_mixnodes(&self) -> Cache<Vec<MixNodeBond>> {
|
||||
self.inner.rewarded_mixnodes.read().await.clone()
|
||||
}
|
||||
|
||||
pub async fn active_mixnodes(&self) -> Cache<Vec<MixNodeBond>> {
|
||||
// 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<EpochRewardParams> {
|
||||
self.inner.current_reward_params.read().await.clone()
|
||||
}
|
||||
|
||||
pub async fn mixnode_details(
|
||||
&self,
|
||||
identity: IdentityKeyRef<'_>,
|
||||
) -> (Option<MixNodeBond>, 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<MixNodeBond> = 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}");
|
||||
// }
|
||||
// }
|
||||
Vendored
-40
@@ -1,40 +0,0 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// 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<ValidatorCache>) -> Json<Vec<MixNodeBond>> {
|
||||
Json(cache.mixnodes().await.value)
|
||||
}
|
||||
|
||||
#[get("/gateways")]
|
||||
pub(crate) async fn get_gateways(cache: &State<ValidatorCache>) -> Json<Vec<GatewayBond>> {
|
||||
Json(cache.gateways().await.value)
|
||||
}
|
||||
|
||||
#[get("/mixnodes/rewarded")]
|
||||
pub(crate) async fn get_rewarded_mixnodes(cache: &State<ValidatorCache>) -> Json<Vec<MixNodeBond>> {
|
||||
Json(cache.rewarded_mixnodes().await.value)
|
||||
}
|
||||
|
||||
#[get("/mixnodes/active")]
|
||||
pub(crate) async fn get_active_mixnodes(cache: &State<ValidatorCache>) -> Json<Vec<MixNodeBond>> {
|
||||
Json(cache.active_mixnodes().await.value)
|
||||
}
|
||||
|
||||
#[get("/mixnodes/rewarded/inclusion-probability/<mixnode_id>")]
|
||||
pub(crate) async fn get_probs_mixnode_rewarded(
|
||||
cache: &State<ValidatorCache>,
|
||||
mixnode_id: String,
|
||||
) -> Json<Option<InclusionProbabilityResponse>> {
|
||||
Json(
|
||||
cache
|
||||
.probs_mixnode_rewarded_calculate(mixnode_id, None)
|
||||
.await,
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
"#
|
||||
}
|
||||
|
||||
@@ -0,0 +1,327 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// 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<C> {
|
||||
nymd_client: Client<C>,
|
||||
cache: ValidatorCache,
|
||||
caching_interval: Duration,
|
||||
update_rewarded_set_notify: Option<Arc<Notify>>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ValidatorCache {
|
||||
initialised: Arc<AtomicBool>,
|
||||
inner: Arc<RwLock<ValidatorCacheInner>>,
|
||||
}
|
||||
|
||||
struct ValidatorCacheInner {
|
||||
mixnodes: Cache<Vec<MixNodeBond>>,
|
||||
gateways: Cache<Vec<GatewayBond>>,
|
||||
|
||||
rewarded_set: Cache<Vec<MixNodeBond>>,
|
||||
active_set: Cache<Vec<MixNodeBond>>,
|
||||
|
||||
current_reward_params: Cache<IntervalRewardParams>,
|
||||
current_interval: Cache<Interval>,
|
||||
}
|
||||
|
||||
fn current_unix_timestamp() -> i64 {
|
||||
let now = OffsetDateTime::now_utc();
|
||||
now.unix_timestamp()
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize, Clone)]
|
||||
pub struct Cache<T> {
|
||||
value: T,
|
||||
as_at: i64,
|
||||
}
|
||||
|
||||
impl<T: Clone> Cache<T> {
|
||||
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<C> ValidatorCacheRefresher<C> {
|
||||
pub(crate) fn new(
|
||||
nymd_client: Client<C>,
|
||||
caching_interval: Duration,
|
||||
cache: ValidatorCache,
|
||||
update_rewarded_set_notify: Option<Arc<Notify>>,
|
||||
) -> 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<MixNodeBond>, Vec<MixNodeBond>) {
|
||||
let mut active_set = Vec::new();
|
||||
let mut rewarded_set = Vec::new();
|
||||
let rewarded_set_identities = rewarded_set_identities
|
||||
.into_iter()
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
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<MixNodeBond>,
|
||||
gateways: Vec<GatewayBond>,
|
||||
rewarded_set: Vec<MixNodeBond>,
|
||||
active_set: Vec<MixNodeBond>,
|
||||
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<Vec<MixNodeBond>> {
|
||||
self.inner.read().await.mixnodes.clone()
|
||||
}
|
||||
|
||||
pub async fn gateways(&self) -> Cache<Vec<GatewayBond>> {
|
||||
self.inner.read().await.gateways.clone()
|
||||
}
|
||||
|
||||
pub async fn rewarded_set(&self) -> Cache<Vec<MixNodeBond>> {
|
||||
self.inner.read().await.rewarded_set.clone()
|
||||
}
|
||||
|
||||
pub async fn active_set(&self) -> Cache<Vec<MixNodeBond>> {
|
||||
self.inner.read().await.active_set.clone()
|
||||
}
|
||||
|
||||
pub(crate) async fn interval_reward_params(&self) -> Cache<IntervalRewardParams> {
|
||||
self.inner.read().await.current_reward_params.clone()
|
||||
}
|
||||
|
||||
pub(crate) async fn current_interval(&self) -> Cache<Interval> {
|
||||
self.inner.read().await.current_interval.clone()
|
||||
}
|
||||
|
||||
pub async fn mixnode_details(
|
||||
&self,
|
||||
identity: IdentityKeyRef<'_>,
|
||||
) -> (Option<MixNodeBond>, 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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// 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<ValidatorCache>) -> Json<Vec<MixNodeBond>> {
|
||||
Json(cache.mixnodes().await.value)
|
||||
}
|
||||
|
||||
#[get("/gateways")]
|
||||
pub(crate) async fn get_gateways(cache: &State<ValidatorCache>) -> Json<Vec<GatewayBond>> {
|
||||
Json(cache.gateways().await.value)
|
||||
}
|
||||
|
||||
#[get("/mixnodes/rewarded")]
|
||||
pub(crate) async fn get_rewarded_set(cache: &State<ValidatorCache>) -> Json<Vec<MixNodeBond>> {
|
||||
Json(cache.rewarded_set().await.value)
|
||||
}
|
||||
|
||||
#[get("/mixnodes/active")]
|
||||
pub(crate) async fn get_active_set(cache: &State<ValidatorCache>) -> Json<Vec<MixNodeBond>> {
|
||||
Json(cache.active_set().await.value)
|
||||
}
|
||||
+52
-46
@@ -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<Url> {
|
||||
@@ -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::<u64>())
|
||||
{
|
||||
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<Ignite>,
|
||||
nymd_client: &Client<SigningNymdClient>,
|
||||
) -> Option<Rewarder> {
|
||||
) -> Result<Option<Rewarder>, ValidatorClientError> {
|
||||
if config.get_rewarding_enabled() && config.get_network_monitor_enabled() {
|
||||
// get instances of managed states
|
||||
let node_status_storage = rocket.state::<ValidatorApiStorage>().unwrap().clone();
|
||||
let validator_cache = rocket.state::<ValidatorCache>().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<Notify>,
|
||||
) -> Result<Rocket<Ignite>> {
|
||||
async fn setup_rocket(config: &Config, liftoff_notify: Arc<Notify>) -> Result<Rocket<Ignite>> {
|
||||
// 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::<ValidatorCache>().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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// 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<MixNodeBond>, Vec<GatewayBond>) {
|
||||
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<String>,
|
||||
) -> Option<Vec<TestRoute>> {
|
||||
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);
|
||||
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
)
|
||||
})
|
||||
|
||||
@@ -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/<identity>/report")]
|
||||
@@ -112,26 +111,25 @@ pub(crate) async fn get_mixnode_status(
|
||||
pub(crate) async fn get_mixnode_reward_estimation(
|
||||
cache: &State<ValidatorCache>,
|
||||
storage: &State<ValidatorApiStorage>,
|
||||
first_epoch: &State<Epoch>,
|
||||
identity: String,
|
||||
) -> Result<Json<RewardEstimationResponse>, 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<Json<StakeSaturationResponse>, 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/<identity>/inclusion-probability")]
|
||||
pub(crate) async fn get_mixnode_inclusion_probability(
|
||||
cache: &State<ValidatorCache>,
|
||||
identity: String,
|
||||
) -> Json<Option<InclusionProbabilityResponse>> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<SigningNymdClient> {
|
||||
|
||||
impl<C> Client<C> {
|
||||
// a helper function for the future to obtain the current block timestamp
|
||||
#[allow(dead_code)]
|
||||
pub(crate) async fn current_block_timestamp(
|
||||
&self,
|
||||
) -> Result<TendermintTime, ValidatorClientError>
|
||||
@@ -98,6 +98,13 @@ impl<C> Client<C> {
|
||||
Ok(time)
|
||||
}
|
||||
|
||||
pub(crate) async fn get_current_interval(&self) -> Result<Interval, ValidatorClientError>
|
||||
where
|
||||
C: CosmWasmClient + Sync,
|
||||
{
|
||||
Ok(self.0.read().await.get_current_interval().await?)
|
||||
}
|
||||
|
||||
pub(crate) async fn get_mixnodes(&self) -> Result<Vec<MixNodeBond>, ValidatorClientError>
|
||||
where
|
||||
C: CosmWasmClient + Sync,
|
||||
@@ -112,6 +119,9 @@ impl<C> Client<C> {
|
||||
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<ContractStateParams, ValidatorClientError>
|
||||
@@ -121,18 +131,9 @@ impl<C> Client<C> {
|
||||
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<RewardingIntervalResponse, ValidatorClientError>
|
||||
where
|
||||
C: CosmWasmClient + Sync,
|
||||
{
|
||||
self.0.read().await.get_current_rewarding_interval().await
|
||||
}
|
||||
|
||||
pub(crate) async fn get_current_epoch_reward_params(
|
||||
&self,
|
||||
) -> Result<EpochRewardParams, ValidatorClientError>
|
||||
) -> Result<IntervalRewardParams, ValidatorClientError>
|
||||
where
|
||||
C: CosmWasmClient + Sync,
|
||||
{
|
||||
@@ -140,25 +141,25 @@ impl<C> Client<C> {
|
||||
|
||||
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<MixnodeRewardingStatusResponse, ValidatorClientError>
|
||||
where
|
||||
C: CosmWasmClient + Sync,
|
||||
@@ -166,7 +167,7 @@ impl<C> Client<C> {
|
||||
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<C> Client<C> {
|
||||
/// # 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<Option<[u8; SHA256_HASH_SIZE]>, ValidatorClientError>
|
||||
@@ -205,26 +207,45 @@ impl<C> Client<C> {
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn begin_mixnode_rewarding(
|
||||
pub(crate) async fn get_rewarded_set_identities(
|
||||
&self,
|
||||
rewarding_interval_nonce: u32,
|
||||
) -> Result<(), RewardingError>
|
||||
) -> Result<Vec<(IdentityKey, RewardedSetNodeStatus)>, 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<RewardedSetUpdateDetails, ValidatorClientError>
|
||||
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<IdentityKey>,
|
||||
expected_active_set_size: u32,
|
||||
) -> Result<(), ValidatorClientError>
|
||||
where
|
||||
C: SigningCosmWasmClient + Sync,
|
||||
{
|
||||
@@ -232,7 +253,7 @@ impl<C> Client<C> {
|
||||
.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<C> Client<C> {
|
||||
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<C> Client<C> {
|
||||
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<C> Client<C> {
|
||||
// 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<C> Client<C> {
|
||||
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<C> Client<C> {
|
||||
// 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<C> Client<C> {
|
||||
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();
|
||||
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// 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<SigningNymdClient>,
|
||||
update_rewarded_set_notify: Arc<Notify>,
|
||||
validator_cache: ValidatorCache,
|
||||
}
|
||||
|
||||
impl RewardedSetUpdater {
|
||||
pub(crate) fn new(
|
||||
nymd_client: Client<SigningNymdClient>,
|
||||
update_rewarded_set_notify: Arc<Notify>,
|
||||
validator_cache: ValidatorCache,
|
||||
) -> Self {
|
||||
RewardedSetUpdater {
|
||||
nymd_client,
|
||||
update_rewarded_set_notify,
|
||||
validator_cache,
|
||||
}
|
||||
}
|
||||
|
||||
fn determine_rewarded_set(
|
||||
&self,
|
||||
mixnodes: Vec<MixNodeBond>,
|
||||
nodes_to_select: u32,
|
||||
) -> Vec<IdentityKey> {
|
||||
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::<Vec<_>>();
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,219 +0,0 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// 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)
|
||||
);
|
||||
}
|
||||
}
|
||||
+211
-258
@@ -1,10 +1,9 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// 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<SigningNymdClient>,
|
||||
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<Vec<MixnodeToReward>, 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::<HashSet<_>>();
|
||||
|
||||
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<Vec<FailedMixnodeRewardChunkDetails>> {
|
||||
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<FailureData>), 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<bool, RewardingError> {
|
||||
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<bool, RewardingError> {
|
||||
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<bool, RewardingError> {
|
||||
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<Epoch, RewardingError> {
|
||||
// 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<Duration> {
|
||||
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<bool, RewardingError> {
|
||||
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<bool, RewardingError> {
|
||||
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<std::ops::ControlFlow<()>, 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Vec<NodeStatus>, 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<Vec<NodeStatus>, 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<Vec<NodeStatus>, 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<Vec<NodeStatus>, 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<NodeResult>,
|
||||
) -> 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<NodeResult>,
|
||||
) -> 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<i32, sqlx::Error> {
|
||||
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<i32, sqlx::Error> {
|
||||
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<i64, sqlx::Error> {
|
||||
pub(super) async fn insert_monitor_run(&self, timestamp: i64) -> Result<i64, sqlx::Error> {
|
||||
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<i32, sqlx::Error> {
|
||||
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<Vec<ActiveNode>, 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<Vec<ActiveNode>, 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<i64, sqlx::Error> {
|
||||
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<Option<EpochRewarding>, sqlx::Error> {
|
||||
// ) -> Result<Option<IntervalRewarding>, 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<Option<EpochRewarding>, sqlx::Error> {
|
||||
interval_start_timestamp: i64,
|
||||
) -> Result<Option<IntervalRewarding>, 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<Vec<ActiveNodeStatuses>, 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<Vec<ActiveNodeStatuses>, sqlx::Error> {
|
||||
let active_nodes = self
|
||||
.get_all_active_gateways_in_interval(since, until)
|
||||
|
||||
@@ -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<Vec<NodeStatus>, ValidatorApiStorageError> {
|
||||
let statuses = self
|
||||
.manager
|
||||
@@ -102,7 +99,7 @@ impl ValidatorApiStorage {
|
||||
async fn get_gateway_statuses(
|
||||
&self,
|
||||
identity: &str,
|
||||
since: UnixTimestamp,
|
||||
since: i64,
|
||||
) -> Result<Vec<NodeStatus>, 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<Uptime, ValidatorApiStorageError> {
|
||||
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<Vec<MixnodeStatusReport>, 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<Vec<GatewayStatusReport>, 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<UnixTimestamp>,
|
||||
since: Option<i64>,
|
||||
) -> Result<i32, ValidatorApiStorageError> {
|
||||
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<UnixTimestamp>,
|
||||
since: Option<i64>,
|
||||
) -> Result<i32, ValidatorApiStorageError> {
|
||||
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<usize, ValidatorApiStorageError> {
|
||||
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<i64, ValidatorApiStorageError> {
|
||||
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<Option<EpochRewarding>, ValidatorApiStorageError> {
|
||||
// ) -> Result<Option<IntervalRewarding>, 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<Option<EpochRewarding>, ValidatorApiStorageError> {
|
||||
interval_timestamp: i64,
|
||||
) -> Result<Option<IntervalRewarding>, 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
|
||||
///
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// 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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user