Refactor to a lazy rewarding system (#1127)

* Remove eager operator and delegator rewarding

* Add mixnodes snapshoting

* Add Intervals map, getter and setter

* Refactor reward params

* Refactor MixnodeToReward

* Persist node reward params and results on chain

* Update cw-storage-plus to 0.12.1

* Refactor delegation storage

* Compound delegator reward command

* Compound delegator reward command

* Add defered delegate and undelegate

* Compound on behalf command

* Scale calculations to epoch

* Rename interval -> epoch where practical

* Store epochs on chain

* Cleanup first pass

* Adapt reporting to lazy rewarding

* make clippy --all
This commit is contained in:
Drazen Urch
2022-03-09 14:28:16 +01:00
committed by GitHub
parent 379dd1f02b
commit b30f680549
51 changed files with 2510 additions and 2664 deletions
Generated
+13 -2
View File
@@ -1077,6 +1077,17 @@ dependencies = [
"serde",
]
[[package]]
name = "cw-storage-plus"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c087ff98fb0475db4c2b5298a5fd12b2848d2854b39d1115d930ee6da24d1eed"
dependencies = [
"cosmwasm-std",
"schemars",
"serde",
]
[[package]]
name = "darling"
version = "0.13.1"
@@ -6046,7 +6057,7 @@ version = "0.1.0"
dependencies = [
"config",
"cosmwasm-std",
"cw-storage-plus",
"cw-storage-plus 0.12.1",
"mixnet-contract-common",
"schemars",
"serde",
@@ -6060,7 +6071,7 @@ version = "0.1.0"
dependencies = [
"config",
"cosmwasm-std",
"cw-storage-plus",
"cw-storage-plus 0.11.1",
"mixnet-contract-common",
"schemars",
"serde",
+3 -3
View File
@@ -16,13 +16,13 @@ clippy-happy-wallet:
cargo clippy --manifest-path nym-wallet/Cargo.toml
clippy-all-main:
cargo clippy --all-features -- -D warnings
cargo clippy --all --all-features -- -D warnings
clippy-all-contracts:
cargo clippy --manifest-path contracts/Cargo.toml --all-features --target wasm32-unknown-unknown -- -D warnings
cargo clippy --all --manifest-path contracts/Cargo.toml --all-features --target wasm32-unknown-unknown -- -D warnings
clippy-all-wallet:
cargo clippy --manifest-path nym-wallet/Cargo.toml --all-features -- -D warnings
cargo clippy --all --manifest-path nym-wallet/Cargo.toml --all-features -- -D warnings
test-main:
cargo test --all-features --all
@@ -283,6 +283,13 @@ impl<C> Client<C> {
Ok(self.nymd.get_current_interval().await?)
}
pub async fn get_epochs_in_interval(&self) -> Result<u64, ValidatorClientError>
where
C: CosmWasmClient + Sync,
{
Ok(self.nymd.get_epochs_in_interval().await?)
}
pub async fn get_circulating_supply(&self) -> Result<u128, ValidatorClientError>
where
C: CosmWasmClient + Sync,
@@ -47,9 +47,12 @@ pub enum Operation {
CreatePeriodicVestingAccount,
AdvanceCurrentInterval,
AdvanceCurrentEpoch,
WriteRewardedSet,
ClearRewardedSet,
UpdateMixnetAddress,
CheckpointMixnodes,
ReconcileDelegations,
}
pub(crate) fn calculate_fee(gas_price: &GasPrice, gas_limit: Gas) -> Coin {
@@ -91,6 +94,9 @@ impl fmt::Display for Operation {
Operation::WriteRewardedSet => f.write_str("WriteRewardedSet"),
Operation::ClearRewardedSet => f.write_str("ClearRewardedSet"),
Operation::UpdateMixnetAddress => f.write_str("UpdateMixnetAddress"),
Operation::CheckpointMixnodes => f.write_str("CheckpointMixnodes"),
Operation::ReconcileDelegations => f.write_str("ReconcileDelegations"),
Operation::AdvanceCurrentEpoch => f.write_str("AdvanceCurrentEpoch"),
}
}
}
@@ -132,6 +138,9 @@ impl Operation {
Operation::WriteRewardedSet => 175_000u64.into(),
Operation::ClearRewardedSet => 175_000u64.into(),
Operation::UpdateMixnetAddress => 80_000u64.into(),
Operation::CheckpointMixnodes => 175_000u64.into(),
Operation::ReconcileDelegations => 500_000u64.into(),
Operation::AdvanceCurrentEpoch => 175_000u64.into(),
}
}
@@ -9,7 +9,8 @@ use crate::nymd::cosmwasm_client::types::{
use crate::nymd::error::NymdError;
use crate::nymd::wallet::DirectSecp256k1HdWallet;
use cosmrs::rpc::endpoint::broadcast;
use cosmrs::rpc::{Error as TendermintRpcError, HttpClientUrl};
use cosmrs::rpc::Error as TendermintRpcError;
use cosmrs::rpc::HttpClientUrl;
use cosmwasm_std::{Coin, Uint128};
pub use fee::gas_price::GasPrice;
use fee::helpers::Operation;
@@ -388,6 +389,16 @@ impl<C> NymdClient<C> {
.await
}
pub async fn get_epochs_in_interval(&self) -> Result<u64, NymdError>
where
C: CosmWasmClient + Sync,
{
let request = QueryMsg::GetEpochsInInterval {};
self.client
.query_contract_smart(self.mixnet_contract_address()?, &request)
.await
}
pub async fn get_circulating_supply(&self) -> Result<Uint128, NymdError>
where
C: CosmWasmClient + Sync,
@@ -496,7 +507,7 @@ impl<C> NymdClient<C> {
pub async fn get_mix_delegations_paged(
&self,
mix_identity: IdentityKey,
start_after: Option<String>,
start_after: Option<(String, u64)>,
page_limit: Option<u32>,
) -> Result<PagedMixDelegationsResponse, NymdError>
where
@@ -515,7 +526,7 @@ impl<C> NymdClient<C> {
/// Gets list of all mixnode delegations on particular page.
pub async fn get_all_network_delegations_paged(
&self,
start_after: Option<(IdentityKey, String)>,
start_after: Option<(IdentityKey, Vec<u8>, u64)>,
page_limit: Option<u32>,
) -> Result<PagedAllDelegationsResponse, NymdError>
where
@@ -1175,6 +1186,25 @@ impl<C> NymdClient<C> {
.await
}
pub async fn advance_current_epoch(&self) -> Result<ExecuteResult, NymdError>
where
C: SigningCosmWasmClient + Sync,
{
let fee = self.operation_fee(Operation::AdvanceCurrentEpoch);
let req = ExecuteMsg::AdvanceCurrentEpoch {};
self.client
.execute(
self.address(),
self.mixnet_contract_address()?,
&req,
fee,
"Advance current epoch",
Vec::new(),
)
.await
}
pub async fn advance_current_interval(&self) -> Result<ExecuteResult, NymdError>
where
C: SigningCosmWasmClient + Sync,
@@ -1194,6 +1224,44 @@ impl<C> NymdClient<C> {
.await
}
pub async fn reconcile_delegations(&self) -> Result<ExecuteResult, NymdError>
where
C: SigningCosmWasmClient + Sync,
{
let fee = self.operation_fee(Operation::ReconcileDelegations);
let req = ExecuteMsg::ReconcileDelegations {};
self.client
.execute(
self.address(),
self.mixnet_contract_address()?,
&req,
fee,
"Reconciling delegation events",
Vec::new(),
)
.await
}
pub async fn checkpoint_mixnodes(&self) -> Result<ExecuteResult, NymdError>
where
C: SigningCosmWasmClient + Sync,
{
let fee = self.operation_fee(Operation::CheckpointMixnodes);
let req = ExecuteMsg::CheckpointMixnodes {};
self.client
.execute(
self.address(),
self.mixnet_contract_address()?,
&req,
fee,
"Snapshotting mixnodes",
Vec::new(),
)
.await
}
pub async fn write_rewarded_set(
&self,
rewarded_set: Vec<IdentityKey>,
@@ -37,8 +37,12 @@ impl Delegation {
}
// TODO: change that to use .joined_key() and return Vec<u8>
pub fn storage_key(&self) -> (IdentityKey, Addr) {
(self.node_identity(), self.owner())
pub fn storage_key(&self) -> (IdentityKey, Vec<u8>, u64) {
(
self.node_identity(),
self.owner().as_bytes().to_vec(),
self.block_height(),
)
}
pub fn increment_amount(&mut self, amount: Uint128, at_height: Option<u64>) {
@@ -78,14 +82,14 @@ impl Display for Delegation {
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)]
pub struct PagedMixDelegationsResponse {
pub delegations: Vec<Delegation>,
pub start_next_after: Option<String>,
pub start_next_after: Option<(String, u64)>,
}
impl PagedMixDelegationsResponse {
pub fn new(delegations: Vec<Delegation>, start_next_after: Option<Addr>) -> Self {
pub fn new(delegations: Vec<Delegation>, start_next_after: Option<(Addr, u64)>) -> Self {
PagedMixDelegationsResponse {
delegations,
start_next_after: start_next_after.map(|s| s.to_string()),
start_next_after: start_next_after.map(|(s, h)| (s.to_string(), h)),
}
}
}
@@ -108,17 +112,17 @@ impl PagedDelegatorDelegationsResponse {
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)]
pub struct PagedAllDelegationsResponse {
pub delegations: Vec<Delegation>,
pub start_next_after: Option<(IdentityKey, String)>,
pub start_next_after: Option<(IdentityKey, Vec<u8>, u64)>,
}
impl PagedAllDelegationsResponse {
pub fn new(
delegations: Vec<Delegation>,
start_next_after: Option<(IdentityKey, Addr)>,
start_next_after: Option<(IdentityKey, Vec<u8>, u64)>,
) -> Self {
PagedAllDelegationsResponse {
delegations,
start_next_after: start_next_after.map(|(id, addr)| (id, addr.to_string())),
start_next_after: start_next_after.map(|(id, addr, height)| (id, addr, height)),
}
}
}
@@ -6,4 +6,11 @@ pub enum MixnetContractError {
OverflowError(#[from] cosmwasm_std::OverflowError),
#[error("reward_blockstamp field not set, set_reward_blockstamp must be called before attempting to issue rewards")]
BlockstampNotSet,
#[error("{source}")]
TryFromIntError {
#[from]
source: std::num::TryFromIntError,
},
#[error("Error casting from U128")]
CastError,
}
@@ -2,14 +2,16 @@
// SPDX-License-Identifier: Apache-2.0
use crate::mixnode::NodeRewardResult;
use crate::{ContractStateParams, Delegation, IdentityKeyRef, Interval, Layer};
use crate::{ContractStateParams, IdentityKeyRef, Interval, Layer};
use cosmwasm_std::{Addr, Coin, Event, Uint128};
pub use contracts_common::events::*;
// FIXME: This should becoma an Enum
// event types
pub const DELEGATION_EVENT_TYPE: &str = "delegation";
pub const PENDING_DELEGATION_EVENT_TYPE: &str = "pending_delegation";
pub const UNDELEGATION_EVENT_TYPE: &str = "undelegation";
pub const PENDING_UNDELEGATION_EVENT_TYPE: &str = "pending_undelegation";
pub const GATEWAY_BONDING_EVENT_TYPE: &str = "gateway_bonding";
pub const GATEWAY_UNBONDING_EVENT_TYPE: &str = "gateway_unbonding";
pub const MIXNODE_BONDING_EVENT_TYPE: &str = "mixnode_bonding";
@@ -19,6 +21,10 @@ pub const OPERATOR_REWARDING_EVENT_TYPE: &str = "mix_rewarding";
pub const MIX_DELEGATORS_REWARDING_EVENT_TYPE: &str = "mix_delegators_rewarding";
pub const CHANGE_REWARDED_SET_EVENT_TYPE: &str = "change_rewarded_set";
pub const ADVANCE_INTERVAL_EVENT_TYPE: &str = "advance_interval";
pub const ADVANCE_EPOCH_EVENT_TYPE: &str = "advance_epoch";
pub const COMPOUND_DELEGATOR_REWARD_EVENT_TYPE: &str = "compound_delegator_reward";
pub const COMPOUND_OPERATOR_REWARD_EVENT_TYPE: &str = "compound_operator_reward";
pub const SNAPSHOT_MIXNODES_EVENT: &str = "snapshot_mixnodes";
// attributes that are used in multiple places
pub const OWNER_KEY: &str = "owner";
@@ -70,6 +76,14 @@ 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 const NEW_CURRENT_EPOCH_KEY: &str = "new_current_epoch";
pub const BLOCK_HEIGHT_KEY: &str = "block_height";
pub const CHECKPOINT_MIXNODES_EVENT: &str = "checkpoint_mixnodes";
pub fn new_checkpoint_mixnodes_event(block_height: u64) -> Event {
Event::new(CHECKPOINT_MIXNODES_EVENT)
.add_attribute(BLOCK_HEIGHT_KEY, format!("{}", block_height))
}
pub fn new_delegation_event(
delegator: &Addr,
@@ -89,11 +103,55 @@ pub fn new_delegation_event(
.add_attribute(DELEGATION_TARGET_KEY, mix_identity)
}
pub fn new_pending_delegation_event(
delegator: &Addr,
proxy: &Option<Addr>,
amount: &Coin,
mix_identity: IdentityKeyRef<'_>,
) -> Event {
let mut event =
Event::new(PENDING_DELEGATION_EVENT_TYPE).add_attribute(DELEGATOR_KEY, delegator);
if let Some(proxy) = proxy {
event = event.add_attribute(PROXY_KEY, proxy)
}
// coin implements Display trait and we use that implementation here
event
.add_attribute(AMOUNT_KEY, amount.to_string())
.add_attribute(DELEGATION_TARGET_KEY, mix_identity)
}
pub fn new_compound_operator_reward_event(owner: &Addr, amount: Uint128) -> Event {
let event = Event::new(COMPOUND_OPERATOR_REWARD_EVENT_TYPE).add_attribute(OWNER_KEY, owner);
event.add_attribute(AMOUNT_KEY, amount.to_string())
}
pub fn new_compound_delegator_reward_event(
delegator: &Addr,
proxy: &Option<Addr>,
amount: Uint128,
mix_identity: IdentityKeyRef<'_>,
) -> Event {
let mut event =
Event::new(COMPOUND_DELEGATOR_REWARD_EVENT_TYPE).add_attribute(DELEGATOR_KEY, delegator);
if let Some(proxy) = proxy {
event = event.add_attribute(PROXY_KEY, proxy)
}
// coin implements Display trait and we use that implementation here
event
.add_attribute(AMOUNT_KEY, amount.to_string())
.add_attribute(DELEGATION_TARGET_KEY, mix_identity)
.add_attribute(DELEGATOR_KEY, delegator)
}
pub fn new_undelegation_event(
delegator: &Addr,
proxy: &Option<Addr>,
old_delegation: &Delegation,
mix_identity: IdentityKeyRef<'_>,
amount: Uint128,
) -> Event {
let mut event = Event::new(UNDELEGATION_EVENT_TYPE).add_attribute(DELEGATOR_KEY, delegator);
@@ -103,14 +161,26 @@ pub fn new_undelegation_event(
// coin implements Display trait and we use that implementation here
event
.add_attribute(AMOUNT_KEY, old_delegation.amount.to_string())
.add_attribute(
DELEGATION_HEIGHT_KEY,
old_delegation.block_height.to_string(),
)
.add_attribute(AMOUNT_KEY, amount.to_string())
.add_attribute(DELEGATION_TARGET_KEY, mix_identity)
}
pub fn new_pending_undelegation_event(
delegator: &Addr,
proxy: &Option<Addr>,
mix_identity: IdentityKeyRef<'_>,
) -> Event {
let mut event =
Event::new(PENDING_UNDELEGATION_EVENT_TYPE).add_attribute(DELEGATOR_KEY, delegator);
if let Some(proxy) = proxy {
event = event.add_attribute(PROXY_KEY, proxy)
}
// coin implements Display trait and we use that implementation here
event.add_attribute(DELEGATION_TARGET_KEY, mix_identity)
}
pub fn new_gateway_bonding_event(
owner: &Addr,
proxy: &Option<Addr>,
@@ -280,9 +350,6 @@ pub fn new_mix_operator_rewarding_event(
node_reward_result: NodeRewardResult,
node_pledge: Uint128,
node_delegation: Uint128,
operator_reward: Uint128,
delegation_rewards_distributed: Uint128,
further_delegations: bool,
) -> Event {
Event::new(OPERATOR_REWARDING_EVENT_TYPE)
.add_attribute(INTERVAL_ID_KEY, interval_id.to_string())
@@ -295,15 +362,6 @@ pub fn new_mix_operator_rewarding_event(
)
.add_attribute(LAMBDA_KEY, node_reward_result.lambda().to_string())
.add_attribute(SIGMA_KEY, node_reward_result.sigma().to_string())
.add_attribute(OPERATOR_REWARD_KEY, operator_reward)
.add_attribute(
DISTRIBUTED_DELEGATION_REWARDS_KEY,
delegation_rewards_distributed,
)
.add_attribute(
FURTHER_DELEGATIONS_TO_REWARD_KEY,
further_delegations.to_string(),
)
}
pub fn new_mix_delegators_rewarding_event(
@@ -343,3 +401,7 @@ pub fn new_advance_interval_event(interval: Interval) -> Event {
Event::new(ADVANCE_INTERVAL_EVENT_TYPE)
.add_attribute(NEW_CURRENT_INTERVAL_KEY, interval.to_string())
}
pub fn new_advance_epoch_event(interval: Interval) -> Event {
Event::new(ADVANCE_EPOCH_EVENT_TYPE).add_attribute(NEW_CURRENT_EPOCH_KEY, interval.to_string())
}
@@ -72,7 +72,7 @@ impl Interval {
/// Returns the next interval.
#[must_use]
pub fn next_interval(&self) -> Self {
pub fn next(&self) -> Self {
Interval {
id: self.id + 1,
start: self.end(),
@@ -81,7 +81,7 @@ impl Interval {
}
/// Returns the last interval.
pub fn previous_interval(&self) -> Option<Self> {
pub fn previous(&self) -> Option<Self> {
if self.id > 0 {
Some(Interval {
id: self.id - 1,
@@ -125,14 +125,14 @@ impl Interval {
if candidate.contains(now) {
return Some(candidate);
}
candidate = candidate.next_interval();
candidate = candidate.next();
}
} else {
loop {
if candidate.contains(now) {
return Some(candidate);
}
candidate = candidate.previous_interval()?;
candidate = candidate.previous()?;
}
}
}
@@ -151,14 +151,14 @@ impl Interval {
if candidate.contains_timestamp(now_unix) {
return Some(candidate);
}
candidate = candidate.next_interval();
candidate = candidate.next();
}
} else {
loop {
if candidate.contains_timestamp(now_unix) {
return Some(candidate);
}
candidate = candidate.previous_interval()?;
candidate = candidate.previous()?;
}
}
}
@@ -239,7 +239,7 @@ mod tests {
use super::*;
#[test]
fn previous_interval() {
fn previous() {
let interval = Interval {
id: 1,
start: time::macros::datetime!(2021-08-23 12:00 UTC),
@@ -250,18 +250,18 @@ mod tests {
start: time::macros::datetime!(2021-08-22 12:00 UTC),
length: Duration::from_secs(24 * 60 * 60),
};
assert_eq!(expected, interval.previous_interval().unwrap());
assert_eq!(expected, interval.previous().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());
assert!(genesis_interval.previous().is_none());
}
#[test]
fn next_interval() {
fn next() {
let interval = Interval {
id: 0,
start: time::macros::datetime!(2021-08-23 12:00 UTC),
@@ -273,7 +273,7 @@ mod tests {
length: Duration::from_secs(24 * 60 * 60),
};
assert_eq!(expected, interval.next_interval())
assert_eq!(expected, interval.next())
}
#[test]
@@ -291,8 +291,8 @@ mod tests {
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()));
assert!(!interval.contains(interval.next().end()));
assert!(!interval.contains(interval.previous().unwrap().start()));
}
#[test]
@@ -305,10 +305,7 @@ mod tests {
// interval just before
let fake_now = first_interval.start - Duration::from_secs(123);
assert_eq!(
first_interval.previous_interval(),
first_interval.current(fake_now)
);
assert_eq!(first_interval.previous(), first_interval.current(fake_now));
// this interval (start boundary)
assert_eq!(
@@ -329,7 +326,7 @@ mod tests {
// next interval
let fake_now = first_interval.end() + Duration::from_secs(123);
assert_eq!(
first_interval.next_interval(),
first_interval.next(),
first_interval.current(fake_now).unwrap()
);
@@ -340,11 +337,11 @@ mod tests {
- first_interval.length;
assert_eq!(
first_interval
.previous_interval()
.previous()
.unwrap()
.previous_interval()
.previous()
.unwrap()
.previous_interval()
.previous()
.unwrap(),
first_interval.current(fake_now).unwrap()
);
@@ -355,10 +352,7 @@ mod tests {
+ first_interval.length
+ first_interval.length;
assert_eq!(
first_interval
.next_interval()
.next_interval()
.next_interval(),
first_interval.next().next().next(),
first_interval.current(fake_now).unwrap()
);
}
@@ -8,6 +8,7 @@ mod gateway;
mod interval;
pub mod mixnode;
mod msg;
pub mod reward_params;
mod types;
pub const MIXNODE_DELEGATORS_PAGE_LIMIT: usize = 250;
@@ -24,3 +25,9 @@ pub use mixnode::{
};
pub use msg::*;
pub use types::*;
pub type U128 = fixed::types::U75F53;
fixed::const_fixed_from_int! {
const ONE: U128 = 1;
}
@@ -1,23 +1,19 @@
// due to code generated by JsonSchema
#![allow(clippy::field_reassign_with_default)]
use crate::{IdentityKey, SphinxKey};
use crate::error::MixnetContractError;
use crate::reward_params::RewardParams;
use crate::{Delegation, IdentityKey, SphinxKey};
use crate::{ONE, U128};
use az::CheckedCast;
use cosmwasm_std::{coin, Addr, Coin, Uint128};
use log::error;
use network_defaults::DEFAULT_OPERATOR_INTERVAL_COST;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr};
use std::cmp::Ordering;
use std::fmt::Display;
type U128 = fixed::types::U75F53; // u128 with 18 significant digits
fixed::const_fixed_from_int! {
const ONE: U128 = 1;
}
#[cfg_attr(test, derive(ts_rs::TS))]
#[cfg_attr(
test,
@@ -26,7 +22,7 @@ fixed::const_fixed_from_int! {
export_to = "../../../nym-wallet/src/types/rust/rewardedsetnodestatus.ts"
)
)]
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, PartialOrd, Serialize, JsonSchema)]
#[derive(Clone, Copy, Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
pub enum RewardedSetNodeStatus {
Active,
Standby,
@@ -38,6 +34,52 @@ impl RewardedSetNodeStatus {
}
}
#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)]
pub enum DelegationEvent {
Delegate(Delegation),
Undelegate(PendingUndelegate),
}
#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)]
pub struct PendingUndelegate {
mix_identity: IdentityKey,
delegate: Addr,
proxy: Option<Addr>,
block_height: u64,
}
impl PendingUndelegate {
pub fn new(
mix_identity: IdentityKey,
delegate: Addr,
proxy: Option<Addr>,
block_height: u64,
) -> Self {
Self {
mix_identity,
delegate,
proxy,
block_height,
}
}
pub fn mix_identity(&self) -> IdentityKey {
self.mix_identity.clone()
}
pub fn delegate(&self) -> Addr {
self.delegate.clone()
}
pub fn proxy(&self) -> Option<Addr> {
self.proxy.clone()
}
pub fn block_height(&self) -> u64 {
self.block_height
}
}
#[cfg_attr(test, derive(ts_rs::TS))]
#[cfg_attr(
test,
@@ -87,112 +129,6 @@ impl From<Layer> for String {
}
}
#[derive(Debug, Clone, JsonSchema, PartialEq, Serialize, Deserialize, Copy)]
pub struct NodeRewardParams {
period_reward_pool: Uint128,
rewarded_set_size: Uint128,
active_set_size: Uint128,
reward_blockstamp: u64,
circulating_supply: Uint128,
uptime: Uint128,
sybil_resistance_percent: u8,
in_active_set: bool,
active_set_work_factor: u8,
}
impl NodeRewardParams {
#[allow(clippy::too_many_arguments)]
pub fn new(
period_reward_pool: u128,
rewarded_set_size: u128,
active_set_size: u128,
reward_blockstamp: u64,
circulating_supply: u128,
uptime: u128,
sybil_resistance_percent: u8,
in_active_set: bool,
active_set_work_factor: u8,
) -> NodeRewardParams {
NodeRewardParams {
period_reward_pool: Uint128::new(period_reward_pool),
rewarded_set_size: Uint128::new(rewarded_set_size),
active_set_size: Uint128::new(active_set_size),
reward_blockstamp,
circulating_supply: Uint128::new(circulating_supply),
uptime: Uint128::new(uptime),
sybil_resistance_percent,
in_active_set,
active_set_work_factor,
}
}
pub fn omega(&self) -> U128 {
// As per keybase://chat/nymtech#tokeneconomics/1179
let denom = self.active_set_work_factor() * U128::from_num(self.rewarded_set_size())
- (self.active_set_work_factor() - ONE) * U128::from_num(self.idle_nodes().u128());
if self.in_active_set() {
// work_active = factor / (factor * self.network.k[month] - (factor - 1) * idle_nodes)
self.active_set_work_factor() / denom * self.rewarded_set_size()
} else {
// work_idle = 1 / (factor * self.network.k[month] - (factor - 1) * idle_nodes)
ONE / denom * self.rewarded_set_size()
}
}
pub fn idle_nodes(&self) -> Uint128 {
self.rewarded_set_size - self.active_set_size
}
pub fn active_set_work_factor(&self) -> U128 {
U128::from_num(self.active_set_work_factor)
}
pub fn in_active_set(&self) -> bool {
self.in_active_set
}
pub fn performance(&self) -> U128 {
U128::from_num(self.uptime.u128()) / U128::from_num(100)
}
pub fn operator_cost(&self) -> U128 {
U128::from_num(self.uptime.u128() / 100u128 * DEFAULT_OPERATOR_INTERVAL_COST as u128)
}
pub fn set_reward_blockstamp(&mut self, blockstamp: u64) {
self.reward_blockstamp = blockstamp;
}
pub fn period_reward_pool(&self) -> u128 {
self.period_reward_pool.u128()
}
pub fn rewarded_set_size(&self) -> u128 {
self.rewarded_set_size.u128()
}
pub fn circulating_supply(&self) -> u128 {
self.circulating_supply.u128()
}
pub fn reward_blockstamp(&self) -> u64 {
self.reward_blockstamp
}
pub fn uptime(&self) -> u128 {
self.uptime.u128()
}
pub fn one_over_k(&self) -> U128 {
ONE / U128::from_num(self.rewarded_set_size.u128())
}
pub fn alpha(&self) -> U128 {
U128::from_num(self.sybil_resistance_percent) / U128::from_num(100)
}
}
// cosmwasm's limited serde doesn't work with U128 directly
#[allow(non_snake_case)]
pub mod fixed_U128_as_string {
@@ -226,7 +162,7 @@ pub mod fixed_U128_as_string {
// everything required to reward delegator of given mixnode
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
pub struct DelegatorRewardParams {
node_reward_params: NodeRewardParams,
reward_params: RewardParams,
// to be completely honest I don't understand all consequences of using `#[schemars(with = "String")]`
// for U128 here, but it seems that CosmWasm is using the same attribute for their Uint128
@@ -242,19 +178,24 @@ pub struct DelegatorRewardParams {
}
impl DelegatorRewardParams {
pub fn new(mixnode_bond: &MixNodeBond, node_reward_params: NodeRewardParams) -> Self {
pub fn new(
sigma: U128,
profit_margin: U128,
node_profit: U128,
reward_params: RewardParams,
) -> Self {
DelegatorRewardParams {
sigma: mixnode_bond.sigma(&node_reward_params),
profit_margin: mixnode_bond.profit_margin(),
node_profit: mixnode_bond.node_profit(&node_reward_params),
node_reward_params,
sigma,
profit_margin,
node_profit,
reward_params,
}
}
pub fn determine_delegation_reward(&self, delegation_amount: Uint128) -> u128 {
// change all values into their fixed representations
let delegation_amount = U128::from_num(delegation_amount.u128());
let circulating_supply = U128::from_num(self.node_reward_params.circulating_supply());
let circulating_supply = U128::from_num(self.reward_params.circulating_supply());
let scaled_delegation_amount = delegation_amount / circulating_supply;
let delegator_reward =
@@ -272,8 +213,56 @@ impl DelegatorRewardParams {
}
}
pub fn node_reward_params(&self) -> &NodeRewardParams {
&self.node_reward_params
pub fn node_reward_params(&self) -> RewardParams {
self.reward_params
}
}
#[derive(Debug, Clone, JsonSchema, PartialEq, Serialize, Deserialize, Copy)]
pub struct StoredNodeRewardResult {
reward: Uint128,
lambda: Uint128,
sigma: Uint128,
}
impl StoredNodeRewardResult {
pub fn reward(&self) -> Uint128 {
self.reward
}
pub fn lambda(&self) -> Uint128 {
self.lambda
}
pub fn sigma(&self) -> Uint128 {
self.sigma
}
}
impl TryFrom<NodeRewardResult> for StoredNodeRewardResult {
type Error = MixnetContractError;
fn try_from(node_reward_result: NodeRewardResult) -> Result<Self, Self::Error> {
Ok(StoredNodeRewardResult {
reward: Uint128::new(
node_reward_result
.reward()
.checked_cast()
.ok_or(MixnetContractError::CastError)?,
),
lambda: Uint128::new(
node_reward_result
.lambda()
.checked_cast()
.ok_or(MixnetContractError::CastError)?,
),
sigma: Uint128::new(
node_reward_result
.sigma()
.checked_cast()
.ok_or(MixnetContractError::CastError)?,
),
})
}
}
@@ -307,6 +296,7 @@ pub struct MixNodeBond {
pub block_height: u64,
pub mix_node: MixNode,
pub proxy: Option<Addr>,
pub accumulated_rewards: Uint128,
}
impl MixNodeBond {
@@ -326,6 +316,7 @@ impl MixNodeBond {
block_height,
mix_node,
proxy,
accumulated_rewards: Uint128::zero(),
}
}
@@ -349,11 +340,16 @@ impl MixNodeBond {
&self.mix_node
}
// Takes into account accumulated rewards as well as current pledge and delegation amounts
pub fn total_bond(&self) -> Option<u128> {
if self.pledge_amount.denom != self.total_delegation.denom {
None
} else {
Some(self.pledge_amount.amount.u128() + self.total_delegation.amount.u128())
Some(
self.pledge_amount.amount.u128()
+ self.total_delegation.amount.u128()
+ self.accumulated_rewards.u128(),
)
}
}
@@ -366,6 +362,10 @@ impl MixNodeBond {
* U128::from_num(rewarded_set_size)
}
// TODO: There is an effect here when adding accumulted rewards to the total bond, ie accumulated rewards will not
// affect lambda, but will affect sigma, in turn over time, if left unclaimed operator rewards will not compound, but
// behave similarly to delegations.
// The question is should this be taken into account when calculating operator rewards?
pub fn pledge_to_circulating_supply(&self, circulating_supply: u128) -> U128 {
U128::from_num(self.pledge_amount().amount.u128()) / U128::from_num(circulating_supply)
}
@@ -375,26 +375,46 @@ impl MixNodeBond {
/ U128::from_num(circulating_supply)
}
pub fn lambda(&self, params: &NodeRewardParams) -> U128 {
pub fn lambda(&self, params: &RewardParams) -> U128 {
// Ratio of a bond to the token circulating supply
let pledge_to_circulating_supply_ratio =
self.pledge_to_circulating_supply(params.circulating_supply());
pledge_to_circulating_supply_ratio.min(params.one_over_k())
}
pub fn sigma(&self, params: &NodeRewardParams) -> U128 {
pub fn sigma(&self, params: &RewardParams) -> U128 {
// Ratio of a delegation to the the token circulating supply
let total_bond_to_circulating_supply_ratio =
self.total_bond_to_circulating_supply(params.circulating_supply());
total_bond_to_circulating_supply_ratio.min(params.one_over_k())
}
pub fn reward(&self, params: &NodeRewardParams) -> NodeRewardResult {
pub fn estimate_reward(
&self,
params: &RewardParams,
) -> Result<(u64, u64, u64), MixnetContractError> {
let total_node_reward = self.reward(params);
let operator_reward = self.operator_reward(params);
// TODO: This overestimates the reward by a lot, it should take a Uint128 and return estiamte for that
let delegators_reward = self.reward_delegation(self.total_delegation().amount, params);
Ok((
total_node_reward
.reward()
.checked_to_num::<u128>()
.unwrap_or_default()
.try_into()?,
operator_reward.try_into()?,
delegators_reward.try_into()?,
))
}
pub fn reward(&self, params: &RewardParams) -> NodeRewardResult {
let lambda = self.lambda(params);
let sigma = self.sigma(params);
let reward = params.performance()
* params.period_reward_pool()
* params.epoch_reward_pool()
* (sigma * params.omega()
+ params.alpha() * lambda * sigma * params.rewarded_set_size())
/ (ONE + params.alpha());
@@ -406,22 +426,22 @@ impl MixNodeBond {
}
}
pub fn node_profit(&self, params: &NodeRewardParams) -> U128 {
if self.reward(params).reward() < params.operator_cost() {
U128::from_num(0)
pub fn node_profit(&self, params: &RewardParams) -> U128 {
if self.reward(params).reward() < params.node.operator_cost() {
U128::from_num(0u128)
} else {
self.reward(params).reward() - params.operator_cost()
self.reward(params).reward() - params.node.operator_cost()
}
}
pub fn operator_reward(&self, params: &NodeRewardParams) -> u128 {
pub fn operator_reward(&self, params: &RewardParams) -> u128 {
let reward = self.reward(params);
let profit = if reward.reward < params.operator_cost() {
U128::from_num(0)
let profit = if reward.reward < params.node.operator_cost() {
U128::from_num(0u128)
} else {
reward.reward - params.operator_cost()
reward.reward - params.node.operator_cost()
};
let operator_base_reward = reward.reward.min(params.operator_cost());
let operator_base_reward = reward.reward.min(params.node.operator_cost());
let operator_reward = (self.profit_margin()
+ (ONE - self.profit_margin()) * reward.lambda / reward.sigma)
* profit;
@@ -440,7 +460,7 @@ impl MixNodeBond {
}
}
pub fn sigma_ratio(&self, params: &NodeRewardParams) -> U128 {
pub fn sigma_ratio(&self, params: &RewardParams) -> U128 {
if self.total_bond_to_circulating_supply(params.circulating_supply()) < params.one_over_k()
{
self.total_bond_to_circulating_supply(params.circulating_supply())
@@ -449,8 +469,13 @@ impl MixNodeBond {
}
}
pub fn reward_delegation(&self, delegation_amount: Uint128, params: &NodeRewardParams) -> u128 {
let reward_params = DelegatorRewardParams::new(self, *params);
pub fn reward_delegation(&self, delegation_amount: Uint128, params: &RewardParams) -> u128 {
let reward_params = DelegatorRewardParams::new(
self.sigma(params),
self.profit_margin(),
self.node_profit(params),
params.to_owned(),
);
reward_params.determine_delegation_reward(delegation_amount)
}
}
@@ -589,6 +614,7 @@ mod tests {
block_height: 100,
mix_node: mixnode_fixture(),
proxy: None,
accumulated_rewards: Uint128::zero(),
};
let mix2 = MixNodeBond {
@@ -599,6 +625,7 @@ mod tests {
block_height: 120,
mix_node: mixnode_fixture(),
proxy: None,
accumulated_rewards: Uint128::zero(),
};
let mix3 = MixNodeBond {
@@ -609,6 +636,7 @@ mod tests {
block_height: 120,
mix_node: mixnode_fixture(),
proxy: None,
accumulated_rewards: Uint128::zero(),
};
let mix4 = MixNodeBond {
@@ -619,6 +647,7 @@ mod tests {
block_height: 120,
mix_node: mixnode_fixture(),
proxy: None,
accumulated_rewards: Uint128::zero(),
};
let mix5 = MixNodeBond {
@@ -629,6 +658,7 @@ mod tests {
block_height: 120,
mix_node: mixnode_fixture(),
proxy: None,
accumulated_rewards: Uint128::zero(),
};
// summary:
@@ -1,12 +1,15 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::mixnode::NodeRewardParams;
use crate::reward_params::NodeRewardParams;
use crate::ContractStateParams;
use crate::{Gateway, IdentityKey, MixNode};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
type BlockHeight = u64;
type DelegateAddress = Vec<u8>;
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct InstantiateMsg {
pub rewarding_validator_address: String,
@@ -15,6 +18,19 @@ pub struct InstantiateMsg {
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ExecuteMsg {
ReconcileDelegations {},
CheckpointMixnodes {},
CompoundOperatorRewardOnBehalf {
owner: String,
},
CompoundDelegatorRewardOnBehalf {
owner: String,
mix_identity: IdentityKey,
},
CompoundOperatorReward {},
CompoundDelegatorReward {
mix_identity: IdentityKey,
},
BondMixnode {
mix_node: MixNode,
owner_signature: String,
@@ -50,11 +66,11 @@ pub enum ExecuteMsg {
// id of the current rewarding interval
interval_id: u32,
},
RewardNextMixDelegators {
mix_identity: IdentityKey,
// id of the current rewarding interval
interval_id: u32,
},
// RewardNextMixDelegators {
// mix_identity: IdentityKey,
// // id of the current rewarding interval
// interval_id: u32,
// },
DelegateToMixnodeOnBehalf {
mix_identity: IdentityKey,
delegate: String,
@@ -84,6 +100,7 @@ pub enum ExecuteMsg {
expected_active_set_size: u32,
},
AdvanceCurrentInterval {},
AdvanceCurrentEpoch {},
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
@@ -108,7 +125,7 @@ pub enum QueryMsg {
// gets all [paged] delegations in the entire network
// TODO: do we even want that?
GetAllNetworkDelegations {
start_after: Option<(IdentityKey, String)>,
start_after: Option<(IdentityKey, DelegateAddress, BlockHeight)>,
limit: Option<u32>,
},
// gets all [paged] delegations associated with particular mixnode
@@ -116,7 +133,7 @@ pub enum QueryMsg {
mix_identity: IdentityKey,
// since `start_after` is user-provided input, we can't use `Addr` as we
// can't guarantee it's validated.
start_after: Option<String>,
start_after: Option<(String, u64)>,
limit: Option<u32>,
},
// gets all [paged] delegations associated with particular delegator
@@ -154,6 +171,7 @@ pub enum QueryMsg {
GetCurrentRewardedSetHeight {},
GetCurrentInterval {},
GetRewardedSetRefreshBlocks {},
GetEpochsInInterval {},
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
@@ -0,0 +1,260 @@
use crate::{error::MixnetContractError, mixnode::StoredNodeRewardResult, ONE, U128};
use az::CheckedCast;
use cosmwasm_std::Uint128;
use network_defaults::DEFAULT_OPERATOR_INTERVAL_COST;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, JsonSchema, PartialEq, Serialize, Deserialize, Copy)]
pub struct NodeEpochRewards {
params: NodeRewardParams,
result: StoredNodeRewardResult,
epoch_id: u32,
}
impl NodeEpochRewards {
pub fn new(params: NodeRewardParams, result: StoredNodeRewardResult, epoch_id: u32) -> Self {
Self {
params,
result,
epoch_id,
}
}
pub fn epoch_id(&self) -> u32 {
self.epoch_id
}
pub fn sigma(&self) -> Uint128 {
self.result.sigma()
}
pub fn lambda(&self) -> Uint128 {
self.result.lambda()
}
pub fn params(&self) -> NodeRewardParams {
self.params
}
pub fn reward(&self) -> Uint128 {
self.result.reward()
}
pub fn operator_cost(&self) -> U128 {
U128::from_num(self.params.uptime.u128() / 100u128 * DEFAULT_OPERATOR_INTERVAL_COST as u128)
}
pub fn node_profit(&self) -> U128 {
let reward = U128::from_num(self.reward().u128());
if reward < self.operator_cost() {
U128::from_num(0u128)
} else {
reward - self.operator_cost()
}
}
pub fn operator_reward(&self, profit_margin: U128) -> Result<Uint128, MixnetContractError> {
let reward = self.node_profit();
let operator_base_reward = reward.min(self.operator_cost());
let operator_reward = (profit_margin
+ (ONE - profit_margin) * U128::from_num(self.lambda().u128())
/ U128::from_num(self.sigma().u128()))
* reward;
let reward = (operator_reward + operator_base_reward).max(U128::from_num(0u128));
if let Some(int_reward) = reward.checked_cast() {
Ok(Uint128::new(int_reward))
} else {
Err(MixnetContractError::CastError)
}
}
pub fn delegation_reward(
&self,
delegation_amount: Uint128,
profit_margin: U128,
epoch_reward_params: EpochRewardParams,
) -> Result<Uint128, MixnetContractError> {
// change all values into their fixed representations
let delegation_amount = U128::from_num(delegation_amount.u128());
let circulating_supply = U128::from_num(epoch_reward_params.circulating_supply());
let scaled_delegation_amount = delegation_amount / circulating_supply;
let delegator_reward = (ONE - profit_margin) * scaled_delegation_amount
/ U128::from_num(self.sigma().u128())
* self.node_profit();
let reward = delegator_reward.max(U128::ZERO);
if let Some(int_reward) = reward.checked_cast() {
Ok(Uint128::new(int_reward))
} else {
Err(MixnetContractError::CastError)
}
}
}
#[derive(Debug, Clone, JsonSchema, PartialEq, Serialize, Deserialize, Copy)]
pub struct EpochRewardParams {
epoch_reward_pool: Uint128,
rewarded_set_size: Uint128,
active_set_size: Uint128,
circulating_supply: Uint128,
sybil_resistance_percent: u8,
active_set_work_factor: u8,
}
impl EpochRewardParams {
pub fn new(
epoch_reward_pool: u128,
rewarded_set_size: u128,
active_set_size: u128,
circulating_supply: u128,
sybil_resistance_percent: u8,
active_set_work_factor: u8,
) -> EpochRewardParams {
EpochRewardParams {
epoch_reward_pool: Uint128::new(epoch_reward_pool),
rewarded_set_size: Uint128::new(rewarded_set_size),
active_set_size: Uint128::new(active_set_size),
circulating_supply: Uint128::new(circulating_supply),
sybil_resistance_percent,
active_set_work_factor,
}
}
// 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 fn new_empty() -> Self {
EpochRewardParams {
epoch_reward_pool: Uint128::new(0),
circulating_supply: Uint128::new(0),
sybil_resistance_percent: 0,
rewarded_set_size: Uint128::new(0),
active_set_size: Uint128::new(0),
active_set_work_factor: 0,
}
}
pub fn rewarded_set_size(&self) -> u128 {
self.rewarded_set_size.u128()
}
pub fn active_set_size(&self) -> u128 {
self.active_set_size.u128()
}
pub fn circulating_supply(&self) -> u128 {
self.circulating_supply.u128()
}
pub fn epoch_reward_pool(&self) -> u128 {
self.epoch_reward_pool.u128()
}
}
#[derive(Debug, Clone, JsonSchema, PartialEq, Serialize, Deserialize, Copy)]
pub struct NodeRewardParams {
reward_blockstamp: u64,
uptime: Uint128,
in_active_set: bool,
}
impl NodeRewardParams {
pub fn new(reward_blockstamp: u64, uptime: u128, in_active_set: bool) -> NodeRewardParams {
NodeRewardParams {
reward_blockstamp,
uptime: Uint128::new(uptime),
in_active_set,
}
}
pub fn operator_cost(&self) -> U128 {
U128::from_num(self.uptime.u128() / 100u128 * DEFAULT_OPERATOR_INTERVAL_COST as u128)
}
pub fn uptime(&self) -> u128 {
self.uptime.u128()
}
pub fn set_reward_blockstamp(&mut self, blockstamp: u64) {
self.reward_blockstamp = blockstamp;
}
}
#[derive(Debug, Clone, JsonSchema, PartialEq, Serialize, Deserialize, Copy)]
pub struct RewardParams {
pub epoch: EpochRewardParams,
pub node: NodeRewardParams,
}
impl RewardParams {
pub fn new(epoch: EpochRewardParams, node: NodeRewardParams) -> RewardParams {
RewardParams { epoch, node }
}
pub fn omega(&self) -> U128 {
// As per keybase://chat/nymtech#tokeneconomics/1179
let denom = self.active_set_work_factor() * U128::from_num(self.rewarded_set_size())
- (self.active_set_work_factor() - ONE) * U128::from_num(self.idle_nodes().u128());
if self.in_active_set() {
// work_active = factor / (factor * self.network.k[month] - (factor - 1) * idle_nodes)
self.active_set_work_factor() / denom * self.rewarded_set_size()
} else {
// work_idle = 1 / (factor * self.network.k[month] - (factor - 1) * idle_nodes)
ONE / denom * self.rewarded_set_size()
}
}
pub fn idle_nodes(&self) -> Uint128 {
self.epoch.rewarded_set_size - self.epoch.active_set_size
}
pub fn active_set_work_factor(&self) -> U128 {
U128::from_num(self.epoch.active_set_work_factor)
}
pub fn in_active_set(&self) -> bool {
self.node.in_active_set
}
pub fn performance(&self) -> U128 {
U128::from_num(self.node.uptime.u128()) / U128::from_num(100)
}
pub fn set_reward_blockstamp(&mut self, blockstamp: u64) {
self.node.reward_blockstamp = blockstamp;
}
pub fn epoch_reward_pool(&self) -> u128 {
self.epoch.epoch_reward_pool.u128()
}
pub fn rewarded_set_size(&self) -> u128 {
self.epoch.rewarded_set_size.u128()
}
pub fn circulating_supply(&self) -> u128 {
self.epoch.circulating_supply.u128()
}
pub fn reward_blockstamp(&self) -> u64 {
self.node.reward_blockstamp
}
pub fn uptime(&self) -> u128 {
self.node.uptime.u128()
}
pub fn one_over_k(&self) -> U128 {
ONE / U128::from_num(self.epoch.rewarded_set_size.u128())
}
pub fn alpha(&self) -> U128 {
U128::from_num(self.epoch.sybil_resistance_percent) / U128::from_num(100)
}
}
@@ -73,8 +73,7 @@ impl Display for ContractStateParams {
#[derive(Default, Debug, Serialize, Deserialize, PartialEq)]
pub struct RewardingResult {
pub operator_reward: Uint128,
pub total_delegator_reward: Uint128,
pub node_reward: Uint128,
}
#[derive(Debug, Serialize, Deserialize)]
@@ -124,7 +123,7 @@ pub type IdentityKey = String;
pub type IdentityKeyRef<'a> = &'a str;
pub type SphinxKey = String;
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)]
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, JsonSchema)]
pub struct PagedRewardedSetResponse {
pub identities: Vec<(IdentityKey, RewardedSetNodeStatus)>,
pub start_next_after: Option<IdentityKey>,
+2 -1
View File
@@ -179,7 +179,8 @@ pub const VALIDATOR_API_VERSION: &str = "v1";
// REWARDING
/// 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$
// pub const DEFAULT_OPERATOR_INTERVAL_COST: u64 = 40_000_000; // 40$/(30 days) at 1 Nym == 1$
pub const DEFAULT_OPERATOR_INTERVAL_COST: u64 = 55_556; // 40$/1hr 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;
+21 -9
View File
@@ -218,9 +218,9 @@ dependencies = [
[[package]]
name = "cosmwasm-crypto"
version = "1.0.0-beta4"
version = "1.0.0-beta5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f903ebbabc0d4880dbc76148efb8be8fc29fa4bf294c440c3d70da1c8bcafff7"
checksum = "8904127a5b9e325ef5d6b2b3f997dcd74943cd35097139b1a4d15b1b6bccae66"
dependencies = [
"digest 0.9.0",
"ed25519-zebra",
@@ -231,9 +231,9 @@ dependencies = [
[[package]]
name = "cosmwasm-derive"
version = "1.0.0-beta4"
version = "1.0.0-beta5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "832bebef577ecb394603de8e2bf0de429b74aa364e17dec18e15ce37e71b0cae"
checksum = "a14364ac4d9d085867929d0cf3e94b1d2100121ce02c33c72961406830002613"
dependencies = [
"syn",
]
@@ -250,9 +250,9 @@ dependencies = [
[[package]]
name = "cosmwasm-std"
version = "1.0.0-beta4"
version = "1.0.0-beta5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6238c45840cc9de5a39f0f619e3a4f7c38c5d2c6ac9e3e4d72751ee045e6d7da"
checksum = "e2ece12e5bbde434b93937d7b2107e6291f11d69ffa72398c50e8bab41d451d3"
dependencies = [
"base64",
"cosmwasm-crypto",
@@ -368,6 +368,17 @@ dependencies = [
"serde",
]
[[package]]
name = "cw-storage-plus"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c087ff98fb0475db4c2b5298a5fd12b2848d2854b39d1115d930ee6da24d1eed"
dependencies = [
"cosmwasm-std",
"schemars",
"serde",
]
[[package]]
name = "der"
version = "0.4.5"
@@ -804,13 +815,14 @@ checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
name = "mixnet-contract"
version = "0.1.0"
dependencies = [
"az",
"bs58",
"config",
"cosmwasm-schema",
"cosmwasm-std",
"cosmwasm-storage",
"crypto",
"cw-storage-plus",
"cw-storage-plus 0.12.1",
"fixed",
"mixnet-contract-common",
"rand",
@@ -1512,7 +1524,7 @@ version = "0.1.0"
dependencies = [
"config",
"cosmwasm-std",
"cw-storage-plus",
"cw-storage-plus 0.12.1",
"mixnet-contract-common",
"schemars",
"serde",
@@ -1526,7 +1538,7 @@ version = "0.1.0"
dependencies = [
"config",
"cosmwasm-std",
"cw-storage-plus",
"cw-storage-plus 0.11.1",
"mixnet-contract-common",
"schemars",
"serde",
+2 -1
View File
@@ -22,8 +22,9 @@ config = { path = "../../common/config"}
cosmwasm-std = "1.0.0-beta3"
cosmwasm-storage = "1.0.0-beta3"
cw-storage-plus = "0.11.1"
cw-storage-plus = "0.12.1"
az = "1.2.0"
bs58 = "0.4.0"
schemars = "0.8"
serde = { version = "1.0.103", default-features = false, features = ["derive"] }
+1
View File
@@ -16,5 +16,6 @@ pub const ACTIVE_SET_WORK_FACTOR: u8 = 10;
// and we can't change this easily to `Duration`, because then the entire rewarded set storage
// would be messed up... (as we look up stuff "by blocks")
pub const REWARDED_SET_REFRESH_BLOCKS: u64 = 720; // with blocktime being approximately 5s, it should be roughly 1h
pub const EPOCHS_IN_INTERVAL: u64 = 720; // Hours in a month
pub const REWARDING_INTERVAL_LENGTH: Duration = Duration::from_secs(60 * 60 * 720); // 720h, i.e. 30 days
+60 -13
View File
@@ -86,7 +86,7 @@ pub fn instantiate(
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, &rewarding_interval)?;
interval_storage::save_interval(deps.storage, &rewarding_interval)?;
interval_storage::CURRENT_REWARDED_SET_HEIGHT.save(deps.storage, &env.block.height)?;
Ok(Response::default())
@@ -112,7 +112,7 @@ pub fn execute(
owner_signature,
),
ExecuteMsg::UnbondMixnode {} => {
crate::mixnodes::transactions::try_remove_mixnode(deps, info)
crate::mixnodes::transactions::try_remove_mixnode(env, deps, info)
}
ExecuteMsg::UpdateMixnodeConfig {
profit_margin_percent,
@@ -168,19 +168,20 @@ pub fn execute(
ExecuteMsg::UndelegateFromMixnode { mix_identity } => {
crate::delegations::transactions::try_remove_delegation_from_mixnode(
deps,
env,
info,
mix_identity,
)
}
ExecuteMsg::RewardNextMixDelegators {
mix_identity,
interval_id,
} => crate::rewards::transactions::try_reward_next_mixnode_delegators(
deps,
info,
mix_identity,
interval_id,
),
// ExecuteMsg::RewardNextMixDelegators {
// mix_identity,
// interval_id,
// } => crate::rewards::transactions::try_reward_next_mixnode_delegators(
// deps,
// info,
// mix_identity,
// interval_id,
// ),
ExecuteMsg::DelegateToMixnodeOnBehalf {
mix_identity,
delegate,
@@ -196,6 +197,7 @@ pub fn execute(
delegate,
} => crate::delegations::transactions::try_remove_delegation_from_mixnode_on_behalf(
deps,
env,
info,
mix_identity,
delegate,
@@ -213,7 +215,7 @@ pub fn execute(
owner_signature,
),
ExecuteMsg::UnbondMixnodeOnBehalf { owner } => {
crate::mixnodes::transactions::try_remove_mixnode_on_behalf(deps, info, owner)
crate::mixnodes::transactions::try_remove_mixnode_on_behalf(env, deps, info, owner)
}
ExecuteMsg::BondGatewayOnBehalf {
gateway,
@@ -243,6 +245,45 @@ pub fn execute(
ExecuteMsg::AdvanceCurrentInterval {} => {
crate::interval::transactions::try_advance_interval(env, deps.storage)
}
ExecuteMsg::AdvanceCurrentEpoch {} => {
crate::interval::transactions::try_advance_epoch(env, deps.storage)
}
ExecuteMsg::CompoundDelegatorReward { mix_identity } => {
crate::rewards::transactions::try_compound_delegator_reward(
deps,
env,
info,
mix_identity,
)
}
ExecuteMsg::CompoundOperatorReward {} => {
crate::rewards::transactions::try_compound_operator_reward(deps, env, info)
}
ExecuteMsg::CompoundDelegatorRewardOnBehalf {
owner,
mix_identity,
} => crate::rewards::transactions::try_compound_delegator_reward_on_behalf(
deps,
env,
info,
owner,
mix_identity,
),
ExecuteMsg::CompoundOperatorRewardOnBehalf { owner } => {
crate::rewards::transactions::try_compound_operator_reward_on_behalf(
deps, env, info, owner,
)
}
ExecuteMsg::ReconcileDelegations {} => {
crate::delegations::transactions::try_reconcile_all_delegation_events(deps, info)
}
ExecuteMsg::CheckpointMixnodes {} => {
crate::mixnodes::transactions::try_checkpoint_mixnodes(
deps.storage,
env.block.height,
info,
)
}
}
}
@@ -288,7 +329,12 @@ pub fn query(deps: Deps<'_>, env: Env, msg: QueryMsg) -> Result<QueryResponse, C
QueryMsg::GetDelegationDetails {
mix_identity,
delegator,
} => to_binary(&query_mixnode_delegation(deps, mix_identity, delegator)?),
} => to_binary(&query_mixnode_delegation(
deps.storage,
deps.api,
mix_identity,
delegator,
)?),
QueryMsg::GetRewardPool {} => to_binary(&query_reward_pool(deps)?),
QueryMsg::GetCirculatingSupply {} => to_binary(&query_circulating_supply(deps)?),
QueryMsg::GetIntervalRewardPercent {} => to_binary(&INTERVAL_REWARD_PERCENT),
@@ -321,6 +367,7 @@ pub fn query(deps: Deps<'_>, env: Env, msg: QueryMsg) -> Result<QueryResponse, C
QueryMsg::GetRewardedSetRefreshBlocks {} => {
to_binary(&query_rewarded_set_refresh_minimum_blocks())
}
QueryMsg::GetEpochsInInterval {} => to_binary(&crate::constants::EPOCHS_IN_INTERVAL),
};
Ok(query_res?)
+64 -49
View File
@@ -3,9 +3,9 @@
use super::storage;
use crate::error::ContractError;
use cosmwasm_std::Deps;
use cosmwasm_std::Order;
use cosmwasm_std::StdResult;
use cosmwasm_std::{Api, Deps, Storage};
use cw_storage_plus::{Bound, PrimaryKey};
use mixnet_contract_common::{
Delegation, IdentityKey, PagedAllDelegationsResponse, PagedDelegatorDelegationsResponse,
@@ -14,16 +14,14 @@ use mixnet_contract_common::{
pub(crate) fn query_all_network_delegations_paged(
deps: Deps<'_>,
start_after: Option<(IdentityKey, String)>,
start_after: Option<(IdentityKey, Vec<u8>, u64)>,
limit: Option<u32>,
) -> StdResult<PagedAllDelegationsResponse> {
let limit = limit
.unwrap_or(storage::DELEGATION_PAGE_DEFAULT_LIMIT)
.min(storage::DELEGATION_PAGE_MAX_LIMIT) as usize;
let start = start_after
.map(|start| start.joined_key())
.map(Bound::exclusive);
let start = start_after.map(Bound::exclusive);
let delegations = storage::delegations()
.range(deps.storage, start, None, Order::Ascending)
@@ -52,8 +50,9 @@ pub(crate) fn query_delegator_delegations_paged(
let limit = limit
.unwrap_or(storage::DELEGATION_PAGE_DEFAULT_LIMIT)
.min(storage::DELEGATION_PAGE_MAX_LIMIT) as usize;
let start = start_after
.map(|mix_identity| Bound::Exclusive((mix_identity, validated_owner.clone()).joined_key()));
let start = start_after.map(|mix_identity| {
Bound::ExclusiveRaw((mix_identity, validated_owner.clone()).joined_key())
});
let delegations = storage::delegations()
.idx
@@ -76,35 +75,47 @@ pub(crate) fn query_delegator_delegations_paged(
// queries for delegation value of given address for particular node
pub(crate) fn query_mixnode_delegation(
deps: Deps<'_>,
storage: &dyn Storage,
api: &dyn Api,
mix_identity: IdentityKey,
delegator: String,
) -> Result<Delegation, ContractError> {
let validated_delegator = deps.api.addr_validate(&delegator)?;
let storage_key = (mix_identity.clone(), validated_delegator.clone()).joined_key();
) -> Result<Vec<Delegation>, ContractError> {
let validated_delegator = api.addr_validate(&delegator)?;
let storage_key = (
mix_identity.clone(),
validated_delegator.as_bytes().to_vec(),
);
storage::delegations()
.may_load(deps.storage, storage_key)?
.ok_or(ContractError::NoMixnodeDelegationFound {
let delegations = storage::delegations()
.prefix(storage_key)
.range(storage, None, None, Order::Ascending)
.filter_map(|d| d.ok())
.map(|r| r.1)
.collect::<Vec<Delegation>>();
if delegations.is_empty() {
Err(ContractError::NoMixnodeDelegationFound {
identity: mix_identity,
address: validated_delegator,
address: delegator,
})
} else {
Ok(delegations)
}
}
pub(crate) fn query_mixnode_delegations_paged(
deps: Deps<'_>,
mix_identity: IdentityKey,
start_after: Option<String>,
start_after: Option<(String, u64)>,
limit: Option<u32>,
) -> StdResult<PagedMixDelegationsResponse> {
let limit = limit
.unwrap_or(storage::DELEGATION_PAGE_DEFAULT_LIMIT)
.min(storage::DELEGATION_PAGE_MAX_LIMIT) as usize;
let start = start_after
.map(|addr| deps.api.addr_validate(&addr))
.transpose()?
.map(|addr| Bound::Exclusive((mix_identity.clone(), addr).joined_key()));
let start = start_after.map(|(addr, height)| {
Bound::ExclusiveRaw((mix_identity.clone(), addr.as_bytes(), height).joined_key())
});
let delegations = storage::delegations()
.idx
@@ -115,7 +126,9 @@ pub(crate) fn query_mixnode_delegations_paged(
.map(|record| record.map(|r| r.1))
.collect::<StdResult<Vec<_>>>()?;
let start_next_after = delegations.last().map(|delegation| delegation.owner());
let start_next_after = delegations
.last()
.map(|delegation| (delegation.owner(), delegation.block_height()));
Ok(PagedMixDelegationsResponse::new(
delegations,
@@ -245,25 +258,26 @@ pub(crate) mod tests {
Option::from(per_page),
)
.unwrap();
// println!("{:?}", page1);
let start_after = page1.start_next_after.unwrap();
assert_eq!(2, page1.delegations.len());
assert_eq!("200".to_string(), page1.start_next_after.unwrap());
assert_eq!(("200".to_string(), 12345), start_after);
// retrieving the next page should start after the last key on this page
let start_after = "200".to_string();
let page2 = query_mixnode_delegations_paged(
deps.as_ref(),
node_identity.clone(),
Option::from(start_after),
Option::from(start_after.clone()),
Option::from(per_page),
)
.unwrap();
// println!("{:?}", page2);
assert_eq!(1, page2.delegations.len());
// save another one
test_helpers::save_dummy_delegation(&mut deps.storage, &node_identity, "400");
let start_after = "200".to_string();
let page2 = query_mixnode_delegations_paged(
deps.as_ref(),
node_identity,
@@ -271,6 +285,7 @@ pub(crate) mod tests {
Option::from(per_page),
)
.unwrap();
// println!("{:?}", page2);
// now we have 2 pages, with 2 results on the second page
assert_eq!(2, page2.delegations.len());
@@ -408,16 +423,17 @@ pub(crate) mod tests {
);
storage::delegations()
.save(
deps.as_mut().storage,
delegation.storage_key().joined_key(),
&delegation,
)
.save(deps.as_mut().storage, delegation.storage_key(), &delegation)
.unwrap();
assert_eq!(
Ok(delegation),
query_mixnode_delegation(deps.as_ref(), node_identity, delegation_owner.to_string())
Ok(vec![delegation]),
query_mixnode_delegation(
&deps.storage,
&deps.api,
node_identity,
delegation_owner.to_string()
)
)
}
@@ -433,10 +449,11 @@ pub(crate) mod tests {
assert_eq!(
Err(ContractError::NoMixnodeDelegationFound {
identity: node_identity1.clone(),
address: delegation_owner1.clone(),
address: delegation_owner1.to_string(),
}),
query_mixnode_delegation(
deps.as_ref(),
&deps.storage,
&deps.api,
node_identity1.clone(),
delegation_owner1.to_string()
)
@@ -452,20 +469,17 @@ pub(crate) mod tests {
);
storage::delegations()
.save(
deps.as_mut().storage,
delegation.storage_key().joined_key(),
&delegation,
)
.save(deps.as_mut().storage, delegation.storage_key(), &delegation)
.unwrap();
assert_eq!(
Err(ContractError::NoMixnodeDelegationFound {
identity: node_identity1.clone(),
address: delegation_owner1.clone(),
address: delegation_owner1.to_string(),
}),
query_mixnode_delegation(
deps.as_ref(),
&deps.storage,
&deps.api,
node_identity1.clone(),
delegation_owner1.to_string()
)
@@ -481,19 +495,20 @@ pub(crate) mod tests {
);
storage::delegations()
.save(
deps.as_mut().storage,
delegation.storage_key().joined_key(),
&delegation,
)
.save(deps.as_mut().storage, delegation.storage_key(), &delegation)
.unwrap();
assert_eq!(
Err(ContractError::NoMixnodeDelegationFound {
identity: node_identity1.clone(),
address: Addr::unchecked(delegation_owner1.clone())
address: delegation_owner1.to_string()
}),
query_mixnode_delegation(deps.as_ref(), node_identity1, delegation_owner1.to_string())
query_mixnode_delegation(
&deps.storage,
&deps.api,
node_identity1,
delegation_owner1.to_string()
)
)
}
+24 -9
View File
@@ -1,20 +1,27 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cw_storage_plus::{Index, IndexList, IndexedMap, MultiIndex};
use mixnet_contract_common::{Addr, Delegation, IdentityKey};
use cw_storage_plus::{Index, IndexList, IndexedMap, Map, MultiIndex};
use mixnet_contract_common::{mixnode::DelegationEvent, Addr, Delegation, IdentityKey};
// storage prefixes
const DELEGATION_PK_NAMESPACE: &str = "dl";
const DELEGATION_OWNER_IDX_NAMESPACE: &str = "dlo";
const DELEGATION_MIXNODE_IDX_NAMESPACE: &str = "dlm";
pub const PENDING_DELEGATION_EVENTS: Map<
(BlockHeight, IdentityKey, OwnerAddress),
DelegationEvent,
> = Map::new("pend");
// paged retrieval limits for all queries and transactions
pub(crate) const DELEGATION_PAGE_MAX_LIMIT: u32 = 500;
pub(crate) const DELEGATION_PAGE_DEFAULT_LIMIT: u32 = 250;
// It's a composite key on node's identity and delegator address
type PrimaryKey = Vec<u8>;
type BlockHeight = u64;
type OwnerAddress = Vec<u8>;
// It's a composite key on node's identity, delegator address, and block height
type PrimaryKey = (IdentityKey, OwnerAddress, BlockHeight);
pub(crate) struct DelegationIndex<'a> {
pub(crate) owner: MultiIndex<'a, Addr, Delegation>,
@@ -73,7 +80,6 @@ mod tests {
use config::defaults::DENOM;
use cosmwasm_std::testing::mock_env;
use cosmwasm_std::{coin, Order};
use cw_storage_plus::PrimaryKey;
use mixnet_contract_common::Delegation;
#[test]
@@ -94,7 +100,7 @@ mod tests {
storage::delegations()
.save(
&mut deps.storage,
(node_identity, delegation_owner.clone()).joined_key(),
(node_identity, delegation_owner.as_bytes().to_vec(), 0),
&dummy_data,
)
.unwrap();
@@ -124,7 +130,8 @@ mod tests {
assert!(test_helpers::read_delegation(
deps.as_ref().storage,
&node_identity1,
&delegation_owner1
delegation_owner1.as_bytes(),
mock_env().block.height
)
.is_none());
@@ -139,7 +146,11 @@ mod tests {
storage::delegations()
.save(
&mut deps.storage,
(node_identity1.clone(), delegation_owner1.clone()).joined_key(),
(
node_identity1.clone(),
delegation_owner1.as_bytes().to_vec(),
0,
),
&dummy_data,
)
.unwrap();
@@ -163,7 +174,11 @@ mod tests {
storage::delegations()
.save(
&mut deps.storage,
(node_identity1.clone(), delegation_owner2).joined_key(),
(
node_identity1.clone(),
delegation_owner2.as_bytes().to_vec(),
0,
),
&dummy_data,
)
.unwrap();
+462 -168
View File
@@ -1,17 +1,69 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use super::storage;
use super::storage::{self, PENDING_DELEGATION_EVENTS};
use crate::error::ContractError;
use crate::mixnet_contract_settings::storage as mixnet_params_storage;
use crate::mixnodes::storage as mixnodes_storage;
use crate::support::helpers::generate_storage_key;
use config::defaults::DENOM;
use cosmwasm_std::{coins, wasm_execute, Addr, BankMsg, Coin, DepsMut, Env, MessageInfo, Response};
use cw_storage_plus::PrimaryKey;
use mixnet_contract_common::events::{new_delegation_event, new_undelegation_event};
use cosmwasm_std::{
coins, wasm_execute, Addr, Api, BankMsg, Coin, DepsMut, Env, Event, MessageInfo, Order,
Response, Storage, Uint128, WasmMsg,
};
use mixnet_contract_common::events::{
new_pending_delegation_event, new_pending_undelegation_event, new_undelegation_event,
};
use mixnet_contract_common::mixnode::{DelegationEvent, PendingUndelegate};
use mixnet_contract_common::{Delegation, IdentityKey};
use vesting_contract_common::messages::ExecuteMsg as VestingContractExecuteMsg;
use vesting_contract_common::one_ucoin;
pub fn try_reconcile_all_delegation_events(
deps: DepsMut<'_>,
info: MessageInfo,
) -> 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);
}
_try_reconcile_all_delegation_events(deps.storage, deps.api)
}
// TODO: Error handling?
pub(crate) fn _try_reconcile_all_delegation_events(
storage: &mut dyn Storage,
api: &dyn Api,
) -> Result<Response, ContractError> {
let pending_delegation_events = PENDING_DELEGATION_EVENTS
.range(storage, None, None, Order::Ascending)
.filter_map(|r| r.ok())
.collect::<Vec<((u64, String, Vec<u8>), DelegationEvent)>>();
let mut response = Response::new();
for (key, delegation_event) in pending_delegation_events {
match delegation_event {
DelegationEvent::Delegate(delegation) => {
let event = try_reconcile_delegation(storage, delegation)?;
response = response.add_event(event);
}
DelegationEvent::Undelegate(pending_undelegate) => {
let undelegate_response =
try_reconcile_undelegation(storage, api, &pending_undelegate)?;
response = response.add_event(undelegate_response.event);
response = response.add_message(undelegate_response.bank_msg);
if let Some(msg) = undelegate_response.wasm_msg {
response = response.add_message(msg);
}
}
}
PENDING_DELEGATION_EVENTS.remove(storage, key);
}
Ok(response)
}
fn validate_delegation_stake(mut delegation: Vec<Coin>) -> Result<Coin, ContractError> {
// check if anything was put as delegation
if delegation.is_empty() {
@@ -44,7 +96,15 @@ pub(crate) fn try_delegate_to_mixnode(
// check if the delegation contains any funds of the appropriate denomination
let amount = validate_delegation_stake(info.funds)?;
_try_delegate_to_mixnode(deps, env, mix_identity, info.sender.as_str(), amount, None)
_try_delegate_to_mixnode(
deps.storage,
deps.api,
env.block.height,
&mix_identity,
info.sender.as_str(),
amount,
None,
)
}
pub(crate) fn try_delegate_to_mixnode_on_behalf(
@@ -58,173 +118,277 @@ pub(crate) fn try_delegate_to_mixnode_on_behalf(
let amount = validate_delegation_stake(info.funds)?;
_try_delegate_to_mixnode(
deps,
env,
mix_identity,
deps.storage,
deps.api,
env.block.height,
&mix_identity,
&delegate,
amount,
Some(info.sender),
)
}
pub(crate) fn _try_delegate_to_mixnode(
deps: DepsMut<'_>,
env: Env,
mix_identity: IdentityKey,
delegate: &str,
amount: Coin,
proxy: Option<Addr>,
) -> Result<Response, ContractError> {
let delegate = deps.api.addr_validate(delegate)?;
// check if the target node actually exists
if mixnodes_storage::mixnodes()
.may_load(deps.storage, &mix_identity)?
.is_none()
{
return Err(ContractError::MixNodeBondNotFound {
identity: mix_identity,
});
}
let maybe_proxy_storage = generate_storage_key(&delegate, proxy.as_ref());
let storage_key = (mix_identity.clone(), maybe_proxy_storage).joined_key();
pub(crate) fn try_reconcile_delegation(
storage: &mut dyn Storage,
delegation: Delegation,
) -> Result<Event, ContractError> {
// update total_delegation of this node
mixnodes_storage::TOTAL_DELEGATION.update::<_, ContractError>(
deps.storage,
&mix_identity,
storage,
&delegation.node_identity,
|total_delegation| {
// since we know that the target node exists and because the total_delegation bucket
// entry is created whenever the node itself is added, the unwrap here is fine
// as the entry MUST exist
Ok(total_delegation.unwrap() + amount.amount)
Ok(total_delegation.unwrap() + delegation.amount.amount)
},
)?;
// update [or create new] delegation of this delegator
// update [or create new] pending delegation of this delegator
storage::delegations().update::<_, ContractError>(
deps.storage,
storage_key,
storage,
delegation.storage_key(),
|existing_delegation| {
Ok(match existing_delegation {
Some(mut existing_delegation) => {
existing_delegation.increment_amount(amount.amount, Some(env.block.height));
existing_delegation
.increment_amount(delegation.amount.amount, Some(delegation.block_height));
existing_delegation
}
None => Delegation::new(
delegate.to_owned(),
mix_identity.clone(),
amount.clone(),
env.block.height,
proxy.clone(),
),
None => delegation.clone(),
})
},
)?;
Ok(Response::new().add_event(new_delegation_event(
Ok(new_pending_delegation_event(
&delegation.owner,
&delegation.proxy,
&delegation.amount,
&delegation.node_identity,
))
}
pub(crate) fn _try_delegate_to_mixnode(
storage: &mut dyn Storage,
api: &dyn Api,
block_height: u64,
mix_identity: &str,
delegate: &str,
amount: Coin,
proxy: Option<Addr>,
) -> Result<Response, ContractError> {
let delegate = api.addr_validate(delegate)?;
// check if the target node actually exists
if mixnodes_storage::mixnodes()
.may_load(storage, mix_identity)?
.is_none()
{
return Err(ContractError::MixNodeBondNotFound {
identity: mix_identity.to_string(),
});
}
let maybe_proxy_storage = generate_storage_key(&delegate, proxy.as_ref());
let storage_key = (block_height, mix_identity.to_string(), maybe_proxy_storage);
storage::PENDING_DELEGATION_EVENTS.save(
storage,
storage_key,
&DelegationEvent::Delegate(Delegation::new(
delegate.to_owned(),
mix_identity.to_string(),
amount.clone(),
block_height,
proxy.clone(),
)),
)?;
Ok(Response::new().add_event(new_pending_delegation_event(
&delegate,
&proxy,
&amount,
&mix_identity,
mix_identity,
)))
}
pub(crate) fn try_remove_delegation_from_mixnode(
deps: DepsMut<'_>,
env: Env,
info: MessageInfo,
mix_identity: IdentityKey,
) -> Result<Response, ContractError> {
_try_remove_delegation_from_mixnode(deps, mix_identity, info.sender.as_str(), None)
_try_remove_delegation_from_mixnode(deps, env, mix_identity, info.sender.as_str(), None)
}
pub(crate) fn try_remove_delegation_from_mixnode_on_behalf(
deps: DepsMut<'_>,
env: Env,
info: MessageInfo,
mix_identity: IdentityKey,
delegate: String,
) -> Result<Response, ContractError> {
_try_remove_delegation_from_mixnode(deps, mix_identity, &delegate, Some(info.sender))
_try_remove_delegation_from_mixnode(deps, env, mix_identity, &delegate, Some(info.sender))
}
pub struct ReconcileUndelegateResponse {
bank_msg: BankMsg,
wasm_msg: Option<WasmMsg>,
event: Event,
}
pub(crate) fn try_reconcile_undelegation(
storage: &mut dyn Storage,
api: &dyn Api,
pending_undelegate: &PendingUndelegate,
) -> Result<ReconcileUndelegateResponse, ContractError> {
let delegation_map = storage::delegations();
let maybe_proxy_storage = generate_storage_key(
&pending_undelegate.delegate(),
pending_undelegate.proxy().as_ref(),
);
let storage_key = (
pending_undelegate.mix_identity(),
maybe_proxy_storage.clone(),
);
let any_delegations = delegation_map
.prefix(storage_key.clone())
.keys(storage, None, None, cosmwasm_std::Order::Ascending)
.filter_map(|v| v.ok())
.next()
.is_some();
if !any_delegations {
return Err(ContractError::NoMixnodeDelegationFound {
identity: pending_undelegate.mix_identity(),
address: pending_undelegate.delegate().to_string(),
});
}
let reward = crate::rewards::transactions::_try_compound_delegator_reward(
pending_undelegate.block_height(),
api,
storage,
pending_undelegate.delegate().as_str(),
&pending_undelegate.mix_identity(),
None,
)?;
// Might want to introduce paging here
let delegation_heights = delegation_map
.prefix(storage_key)
.keys(storage, None, None, cosmwasm_std::Order::Ascending)
.filter_map(|v| v.ok())
.collect::<Vec<u64>>();
if delegation_heights.is_empty() {
return Err(ContractError::NoMixnodeDelegationFound {
identity: pending_undelegate.mix_identity(),
address: pending_undelegate.delegate().to_string(),
});
}
let mut total_delegation = Uint128::zero();
if crate::mixnodes::storage::mixnodes()
.may_load(storage, &pending_undelegate.mix_identity())?
.is_none()
{
// Since the mixnode is no longer bonded the reward did not compound and we need to manually add it to the total
total_delegation = reward;
}
for h in delegation_heights {
let storage_key = (
pending_undelegate.mix_identity(),
maybe_proxy_storage.clone(),
h,
);
let delegation = delegation_map.load(storage, storage_key.clone())?;
total_delegation += delegation.amount.amount;
delegation_map.replace(storage, storage_key, None, Some(&delegation))?;
}
let bank_msg = BankMsg::Send {
to_address: pending_undelegate
.proxy()
.as_ref()
.unwrap_or(&pending_undelegate.delegate())
.to_string(),
amount: coins(total_delegation.u128(), DENOM),
};
mixnodes_storage::TOTAL_DELEGATION.update::<_, ContractError>(
storage,
&pending_undelegate.mix_identity(),
|total_node_delegation| {
// the first unwrap is fine because the delegation information MUST exist, otherwise we would
// have never gotten here in the first place
// the second unwrap is also fine because we should NEVER underflow here,
// if we do, it means we have some serious error in our logic
Ok(total_node_delegation
.unwrap()
.checked_sub(total_delegation)
.unwrap())
},
)?;
let mut wasm_msg = None;
if let Some(proxy) = &pending_undelegate.proxy() {
let msg = Some(VestingContractExecuteMsg::TrackUndelegation {
owner: pending_undelegate.delegate().as_str().to_string(),
mix_identity: pending_undelegate.mix_identity(),
amount: Coin::new(total_delegation.u128(), DENOM),
});
wasm_msg = Some(wasm_execute(proxy, &msg, vec![one_ucoin()])?);
}
let event = new_undelegation_event(
&pending_undelegate.delegate(),
&pending_undelegate.proxy(),
&pending_undelegate.mix_identity(),
total_delegation,
);
Ok(ReconcileUndelegateResponse {
bank_msg,
wasm_msg,
event,
})
}
pub(crate) fn _try_remove_delegation_from_mixnode(
deps: DepsMut<'_>,
env: Env,
mix_identity: IdentityKey,
delegate: &str,
proxy: Option<Addr>,
) -> Result<Response, ContractError> {
let delegate = deps.api.addr_validate(delegate)?;
let delegation_map = storage::delegations();
let maybe_proxy_storage = generate_storage_key(&delegate, proxy.as_ref());
let storage_key = (mix_identity.clone(), maybe_proxy_storage).joined_key();
match delegation_map.may_load(deps.storage, storage_key.clone())? {
None => Err(ContractError::NoMixnodeDelegationFound {
identity: mix_identity,
address: delegate,
}),
Some(old_delegation) => {
// remove all delegation associated with this delegator
if proxy != old_delegation.proxy {
return Err(ContractError::ProxyMismatch {
existing: old_delegation
.proxy
.map_or_else(|| "None".to_string(), |a| a.to_string()),
incoming: proxy.map_or_else(|| "None".to_string(), |a| a.to_string()),
});
}
// remove old delegation data from the store
// note for reviewers: I'm using `replace` as `remove` is just `may_load` followed by `replace`
// and we've already performed `may_load` and have access to pre-existing data
delegation_map.replace(deps.storage, storage_key, None, Some(&old_delegation))?;
PENDING_DELEGATION_EVENTS.save(
deps.storage,
(
env.block.height,
mix_identity.to_string(),
delegate.as_bytes().to_vec(),
),
&DelegationEvent::Undelegate(PendingUndelegate::new(
mix_identity.to_string(),
delegate.clone(),
proxy.clone(),
env.block.height,
)),
)?;
// send delegated funds back to the delegation owner
let return_tokens = BankMsg::Send {
to_address: proxy.as_ref().unwrap_or(&delegate).to_string(),
amount: coins(
old_delegation.amount.amount.u128(),
old_delegation.amount.denom.clone(),
),
};
// update total_delegation of this node
mixnodes_storage::TOTAL_DELEGATION.update::<_, ContractError>(
deps.storage,
&mix_identity,
|total_delegation| {
// the first unwrap is fine because the delegation information MUST exist, otherwise we would
// have never gotten here in the first place
// the second unwrap is also fine because we should NEVER underflow here,
// if we do, it means we have some serious error in our logic
Ok(total_delegation
.unwrap()
.checked_sub(old_delegation.amount.amount)
.unwrap())
},
)?;
let mut response = Response::new().add_message(return_tokens);
if let Some(proxy) = &proxy {
let msg = Some(VestingContractExecuteMsg::TrackUndelegation {
owner: delegate.as_str().to_string(),
mix_identity: mix_identity.clone(),
amount: old_delegation.amount.clone(),
});
let track_undelegation_msg = wasm_execute(proxy, &msg, vec![one_ucoin()])?;
response = response.add_message(track_undelegation_msg);
}
Ok(response.add_event(new_undelegation_event(
&delegate,
&proxy,
&old_delegation,
&mix_identity,
)))
}
}
Ok(Response::new().add_event(new_pending_undelegation_event(
&delegate,
&proxy,
&mix_identity,
)))
}
#[cfg(test)]
@@ -332,6 +496,8 @@ mod tests {
)
.is_ok());
_try_reconcile_all_delegation_events(&mut deps.storage, &deps.api).unwrap();
let expected = Delegation::new(
delegation_owner.clone(),
identity.clone(),
@@ -342,7 +508,13 @@ mod tests {
assert_eq!(
expected,
test_helpers::read_delegation(&deps.storage, &identity, delegation_owner).unwrap()
test_helpers::read_delegation(
&deps.storage,
&identity,
delegation_owner.as_bytes(),
mock_env().block.height
)
.unwrap()
);
// node's "total_delegation" is increased
@@ -364,7 +536,7 @@ mod tests {
deps.as_mut(),
);
let delegation_owner = Addr::unchecked("sender");
try_remove_mixnode(deps.as_mut(), mock_info(mixnode_owner, &[])).unwrap();
try_remove_mixnode(mock_env(), deps.as_mut(), mock_info(mixnode_owner, &[])).unwrap();
assert_eq!(
Err(ContractError::MixNodeBondNotFound {
identity: identity.clone()
@@ -387,7 +559,7 @@ mod tests {
tests::fixtures::good_mixnode_pledge(),
deps.as_mut(),
);
try_remove_mixnode(deps.as_mut(), mock_info(mixnode_owner, &[])).unwrap();
try_remove_mixnode(mock_env(), deps.as_mut(), mock_info(mixnode_owner, &[])).unwrap();
let identity = test_helpers::add_mixnode(
mixnode_owner,
tests::fixtures::good_mixnode_pledge(),
@@ -403,6 +575,8 @@ mod tests {
)
.is_ok());
_try_reconcile_all_delegation_events(&mut deps.storage, &deps.api).unwrap();
let expected = Delegation::new(
delegation_owner.clone(),
identity.clone(),
@@ -413,7 +587,13 @@ mod tests {
assert_eq!(
expected,
test_helpers::read_delegation(&deps.storage, &identity, delegation_owner).unwrap()
test_helpers::read_delegation(
&deps.storage,
&identity,
delegation_owner.as_bytes(),
mock_env().block.height
)
.unwrap()
);
// node's "total_delegation" is increased
@@ -437,33 +617,47 @@ mod tests {
let delegation_owner = Addr::unchecked("sender");
let delegation1 = coin(100, DENOM);
let delegation2 = coin(50, DENOM);
let mut env = mock_env();
try_delegate_to_mixnode(
deps.as_mut(),
mock_env(),
env.clone(),
mock_info(delegation_owner.as_str(), &[delegation1.clone()]),
identity.clone(),
)
.unwrap();
env.block.height += 1;
try_delegate_to_mixnode(
deps.as_mut(),
mock_env(),
env,
mock_info(delegation_owner.as_str(), &[delegation2.clone()]),
identity.clone(),
)
.unwrap();
let expected = Delegation::new(
delegation_owner.clone(),
identity.clone(),
coin(delegation1.amount.u128() + delegation2.amount.u128(), DENOM),
mock_env().block.height,
None,
);
_try_reconcile_all_delegation_events(&mut deps.storage, &deps.api).unwrap();
assert_eq!(
expected,
test_helpers::read_delegation(&deps.storage, &identity, delegation_owner).unwrap()
);
// let expected = Delegation::new(
// delegation_owner.clone(),
// identity.clone(),
// coin(delegation1.amount.u128() + delegation2.amount.u128(), DENOM),
// mock_env().block.height,
// None,
// );
// assert_eq!(
// expected,
// test_helpers::read_delegation(
// &deps.storage,
// &identity,
// delegation_owner.as_bytes(),
// mock_env().block.height
// )
// .unwrap()
// );
// node's "total_delegation" is sum of both
assert_eq!(
@@ -493,16 +687,24 @@ mod tests {
env2.block.height = updated_height;
try_delegate_to_mixnode(
deps.as_mut(),
env1,
env1.clone(),
mock_info(delegation_owner.as_str(), &[delegation.clone()]),
identity.clone(),
)
.unwrap();
_try_reconcile_all_delegation_events(&mut deps.storage, &deps.api).unwrap();
assert_eq!(
initial_height,
test_helpers::read_delegation(&deps.storage, &identity, &delegation_owner)
.unwrap()
.block_height
test_helpers::read_delegation(
&deps.storage,
&identity,
delegation_owner.as_bytes(),
env1.block.height
)
.unwrap()
.block_height
);
try_delegate_to_mixnode(
deps.as_mut(),
@@ -512,11 +714,21 @@ mod tests {
)
.unwrap();
let updated =
test_helpers::read_delegation(&deps.storage, &identity, &delegation_owner).unwrap();
_try_reconcile_all_delegation_events(&mut deps.storage, &deps.api).unwrap();
assert_eq!(delegation.amount + delegation.amount, updated.amount.amount);
assert_eq!(updated_height, updated.block_height);
let delegations = crate::delegations::queries::query_mixnode_delegation(
&deps.storage,
&deps.api,
identity,
delegation_owner.to_string(),
)
.unwrap();
let total_delegation = delegations
.iter()
.fold(Uint128::zero(), |acc, d| acc + d.amount.amount);
assert_eq!(delegation.amount + delegation.amount, total_delegation);
}
#[test]
@@ -540,37 +752,56 @@ mod tests {
env2.block.height = second_height;
try_delegate_to_mixnode(
deps.as_mut(),
env1,
env1.clone(),
mock_info(delegation_owner1.as_str(), &[delegation1]),
identity.clone(),
)
.unwrap();
_try_reconcile_all_delegation_events(&mut deps.storage, &deps.api).unwrap();
assert_eq!(
initial_height,
test_helpers::read_delegation(&deps.storage, &identity, &delegation_owner1)
.unwrap()
.block_height
test_helpers::read_delegation(
&deps.storage,
&identity,
delegation_owner1.as_bytes(),
env1.block.height
)
.unwrap()
.block_height
);
try_delegate_to_mixnode(
deps.as_mut(),
env2,
env2.clone(),
mock_info(delegation_owner2.as_str(), &[delegation2]),
identity.clone(),
)
.unwrap();
_try_reconcile_all_delegation_events(&mut deps.storage, &deps.api).unwrap();
assert_eq!(
initial_height,
test_helpers::read_delegation(&deps.storage, &identity, &delegation_owner1)
.unwrap()
.block_height
test_helpers::read_delegation(
&deps.storage,
&identity,
delegation_owner1.as_bytes(),
env1.block.height
)
.unwrap()
.block_height
);
assert_eq!(
second_height,
test_helpers::read_delegation(&deps.storage, identity, &delegation_owner2)
.unwrap()
.block_height
test_helpers::read_delegation(
&deps.storage,
identity,
delegation_owner2.as_bytes(),
env2.block.height
)
.unwrap()
.block_height
);
}
@@ -591,7 +822,7 @@ mod tests {
identity.clone(),
)
.unwrap();
try_remove_mixnode(deps.as_mut(), mock_info(mixnode_owner, &[])).unwrap();
try_remove_mixnode(mock_env(), deps.as_mut(), mock_info(mixnode_owner, &[])).unwrap();
assert_eq!(
Err(ContractError::MixNodeBondNotFound {
identity: identity.clone()
@@ -636,6 +867,8 @@ mod tests {
)
.is_ok());
_try_reconcile_all_delegation_events(&mut deps.storage, &deps.api).unwrap();
let expected1 = Delegation::new(
delegation_owner.clone(),
identity1.clone(),
@@ -654,11 +887,23 @@ mod tests {
assert_eq!(
expected1,
test_helpers::read_delegation(&deps.storage, identity1, &delegation_owner).unwrap()
test_helpers::read_delegation(
&deps.storage,
identity1,
delegation_owner.as_bytes(),
mock_env().block.height
)
.unwrap()
);
assert_eq!(
expected2,
test_helpers::read_delegation(&deps.storage, identity2, &delegation_owner).unwrap()
test_helpers::read_delegation(
&deps.storage,
identity2,
delegation_owner.as_bytes(),
mock_env().block.height
)
.unwrap()
);
}
@@ -687,6 +932,8 @@ mod tests {
identity.clone(),
)
.is_ok());
_try_reconcile_all_delegation_events(&mut deps.storage, &deps.api).unwrap();
// node's "total_delegation" is sum of both
assert_eq!(
delegation1.amount + delegation2.amount,
@@ -714,7 +961,10 @@ mod tests {
identity.clone(),
)
.unwrap();
try_remove_mixnode(deps.as_mut(), mock_info(mixnode_owner, &[])).unwrap();
_try_reconcile_all_delegation_events(&mut deps.storage, &deps.api).unwrap();
try_remove_mixnode(mock_env(), deps.as_mut(), mock_info(mixnode_owner, &[])).unwrap();
let expected = Delegation::new(
delegation_owner.clone(),
@@ -726,7 +976,13 @@ mod tests {
assert_eq!(
expected,
test_helpers::read_delegation(&deps.storage, identity, delegation_owner).unwrap()
test_helpers::read_delegation(
&deps.storage,
identity,
delegation_owner.as_bytes(),
mock_env().block.height
)
.unwrap()
)
}
}
@@ -746,9 +1002,12 @@ mod tests {
use super::storage;
use super::*;
// TODO: Probably delete due to reconciliation logic
#[ignore]
#[test]
fn fails_if_delegation_never_existed() {
let mut deps = test_helpers::init_contract();
let env = mock_env();
let mixnode_owner = "bob";
let identity = test_helpers::add_mixnode(
mixnode_owner,
@@ -759,20 +1018,24 @@ mod tests {
assert_eq!(
Err(ContractError::NoMixnodeDelegationFound {
identity: identity.clone(),
address: delegation_owner.clone(),
address: delegation_owner.to_string(),
}),
try_remove_delegation_from_mixnode(
deps.as_mut(),
env,
mock_info(delegation_owner.as_str(), &[]),
identity,
)
);
}
// TODO: Update to work with reconciliation
#[ignore]
#[test]
fn succeeds_if_delegation_existed() {
let mut deps = test_helpers::init_contract();
let mixnode_owner = "bob";
let env = mock_env();
let identity = test_helpers::add_mixnode(
mixnode_owner,
tests::fixtures::good_mixnode_pledge(),
@@ -786,8 +1049,12 @@ mod tests {
identity.clone(),
)
.unwrap();
_try_reconcile_all_delegation_events(&mut deps.storage, &deps.api).unwrap();
let delegation = query_mixnode_delegation(
deps.as_ref(),
&deps.storage,
&deps.api,
identity.clone(),
delegation_owner.clone().into_string(),
)
@@ -801,14 +1068,15 @@ mod tests {
.add_event(new_undelegation_event(
&delegation_owner,
&None,
&delegation,
&identity,
Uint128::new(100),
));
assert_eq!(
Ok(expected_response),
try_remove_delegation_from_mixnode(
deps.as_mut(),
env,
mock_info(delegation_owner.as_str(), &[]),
identity.clone(),
)
@@ -816,7 +1084,7 @@ mod tests {
assert!(storage::delegations()
.may_load(
&deps.storage,
(identity.clone(), delegation_owner).joined_key(),
(identity.clone(), delegation_owner.as_bytes().to_vec(), 0),
)
.unwrap()
.is_none());
@@ -830,10 +1098,13 @@ mod tests {
)
}
// TODO: Update to work with reconciliation
#[ignore]
#[test]
fn succeeds_if_delegation_existed_even_if_node_unbonded() {
let mut deps = test_helpers::init_contract();
let mixnode_owner = "bob";
let env = mock_env();
let identity = test_helpers::add_mixnode(
mixnode_owner,
tests::fixtures::good_mixnode_pledge(),
@@ -847,12 +1118,17 @@ mod tests {
identity.clone(),
)
.unwrap();
_try_reconcile_all_delegation_events(&mut deps.storage, &deps.api).unwrap();
let delegation = query_mixnode_delegation(
deps.as_ref(),
&deps.storage,
&deps.api,
identity.clone(),
delegation_owner.clone().into_string(),
)
.unwrap();
let expected_response = Response::new()
.add_message(BankMsg::Send {
to_address: delegation_owner.clone().into(),
@@ -861,28 +1137,37 @@ mod tests {
.add_event(new_undelegation_event(
&delegation_owner,
&None,
&delegation,
&identity,
Uint128::new(100),
));
try_remove_mixnode(deps.as_mut(), mock_info(mixnode_owner, &[])).unwrap();
try_remove_mixnode(mock_env(), deps.as_mut(), mock_info(mixnode_owner, &[])).unwrap();
assert_eq!(
Ok(expected_response),
try_remove_delegation_from_mixnode(
deps.as_mut(),
env,
mock_info(delegation_owner.as_str(), &[]),
identity.clone(),
)
);
assert!(
test_helpers::read_delegation(&deps.storage, identity, delegation_owner).is_none()
);
_try_reconcile_all_delegation_events(&mut deps.storage, &deps.api).unwrap();
assert!(test_helpers::read_delegation(
&deps.storage,
identity,
delegation_owner.as_bytes(),
mock_env().block.height
)
.is_none());
}
#[test]
fn total_delegation_is_preserved_if_only_some_undelegate() {
let mut deps = test_helpers::init_contract();
let env = mock_env();
let mixnode_owner = "bob";
let identity = test_helpers::add_mixnode(
mixnode_owner,
@@ -900,6 +1185,9 @@ mod tests {
identity.clone(),
)
.is_ok());
_try_reconcile_all_delegation_events(&mut deps.storage, &deps.api).unwrap();
assert!(try_delegate_to_mixnode(
deps.as_mut(),
mock_env(),
@@ -907,13 +1195,19 @@ mod tests {
identity.clone(),
)
.is_ok());
_try_reconcile_all_delegation_events(&mut deps.storage, &deps.api).unwrap();
// sender1 undelegates
try_remove_delegation_from_mixnode(
deps.as_mut(),
env,
mock_info(delegation_owner1.as_str(), &[]),
identity.clone(),
)
.unwrap();
_try_reconcile_all_delegation_events(&mut deps.storage, &deps.api).unwrap();
// but total delegation should still equal to what sender2 sent
// node's "total_delegation" is sum of both
assert_eq!(
+19 -2
View File
@@ -3,7 +3,7 @@
use config::defaults::DENOM;
use cosmwasm_std::{Addr, StdError};
use mixnet_contract_common::IdentityKey;
use mixnet_contract_common::{error::MixnetContractError, IdentityKey};
use thiserror::Error;
/// Custom errors for contract failure conditions.
@@ -69,7 +69,7 @@ pub enum ContractError {
#[error("MIXNET ({}): Could not find any delegation information associated with mixnode {identity} for {address}", line!())]
NoMixnodeDelegationFound {
identity: IdentityKey,
address: Addr,
address: String,
},
#[error("MIXNET ({}): We tried to remove more funds then are available in the Reward pool. Wanted to remove {to_remove}, but have only {reward_pool}", line!())]
@@ -124,4 +124,21 @@ pub enum ContractError {
interval_start: i64,
interval_end: i64,
},
#[error("MIXNET ({}): Can't change to the desired interval as it's not in progress yet. It starts at {epoch_start} and finishes at {epoch_end}, while the current block time is {current_block_time}", line!())]
EpochNotInProgress {
current_block_time: u64,
epoch_start: i64,
epoch_end: i64,
},
#[error("Could not cast reward to a u128, this should be impossible, at {}", line!())]
CastError,
#[error("{source}")]
MixnetCommonError {
#[from]
source: MixnetContractError,
},
#[error("No rewards to claim for mixnode {identity} for delegate {delegate}")]
NoRewardsToClaim { identity: String, delegate: String },
}
+2 -1
View File
@@ -17,7 +17,8 @@ pub(crate) fn query_gateways_paged(
let limit = limit
.unwrap_or(BOND_PAGE_DEFAULT_LIMIT)
.min(BOND_PAGE_MAX_LIMIT) as usize;
let start = start_after.map(Bound::exclusive);
let start = start_after.as_deref().map(Bound::exclusive);
let nodes = storage::gateways()
.range(deps.storage, start, None, Order::Ascending)
+1 -1
View File
@@ -11,7 +11,7 @@ use mixnet_contract_common::{
};
pub fn query_current_interval(storage: &dyn Storage) -> Result<Interval, ContractError> {
Ok(storage::CURRENT_INTERVAL.load(storage)?)
storage::current_interval(storage)
}
pub(crate) fn query_rewarded_set_refresh_minimum_blocks() -> u64 {
+47 -2
View File
@@ -3,7 +3,11 @@
use cosmwasm_std::{StdResult, Storage};
use cw_storage_plus::{Item, Map};
use mixnet_contract_common::{IdentityKey, Interval, RewardedSetNodeStatus};
use mixnet_contract_common::{
reward_params::EpochRewardParams, IdentityKey, Interval, RewardedSetNodeStatus,
};
use crate::{error::ContractError, support::helpers::epoch_reward_params};
// type aliases for better reasoning for storage keys
// (I found it helpful)
@@ -14,7 +18,9 @@ type IntervalId = u32;
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");
const CURRENT_INTERVAL: Item<'_, Interval> = Item::new("cei");
const CURRENT_EPOCH: Item<'_, Interval> = Item::new("cep");
const CURRENT_EPOCH_REWARD_PARAMS: Item<'_, EpochRewardParams> = Item::new("erp");
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",
@@ -26,6 +32,45 @@ pub(crate) const REWARDED_SET_HEIGHTS_FOR_INTERVAL: Map<'_, (IntervalId, BlockHe
pub(crate) const REWARDED_SET: Map<'_, (BlockHeight, IdentityKey), RewardedSetNodeStatus> =
Map::new("rs");
pub(crate) const INTERVALS: Map<'_, IntervalId, Interval> = Map::new("ins");
pub(crate) const EPOCHS: Map<'_, IntervalId, Interval> = Map::new("ephs");
pub fn save_interval(storage: &mut dyn Storage, interval: &Interval) -> Result<(), ContractError> {
CURRENT_INTERVAL.save(storage, interval)?;
INTERVALS.save(storage, interval.id(), interval)?;
Ok(())
}
pub fn save_epoch(storage: &mut dyn Storage, interval: &Interval) -> Result<(), ContractError> {
CURRENT_EPOCH.save(storage, interval)?;
EPOCHS.save(storage, interval.id(), interval)?;
Ok(())
}
pub fn current_epoch_reward_params(
storage: &dyn Storage,
) -> Result<EpochRewardParams, ContractError> {
Ok(CURRENT_EPOCH_REWARD_PARAMS.load(storage)?)
}
pub fn save_epoch_reward_params(
epoch_id: u32,
storage: &mut dyn Storage,
) -> Result<(), ContractError> {
let epoch_reward_params = epoch_reward_params(epoch_id, storage)?;
CURRENT_EPOCH_REWARD_PARAMS.save(storage, &epoch_reward_params)?;
crate::rewards::storage::EPOCH_REWARD_PARAMS.save(storage, epoch_id, &epoch_reward_params)?;
Ok(())
}
pub fn current_interval(storage: &dyn Storage) -> Result<Interval, ContractError> {
Ok(CURRENT_INTERVAL.load(storage)?)
}
pub fn current_epoch(storage: &dyn Storage) -> Result<Interval, ContractError> {
Ok(CURRENT_EPOCH.load(storage)?)
}
pub(crate) fn save_rewarded_set(
storage: &mut dyn Storage,
height: BlockHeight,
+42 -14
View File
@@ -3,12 +3,15 @@
use super::storage;
use crate::error::ContractError;
use crate::error::ContractError::IntervalNotInProgress;
use crate::error::ContractError::{EpochNotInProgress, 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;
// We've distributed the rewards to the rewarded set from the validator api before making this call (implicit order, should be solved in the future)
// We now write the new rewarded set, snapshot the mixnodes and finally reconcile all delegations and undelegations. That way the rewards for the previous
// epoch will be calculated correctly as the delegations and undelegations from the previous epoch will only take effect in the next (current) one.
pub fn try_write_rewarded_set(
deps: DepsMut<'_>,
env: Env,
@@ -50,7 +53,7 @@ pub fn try_write_rewarded_set(
});
}
let current_interval = storage::CURRENT_INTERVAL.load(deps.storage)?.id();
let current_interval = storage::current_interval(deps.storage)?.id();
let num_nodes = rewarded_set.len();
storage::save_rewarded_set(deps.storage, block_height, active_set_size, rewarded_set)?;
@@ -76,8 +79,8 @@ pub fn try_advance_interval(
// 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();
let current_interval = storage::current_interval(storage)?;
let next_interval = current_interval.next();
if next_interval.start_unix_timestamp() > env.block.time.seconds() as i64 {
// the reason for this check is as follows:
@@ -95,11 +98,40 @@ pub fn try_advance_interval(
});
}
storage::CURRENT_INTERVAL.save(storage, &next_interval)?;
storage::save_interval(storage, &next_interval)?;
Ok(Response::new().add_event(new_advance_interval_event(next_interval)))
}
pub fn try_advance_epoch(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_epoch = storage::current_epoch(storage)?;
let next_epoch = current_epoch.next();
if next_epoch.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 epochs,
// 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(EpochNotInProgress {
current_block_time: env.block.time.seconds(),
epoch_start: next_epoch.start_unix_timestamp(),
epoch_end: next_epoch.end_unix_timestamp(),
});
}
storage::save_epoch(storage, &next_epoch)?;
storage::save_epoch_reward_params(next_epoch.id(), storage)?;
Ok(Response::new().add_event(new_advance_interval_event(next_epoch)))
}
#[cfg(test)]
mod tests {
use super::*;
@@ -251,10 +283,8 @@ mod tests {
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, &current_interval)
.unwrap();
let next_interval = current_interval.next();
storage::save_interval(deps.as_mut().storage, &current_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);
@@ -284,7 +314,7 @@ mod tests {
// 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_new_interval = current_interval.next();
let expected_response =
Response::new().add_event(new_advance_interval_event(expected_new_interval));
assert_eq!(
@@ -294,10 +324,8 @@ mod tests {
// 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, &current_interval)
.unwrap();
let expected_new_interval = current_interval.next_interval();
storage::save_interval(deps.as_mut().storage, &current_interval).unwrap();
let expected_new_interval = current_interval.next();
let expected_response =
Response::new().add_event(new_advance_interval_event(expected_new_interval));
assert_eq!(
@@ -17,7 +17,7 @@ pub fn query_mixnodes_paged(
.unwrap_or(storage::BOND_PAGE_DEFAULT_LIMIT)
.min(storage::BOND_PAGE_MAX_LIMIT) as usize;
let start = start_after.map(Bound::exclusive);
let start = start_after.as_deref().map(Bound::exclusive);
let nodes = storage::mixnodes()
.range(deps.storage, start, None, Order::Ascending)
@@ -203,6 +203,7 @@ pub(crate) mod tests {
#[test]
fn query_for_mixnode_owner_works() {
let mut deps = test_helpers::init_contract();
let env = mock_env();
// "fred" does not own a mixnode if there are no mixnodes
let res = query_owns_mixnode(deps.as_ref(), "fred".to_string()).unwrap();
@@ -225,8 +226,12 @@ pub(crate) mod tests {
assert!(res.mixnode.is_some());
// but after unbonding it, he doesn't own one anymore
crate::mixnodes::transactions::try_remove_mixnode(deps.as_mut(), mock_info("fred", &[]))
.unwrap();
crate::mixnodes::transactions::try_remove_mixnode(
env,
deps.as_mut(),
mock_info("fred", &[]),
)
.unwrap();
let res = query_owns_mixnode(deps.as_ref(), "fred".to_string()).unwrap();
assert!(res.mixnode.is_none());
+48 -7
View File
@@ -3,14 +3,19 @@
use config::defaults::DENOM;
use cosmwasm_std::{StdResult, Storage, Uint128};
use cw_storage_plus::{Index, IndexList, IndexedMap, Map, UniqueIndex};
use mixnet_contract_common::{Addr, Coin, IdentityKeyRef, Layer, MixNode, MixNodeBond};
use cw_storage_plus::{Index, IndexList, IndexedSnapshotMap, Map, Strategy, UniqueIndex};
use mixnet_contract_common::U128;
use mixnet_contract_common::{
reward_params::NodeEpochRewards, Addr, Coin, IdentityKeyRef, Layer, MixNode, MixNodeBond,
};
use serde::{Deserialize, Serialize};
use std::fmt::{Display, Formatter};
// storage prefixes
const TOTAL_DELEGATION_NAMESPACE: &str = "td";
const MIXNODES_PK_NAMESPACE: &str = "mn";
const MIXNODES_PK_CHECKPOINTS: &str = "mn__check";
const MIXNODES_PK_CHANGELOG: &str = "mn__change";
const MIXNODES_OWNER_IDX_NAMESPACE: &str = "mno";
// paged retrieval limits for all queries and transactions
@@ -35,11 +40,17 @@ impl<'a> IndexList<StoredMixnodeBond> for MixnodeBondIndex<'a> {
// mixnodes() is the storage access function.
pub(crate) fn mixnodes<'a>(
) -> IndexedMap<'a, IdentityKeyRef<'a>, StoredMixnodeBond, MixnodeBondIndex<'a>> {
) -> IndexedSnapshotMap<'a, IdentityKeyRef<'a>, StoredMixnodeBond, MixnodeBondIndex<'a>> {
let indexes = MixnodeBondIndex {
owner: UniqueIndex::new(|d| d.owner.clone(), MIXNODES_OWNER_IDX_NAMESPACE),
};
IndexedMap::new(MIXNODES_PK_NAMESPACE, indexes)
IndexedSnapshotMap::new(
MIXNODES_PK_NAMESPACE,
MIXNODES_PK_CHECKPOINTS,
MIXNODES_PK_CHANGELOG,
Strategy::Never,
indexes,
)
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
@@ -50,9 +61,27 @@ pub(crate) struct StoredMixnodeBond {
pub block_height: u64,
pub mix_node: MixNode,
pub proxy: Option<Addr>,
pub accumulated_rewards: Uint128,
pub epoch_rewards: Option<NodeEpochRewards>,
}
impl From<MixNodeBond> for StoredMixnodeBond {
fn from(mixnode_bond: MixNodeBond) -> StoredMixnodeBond {
StoredMixnodeBond {
pledge_amount: mixnode_bond.pledge_amount,
owner: mixnode_bond.owner,
layer: mixnode_bond.layer,
block_height: mixnode_bond.block_height,
mix_node: mixnode_bond.mix_node,
proxy: mixnode_bond.proxy,
accumulated_rewards: mixnode_bond.accumulated_rewards,
epoch_rewards: None,
}
}
}
impl StoredMixnodeBond {
#[allow(clippy::too_many_arguments)]
pub(crate) fn new(
pledge_amount: Coin,
owner: Addr,
@@ -60,6 +89,8 @@ impl StoredMixnodeBond {
block_height: u64,
mix_node: MixNode,
proxy: Option<Addr>,
accumulated_rewards: Uint128,
epoch_rewards: Option<NodeEpochRewards>,
) -> Self {
StoredMixnodeBond {
pledge_amount,
@@ -68,6 +99,8 @@ impl StoredMixnodeBond {
block_height,
mix_node,
proxy,
accumulated_rewards,
epoch_rewards,
}
}
@@ -83,6 +116,7 @@ impl StoredMixnodeBond {
block_height: self.block_height,
mix_node: self.mix_node,
proxy: self.proxy,
accumulated_rewards: self.accumulated_rewards,
}
}
@@ -93,6 +127,10 @@ impl StoredMixnodeBond {
pub(crate) fn pledge_amount(&self) -> Coin {
self.pledge_amount.clone()
}
pub fn profit_margin(&self) -> U128 {
U128::from_num(self.mix_node.profit_margin_percent) / U128::from_num(100)
}
}
impl Display for StoredMixnodeBond {
@@ -125,6 +163,7 @@ pub(crate) fn read_full_mixnode_bond(
block_height: stored_bond.block_height,
mix_node: stored_bond.mix_node,
proxy: stored_bond.proxy,
accumulated_rewards: stored_bond.accumulated_rewards,
}))
}
}
@@ -145,8 +184,8 @@ mod tests {
let mut storage = MockStorage::new();
let bond1 = tests::fixtures::stored_mixnode_bond_fixture("owner1");
let bond2 = tests::fixtures::stored_mixnode_bond_fixture("owner2");
mixnodes().save(&mut storage, "bond1", &bond1).unwrap();
mixnodes().save(&mut storage, "bond2", &bond2).unwrap();
mixnodes().save(&mut storage, "bond1", &bond1, 1).unwrap();
mixnodes().save(&mut storage, "bond2", &bond2, 1).unwrap();
let res1 = mixnodes().load(&storage, "bond1").unwrap();
let res2 = mixnodes().load(&storage, "bond2").unwrap();
@@ -177,10 +216,12 @@ mod tests {
..tests::fixtures::mix_node_fixture()
},
proxy: None,
accumulated_rewards: Uint128::zero(),
epoch_rewards: None,
};
storage::mixnodes()
.save(&mut mock_storage, &node_identity, &mixnode_bond)
.save(&mut mock_storage, &node_identity, &mixnode_bond, 1)
.unwrap();
assert_eq!(
+59 -18
View File
@@ -9,13 +9,31 @@ use crate::mixnodes::storage::StoredMixnodeBond;
use crate::support::helpers::{ensure_no_existing_bond, validate_node_identity_signature};
use config::defaults::DENOM;
use cosmwasm_std::{
wasm_execute, Addr, BankMsg, Coin, DepsMut, Env, MessageInfo, Response, Uint128,
wasm_execute, Addr, BankMsg, Coin, DepsMut, Env, MessageInfo, Response, Storage, Uint128,
};
use mixnet_contract_common::events::{
new_checkpoint_mixnodes_event, new_mixnode_bonding_event, new_mixnode_unbonding_event,
};
use mixnet_contract_common::events::{new_mixnode_bonding_event, new_mixnode_unbonding_event};
use mixnet_contract_common::MixNode;
use vesting_contract_common::messages::ExecuteMsg as VestingContractExecuteMsg;
use vesting_contract_common::one_ucoin;
pub fn try_checkpoint_mixnodes(
storage: &mut dyn Storage,
block_height: u64,
info: MessageInfo,
) -> Result<Response, ContractError> {
let state = mixnet_params_storage::CONTRACT_STATE.load(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);
}
crate::mixnodes::storage::mixnodes().add_checkpoint(storage, block_height)?;
Ok(Response::new().add_event(new_checkpoint_mixnodes_event(block_height)))
}
pub fn try_add_mixnode(
deps: DepsMut<'_>,
env: Env,
@@ -117,12 +135,14 @@ fn _try_add_mixnode(
env.block.height,
mix_node,
proxy.clone(),
Uint128::zero(),
None,
);
// technically we don't have to set the total_delegation bucket, but it makes things easier
// in different places that we can guarantee that if node exists, so does the data behind the total delegation
let identity = stored_bond.identity();
storage::mixnodes().save(deps.storage, identity, &stored_bond)?;
storage::mixnodes().save(deps.storage, identity, &stored_bond, env.block.height)?;
// if this is a fresh mixnode - write 0 total delegation, otherwise, don't touch it since the node has just rebonded
if storage::TOTAL_DELEGATION
@@ -144,25 +164,38 @@ fn _try_add_mixnode(
}
pub fn try_remove_mixnode_on_behalf(
env: Env,
deps: DepsMut<'_>,
info: MessageInfo,
owner: String,
) -> Result<Response, ContractError> {
let proxy = info.sender;
_try_remove_mixnode(deps, &owner, Some(proxy))
_try_remove_mixnode(env, deps, &owner, Some(proxy))
}
pub fn try_remove_mixnode(deps: DepsMut<'_>, info: MessageInfo) -> Result<Response, ContractError> {
_try_remove_mixnode(deps, info.sender.as_ref(), None)
pub fn try_remove_mixnode(
env: Env,
deps: DepsMut<'_>,
info: MessageInfo,
) -> Result<Response, ContractError> {
_try_remove_mixnode(env, deps, info.sender.as_ref(), None)
}
pub(crate) fn _try_remove_mixnode(
env: Env,
deps: DepsMut<'_>,
owner: &str,
proxy: Option<Addr>,
) -> Result<Response, ContractError> {
let owner = deps.api.addr_validate(owner)?;
crate::rewards::transactions::_try_compound_operator_reward(
deps.storage,
env.block.height,
&owner,
None,
)?;
// try to find the node of the sender
let mixnode_bond = match storage::mixnodes()
.idx
@@ -188,7 +221,7 @@ pub(crate) fn _try_remove_mixnode(
};
// remove the bond
storage::mixnodes().remove(deps.storage, mixnode_bond.identity())?;
storage::mixnodes().remove(deps.storage, mixnode_bond.identity(), env.block.height)?;
// decrement layer count
mixnet_params_storage::decrement_layer_count(deps.storage, mixnode_bond.layer)?;
@@ -267,15 +300,20 @@ pub(crate) fn _try_update_mixnode_config(
));
}
storage::mixnodes().update(deps.storage, mixnode_bond.identity(), |mixnode_bond_opt| {
mixnode_bond_opt
.map(|mut mixnode_bond| {
mixnode_bond.mix_node.profit_margin_percent = profit_margin_percent;
mixnode_bond.block_height = env.block.height;
mixnode_bond
})
.ok_or(ContractError::NoBondFound)
})?;
storage::mixnodes().update(
deps.storage,
mixnode_bond.identity(),
env.block.height,
|mixnode_bond_opt| {
mixnode_bond_opt
.map(|mut mixnode_bond| {
mixnode_bond.mix_node.profit_margin_percent = profit_margin_percent;
mixnode_bond.block_height = env.block.height;
mixnode_bond
})
.ok_or(ContractError::NoBondFound)
},
)?;
let mut response = Response::new();
@@ -607,7 +645,7 @@ pub mod tests {
.add_event(new_mixnode_unbonding_event(
&Addr::unchecked("fred"),
&None,
&tests::fixtures::good_gateway_pledge()[0],
&tests::fixtures::good_mixnode_pledge()[0],
&fred_identity,
));
@@ -615,6 +653,7 @@ pub mod tests {
// only 1 node now exists, owned by bob:
let mix_node_bonds = tests::queries::get_mix_nodes(&mut deps);
assert_eq!(1, mix_node_bonds.len());
assert_eq!(&Addr::unchecked("bob"), mix_node_bonds[0].owner());
}
@@ -642,7 +681,9 @@ pub mod tests {
let info = mock_info("mix-owner", &[]);
let msg = ExecuteMsg::UnbondMixnode {};
assert!(execute(deps.as_mut(), mock_env(), info, msg).is_ok());
let response = execute(deps.as_mut(), mock_env(), info, msg);
assert!(response.is_ok());
assert!(storage::mixnodes()
.idx
+8 -69
View File
@@ -3,81 +3,20 @@
use super::storage;
use crate::error::ContractError;
use crate::mixnodes::storage as mixnodes_storage;
use cosmwasm_std::{Addr, Storage, Uint128};
use mixnet_contract_common::mixnode::DelegatorRewardParams;
use mixnet_contract_common::{
IdentityKey, IdentityKeyRef, PendingDelegatorRewarding, RewardingResult, RewardingStatus,
};
pub(crate) fn update_post_rewarding_storage(
storage: &mut dyn Storage,
mix_identity: IdentityKeyRef<'_>,
operator_reward: Uint128,
delegators_reward: Uint128,
) -> Result<(), ContractError> {
if operator_reward == Uint128::zero() && delegators_reward == Uint128::zero() {
return Ok(());
}
// update pledge
if operator_reward > Uint128::zero() {
mixnodes_storage::mixnodes().update(storage, mix_identity, |current_bond| {
match current_bond {
None => Err(ContractError::MixNodeBondNotFound {
identity: mix_identity.to_string(),
}),
Some(mut mixnode_bond) => {
mixnode_bond.pledge_amount.amount += operator_reward;
Ok(mixnode_bond)
}
}
})?;
}
// update total_delegation
if delegators_reward > Uint128::zero() {
mixnodes_storage::TOTAL_DELEGATION.update(storage, mix_identity, |current_total| {
match current_total {
None => Err(ContractError::MixNodeBondNotFound {
identity: mix_identity.to_string(),
}),
Some(current_total) => Ok(current_total + delegators_reward),
}
})?;
}
// update reward pool
storage::decr_reward_pool(storage, operator_reward + delegators_reward)?;
Ok(())
}
use cosmwasm_std::Storage;
use mixnet_contract_common::{IdentityKey, RewardingResult, RewardingStatus};
pub(crate) fn update_rewarding_status(
storage: &mut dyn Storage,
interval_id: u32,
mix_identity: IdentityKey,
rewarding_results: RewardingResult,
next_start: Option<Addr>,
delegators_rewarding_params: DelegatorRewardParams,
rewarding_result: RewardingResult,
) -> Result<(), ContractError> {
if let Some(next_start) = next_start {
storage::REWARDING_STATUS.save(
storage,
(interval_id, mix_identity),
&RewardingStatus::PendingNextDelegatorPage(PendingDelegatorRewarding {
running_results: rewarding_results,
next_start,
rewarding_params: delegators_rewarding_params,
}),
)?;
} else {
storage::REWARDING_STATUS.save(
storage,
(interval_id, mix_identity),
&RewardingStatus::Complete(rewarding_results),
)?;
}
storage::REWARDING_STATUS.save(
storage,
(interval_id, mix_identity),
&RewardingStatus::Complete(rewarding_result),
)?;
Ok(())
}
+16 -90
View File
@@ -35,11 +35,10 @@ pub(crate) mod tests {
#[cfg(test)]
mod querying_for_rewarding_status {
use super::*;
use crate::constants;
use crate::delegations::transactions::try_delegate_to_mixnode;
use crate::rewards::transactions::{
try_reward_mixnode, try_reward_next_mixnode_delegators,
};
use crate::interval::storage::{save_epoch, save_epoch_reward_params};
use crate::rewards::transactions::try_reward_mixnode;
use crate::{constants, support::tests::fixtures::epoch_fixture};
use config::defaults::DENOM;
use cosmwasm_std::{coin, Addr};
use mixnet_contract_common::{
@@ -75,7 +74,7 @@ pub(crate) mod tests {
env,
info,
node_identity.clone(),
tests::fixtures::node_rewarding_params_fixture(100),
tests::fixtures::node_reward_params_fixture(100),
0,
)
.unwrap();
@@ -87,6 +86,7 @@ pub(crate) mod tests {
}
#[test]
fn returns_complete_status_for_fully_rewarded_node() {
// with single page
let mut deps = test_helpers::init_contract();
@@ -106,12 +106,17 @@ pub(crate) mod tests {
env.block.height += constants::MINIMUM_BLOCK_AGE_FOR_REWARDING;
let info = mock_info(rewarding_validator_address.as_ref(), &[]);
let epoch = epoch_fixture();
save_epoch(&mut deps.storage, &epoch).unwrap();
save_epoch_reward_params(epoch.id(), &mut deps.storage).unwrap();
try_reward_mixnode(
deps.as_mut(),
env.clone(),
info,
node_identity.clone(),
tests::fixtures::node_rewarding_params_fixture(100),
tests::fixtures::node_reward_params_fixture(100),
0,
)
.unwrap();
@@ -121,14 +126,7 @@ pub(crate) mod tests {
match res.status.unwrap() {
RewardingStatus::Complete(result) => {
assert_ne!(
RewardingResult::default().operator_reward,
result.operator_reward
);
assert_eq!(
RewardingResult::default().total_delegator_reward,
result.total_delegator_reward
);
assert_ne!(RewardingResult::default().node_reward, result.node_reward);
}
_ => unreachable!(),
}
@@ -161,93 +159,21 @@ pub(crate) mod tests {
env,
info.clone(),
node_identity.clone(),
tests::fixtures::node_rewarding_params_fixture(100),
tests::fixtures::node_reward_params_fixture(100),
1,
)
.unwrap();
// rewards all pending
try_reward_next_mixnode_delegators(deps.as_mut(), info, node_identity.to_string(), 1)
.unwrap();
// 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, 1).unwrap();
assert!(matches!(res.status, Some(RewardingStatus::Complete(..))));
match res.status.unwrap() {
RewardingStatus::Complete(result) => {
assert_ne!(
RewardingResult::default().operator_reward,
result.operator_reward
);
assert_ne!(
RewardingResult::default().total_delegator_reward,
result.total_delegator_reward
);
}
_ => unreachable!(),
}
}
#[test]
fn returns_pending_next_delegator_page_status_when_there_are_more_delegators_to_reward() {
let mut deps = test_helpers::init_contract();
let mut env = mock_env();
let current_state = mixnet_params_storage::CONTRACT_STATE
.load(deps.as_mut().storage)
.unwrap();
let rewarding_validator_address = current_state.rewarding_validator_address;
let node_owner: Addr = Addr::unchecked("bob");
let node_identity = test_helpers::add_mixnode(
node_owner.as_str(),
tests::fixtures::good_mixnode_pledge(),
deps.as_mut(),
);
for i in 0..MIXNODE_DELEGATORS_PAGE_LIMIT + 123 {
try_delegate_to_mixnode(
deps.as_mut(),
env.clone(),
mock_info(&*format!("delegator{:04}", i), &[coin(200_000000, DENOM)]),
node_identity.clone(),
)
.unwrap();
}
env.block.height += constants::MINIMUM_BLOCK_AGE_FOR_REWARDING;
let info = mock_info(rewarding_validator_address.as_ref(), &[]);
try_reward_mixnode(
deps.as_mut(),
env,
info,
node_identity.clone(),
tests::fixtures::node_rewarding_params_fixture(100),
0,
)
.unwrap();
let res = query_rewarding_status(deps.as_ref(), node_identity, 0).unwrap();
assert!(matches!(
res.status,
Some(RewardingStatus::PendingNextDelegatorPage(..))
));
match res.status.unwrap() {
RewardingStatus::PendingNextDelegatorPage(result) => {
assert_ne!(
RewardingResult::default().operator_reward,
result.running_results.operator_reward
);
assert_ne!(
RewardingResult::default().total_delegator_reward,
result.running_results.total_delegator_reward
);
assert_eq!(
&*format!("delegator{:04}", MIXNODE_DELEGATORS_PAGE_LIMIT),
result.next_start
);
assert_ne!(RewardingResult::default().node_reward, result.node_reward);
}
_ => unreachable!(),
}
+20 -1
View File
@@ -5,12 +5,31 @@ use crate::error::ContractError;
use config::defaults::TOTAL_SUPPLY;
use cosmwasm_std::{StdResult, Storage, Uint128};
use cw_storage_plus::{Item, Map};
use mixnet_contract_common::{IdentityKey, RewardingStatus};
use mixnet_contract_common::{reward_params::EpochRewardParams, IdentityKey, RewardingStatus};
type BlockHeight = u64;
type Address = String;
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");
pub(crate) const DELEGATOR_REWARD_CLAIMED_HEIGHT: Map<'_, (Address, IdentityKey), BlockHeight> =
Map::new("drc");
pub(crate) const OPERATOR_REWARD_CLAIMED_HEIGHT: Map<'_, (Address, IdentityKey), BlockHeight> =
Map::new("orc");
type EpochId = u32;
pub(crate) const EPOCH_REWARD_PARAMS: Map<'_, EpochId, EpochRewardParams> = Map::new("epr");
pub fn epoch_reward_params_for_id(
storage: &dyn Storage,
id: EpochId,
) -> StdResult<EpochRewardParams> {
EPOCH_REWARD_PARAMS.load(storage, id)
}
#[allow(dead_code)]
pub fn incr_reward_pool(
amount: Uint128,
File diff suppressed because it is too large Load Diff
+29 -3
View File
@@ -1,11 +1,37 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::error::ContractError;
use crate::gateways::storage as gateways_storage;
use crate::mixnodes::storage as mixnodes_storage;
use crate::{constants, gateways::storage as gateways_storage};
use crate::error::ContractError;
use cosmwasm_std::{Addr, Deps, Storage};
use mixnet_contract_common::IdentityKeyRef;
use mixnet_contract_common::{reward_params::EpochRewardParams, IdentityKeyRef};
pub(crate) fn epoch_reward_params(
epoch_id: u32,
storage: &mut dyn Storage,
) -> Result<EpochRewardParams, ContractError> {
let state = crate::mixnet_contract_settings::storage::CONTRACT_STATE
.load(storage)
.map(|settings| settings.params)?;
let reward_pool = crate::rewards::storage::REWARD_POOL.load(storage)?;
let interval_reward_percent = crate::constants::INTERVAL_REWARD_PERCENT;
let epochs_in_interval = crate::constants::EPOCHS_IN_INTERVAL;
let epoch_reward_params = EpochRewardParams::new(
(reward_pool.u128() / 100 / epochs_in_interval as u128) * interval_reward_percent as u128,
state.mixnode_rewarded_set_size as u128,
state.mixnode_active_set_size as u128,
crate::rewards::storage::circulating_supply(storage)?.u128(),
constants::SYBIL_RESISTANCE_PERCENT,
constants::ACTIVE_SET_WORK_FACTOR,
);
crate::rewards::storage::EPOCH_REWARD_PARAMS.save(storage, epoch_id, &epoch_reward_params)?;
Ok(epoch_reward_params)
}
pub fn generate_storage_key(address: &Addr, proxy: Option<&Addr>) -> Vec<u8> {
if let Some(proxy) = &proxy {
+16 -19
View File
@@ -1,11 +1,13 @@
use crate::constants::{INTERVAL_REWARD_PERCENT, SYBIL_RESISTANCE_PERCENT};
use crate::contract::{INITIAL_MIXNODE_PLEDGE, INITIAL_REWARD_POOL};
use std::time::Duration;
use crate::contract::INITIAL_MIXNODE_PLEDGE;
use crate::mixnodes::storage as mixnodes_storage;
use crate::{mixnodes::storage::StoredMixnodeBond, support::tests};
use config::defaults::{DENOM, TOTAL_SUPPLY};
use cosmwasm_std::{coin, Addr, Coin};
use mixnet_contract_common::mixnode::NodeRewardParams;
use mixnet_contract_common::{Gateway, GatewayBond, Layer, MixNode};
use config::defaults::DENOM;
use cosmwasm_std::{coin, Addr, Coin, Uint128};
use mixnet_contract_common::reward_params::NodeRewardParams;
use mixnet_contract_common::{Gateway, GatewayBond, Interval, Layer, MixNode};
use time::OffsetDateTime;
pub fn mix_node_fixture() -> MixNode {
MixNode {
@@ -57,6 +59,8 @@ pub(crate) fn stored_mixnode_bond_fixture(owner: &str) -> mixnodes_storage::Stor
..super::fixtures::mix_node_fixture()
},
None,
Uint128::zero(),
None,
)
}
@@ -74,17 +78,10 @@ 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) * INTERVAL_REWARD_PERCENT as u128,
50 as u128,
25 as u128,
0,
TOTAL_SUPPLY - INITIAL_REWARD_POOL,
uptime,
SYBIL_RESISTANCE_PERCENT,
true,
10,
)
pub fn node_reward_params_fixture(uptime: u128) -> NodeRewardParams {
NodeRewardParams::new(0, uptime, true)
}
pub fn epoch_fixture() -> Interval {
Interval::new(1, OffsetDateTime::now_utc(), Duration::from_secs(3600))
}
+6 -7
View File
@@ -29,7 +29,6 @@ pub mod test_helpers {
use cosmwasm_std::{coin, Env, Timestamp};
use cosmwasm_std::{Addr, StdResult, Storage};
use cosmwasm_std::{Empty, MemoryStorage};
use cw_storage_plus::PrimaryKey;
use mixnet_contract_common::{Delegation, Gateway, IdentityKeyRef, InstantiateMsg, MixNode};
use rand::thread_rng;
@@ -114,27 +113,27 @@ pub mod test_helpers {
};
delegations_storage::delegations()
.save(storage, delegation.storage_key().joined_key(), &delegation)
.save(storage, delegation.storage_key(), &delegation)
.unwrap();
}
pub(crate) fn read_delegation(
storage: &dyn Storage,
mix: impl Into<String>,
owner: impl Into<String>,
owner: impl Into<Vec<u8>>,
block_height: u64,
) -> Option<Delegation> {
delegations_storage::delegations()
.may_load(storage, (mix.into(), owner.into()).joined_key())
.may_load(storage, (mix.into(), owner.into(), block_height))
.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)
(interval_storage::current_interval(storage)
.unwrap()
.next_interval()
.next()
.start_unix_timestamp()
+ 123) as u64,
);
+1 -1
View File
@@ -19,7 +19,7 @@ vesting-contract-common = { path = "../../common/cosmwasm-smart-contracts/vestin
config = { path = "../../common/config" }
cosmwasm-std = { version = "1.0.0-beta4"}
cw-storage-plus = { version = "0.11.1", features = ["iterator"] }
cw-storage-plus = { version = "0.12.1", features = ["iterator"] }
schemars = "0.8"
serde = { version = "1.0", default-features = false, features = ["derive"] }
+1 -1
View File
@@ -204,7 +204,7 @@ impl Account {
prev_len = block_heights.len();
start_after = block_heights.last().map(|last| Bound::exclusive_int(*last));
start_after = block_heights.last().map(|last| Bound::exclusive(*last));
if start_after.is_none() {
break;
}
+13 -2
View File
@@ -949,6 +949,17 @@ dependencies = [
"serde",
]
[[package]]
name = "cw-storage-plus"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c087ff98fb0475db4c2b5298a5fd12b2848d2854b39d1115d930ee6da24d1eed"
dependencies = [
"cosmwasm-std",
"schemars",
"serde",
]
[[package]]
name = "darling"
version = "0.10.2"
@@ -5257,7 +5268,7 @@ version = "0.1.0"
dependencies = [
"config",
"cosmwasm-std",
"cw-storage-plus",
"cw-storage-plus 0.12.1",
"mixnet-contract-common",
"schemars",
"serde",
@@ -5271,7 +5282,7 @@ version = "0.1.0"
dependencies = [
"config",
"cosmwasm-std",
"cw-storage-plus",
"cw-storage-plus 0.11.1",
"mixnet-contract-common",
"schemars",
"serde",
+1 -1
View File
@@ -1,2 +1,2 @@
export type Operation = "Upload" | "Init" | "Migrate" | "ChangeAdmin" | "Send" | "BondMixnode" | "BondMixnodeOnBehalf" | "UnbondMixnode" | "UnbondMixnodeOnBehalf" | "UpdateMixnodeConfig" | "DelegateToMixnode" | "DelegateToMixnodeOnBehalf" | "UndelegateFromMixnode" | "UndelegateFromMixnodeOnBehalf" | "BondGateway" | "BondGatewayOnBehalf" | "UnbondGateway" | "UnbondGatewayOnBehalf" | "UpdateContractSettings" | "BeginMixnodeRewarding" | "FinishMixnodeRewarding" | "TrackUnbondGateway" | "TrackUnbondMixnode" | "WithdrawVestedCoins" | "TrackUndelegation" | "CreatePeriodicVestingAccount" | "AdvanceCurrentInterval" | "WriteRewardedSet" | "ClearRewardedSet" | "UpdateMixnetAddress";
export type Operation = "Upload" | "Init" | "Migrate" | "ChangeAdmin" | "Send" | "BondMixnode" | "BondMixnodeOnBehalf" | "UnbondMixnode" | "UnbondMixnodeOnBehalf" | "UpdateMixnodeConfig" | "DelegateToMixnode" | "DelegateToMixnodeOnBehalf" | "UndelegateFromMixnode" | "UndelegateFromMixnodeOnBehalf" | "BondGateway" | "BondGatewayOnBehalf" | "UnbondGateway" | "UnbondGatewayOnBehalf" | "UpdateContractSettings" | "BeginMixnodeRewarding" | "FinishMixnodeRewarding" | "TrackUnbondGateway" | "TrackUnbondMixnode" | "WithdrawVestedCoins" | "TrackUndelegation" | "CreatePeriodicVestingAccount" | "AdvanceCurrentInterval" | "AdvanceCurrentEpoch" | "WriteRewardedSet" | "ClearRewardedSet" | "UpdateMixnetAddress" | "CheckpointMixnodes" | "ReconcileDelegations";
+4
View File
@@ -364,6 +364,8 @@ impl Config {
self.network_monitor.eth_endpoint.clone()
}
// TODO: Remove if still unused
#[allow(dead_code)]
pub fn get_rewarding_enabled(&self) -> bool {
self.rewarding.enabled
}
@@ -438,6 +440,8 @@ impl Config {
self.network_monitor.all_validator_apis.clone()
}
// TODO: Remove if still unused
#[allow(dead_code)]
pub fn get_minimum_interval_monitor_threshold(&self) -> u8 {
self.rewarding.minimum_interval_monitor_threshold
}
+15 -18
View File
@@ -2,10 +2,10 @@
// 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::reward_params::EpochRewardParams;
use mixnet_contract_common::{
GatewayBond, IdentityKey, IdentityKeyRef, Interval, MixNodeBond, RewardedSetNodeStatus,
};
@@ -23,6 +23,8 @@ use validator_client::nymd::CosmWasmClient;
pub(crate) mod routes;
type Epoch = Interval;
pub struct ValidatorCacheRefresher<C> {
nymd_client: Client<C>,
cache: ValidatorCache,
@@ -43,8 +45,8 @@ struct ValidatorCacheInner {
rewarded_set: Cache<Vec<MixNodeBond>>,
active_set: Cache<Vec<MixNodeBond>>,
current_reward_params: Cache<IntervalRewardParams>,
current_interval: Cache<Interval>,
current_reward_params: Cache<EpochRewardParams>,
current_epoch: Cache<Interval>,
}
fn current_unix_timestamp() -> i64 {
@@ -131,10 +133,7 @@ impl<C> ValidatorCacheRefresher<C> {
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 epoch_rewarding_params = self.nymd_client.get_current_epoch_reward_params().await?;
let current_interval = self.nymd_client.get_current_interval().await?;
info!(
@@ -149,7 +148,7 @@ impl<C> ValidatorCacheRefresher<C> {
gateways,
rewarded_set,
active_set,
interval_rewarding_params,
epoch_rewarding_params,
current_interval,
)
.await;
@@ -219,8 +218,8 @@ impl ValidatorCache {
gateways: Vec<GatewayBond>,
rewarded_set: Vec<MixNodeBond>,
active_set: Vec<MixNodeBond>,
interval_rewarding_params: IntervalRewardParams,
current_interval: Interval,
epoch_rewarding_params: EpochRewardParams,
current_epoch: Epoch,
) {
let mut inner = self.inner.write().await;
@@ -228,10 +227,8 @@ impl ValidatorCache {
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);
inner.current_reward_params.update(epoch_rewarding_params);
inner.current_epoch.update(current_epoch);
}
pub async fn mixnodes(&self) -> Cache<Vec<MixNodeBond>> {
@@ -250,12 +247,12 @@ impl ValidatorCache {
self.inner.read().await.active_set.clone()
}
pub(crate) async fn interval_reward_params(&self) -> Cache<IntervalRewardParams> {
pub(crate) async fn epoch_reward_params(&self) -> Cache<EpochRewardParams> {
self.inner.read().await.current_reward_params.clone()
}
pub(crate) async fn current_interval(&self) -> Cache<Interval> {
self.inner.read().await.current_interval.clone()
self.inner.read().await.current_epoch.clone()
}
pub async fn mixnode_details(
@@ -320,10 +317,10 @@ impl ValidatorCacheInner {
gateways: Cache::default(),
rewarded_set: Cache::default(),
active_set: Cache::default(),
current_reward_params: Cache::new(IntervalRewardParams::new_empty()),
current_reward_params: Cache::new(EpochRewardParams::new_empty()),
// setting it to a dummy value on creation is fine, as nothing will be able to ready from it
// since 'initialised' flag won't be set
current_interval: Cache::new(Interval::new(
current_epoch: Cache::new(Interval::new(
u32::MAX,
OffsetDateTime::UNIX_EPOCH,
Duration::default(),
+13 -48
View File
@@ -9,7 +9,6 @@ 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::Rewarder;
use crate::storage::ValidatorApiStorage;
use ::config::NymConfig;
use anyhow::Result;
@@ -25,8 +24,8 @@ use std::sync::Arc;
use std::time::Duration;
use tokio::sync::Notify;
use url::Url;
use validator_client::nymd::SigningNymdClient;
use validator_client::ValidatorClientError;
// use validator_client::nymd::SigningNymdClient;
// use validator_client::ValidatorClientError;
use crate::rewarded_set_updater::RewardedSetUpdater;
#[cfg(feature = "coconut")]
@@ -38,7 +37,6 @@ mod network_monitor;
mod node_status_api;
pub(crate) mod nymd_client;
mod rewarded_set_updater;
mod rewarding;
pub(crate) mod storage;
#[cfg(feature = "coconut")]
@@ -358,6 +356,8 @@ fn setup_network_monitor<'a>(
))
}
// TODO: Remove if still unused
#[allow(dead_code)]
fn expected_monitor_test_runs(config: &Config, interval_length: Duration) -> usize {
let test_delay = config.get_network_monitor_run_interval();
@@ -366,32 +366,6 @@ fn expected_monitor_test_runs(config: &Config, interval_length: Duration) -> usi
(interval_length.as_secs() / test_delay.as_secs()) as usize
}
async fn setup_rewarder(
config: &Config,
rocket: &Rocket<Ignite>,
nymd_client: &Client<SigningNymdClient>,
) -> 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();
let rewarding_interval_length = nymd_client.get_current_interval().await?.length();
Ok(Some(Rewarder::new(
nymd_client.clone(),
validator_cache,
node_status_storage,
expected_monitor_test_runs(config, rewarding_interval_length),
config.get_minimum_interval_monitor_threshold(),
)))
} else if config.get_rewarding_enabled() {
warn!("Cannot enable rewarding with the network monitor being disabled");
Ok(None)
} else {
Ok(None)
}
}
async fn setup_rocket(config: &Config, liftoff_notify: Arc<Notify>) -> Result<Rocket<Ignite>> {
// let's build our rocket!
let rocket = rocket::build()
@@ -482,27 +456,18 @@ async fn run_validator_api(matches: ArgMatches<'static>) -> Result<()> {
// setup our daily uptime updater. Note that if network monitor is disabled, then we have
// no data for the updates and hence we don't need to start it up
let storage = rocket.state::<ValidatorApiStorage>().unwrap().clone();
let uptime_updater = HistoricalUptimeUpdater::new(storage);
let uptime_updater = HistoricalUptimeUpdater::new(storage.clone());
tokio::spawn(async move { uptime_updater.run().await });
if let Some(rewarder) = setup_rewarder(&config, &rocket, &nymd_client).await? {
info!("Periodic rewarding is starting...");
let mut rewarded_set_updater = RewardedSetUpdater::new(
nymd_client,
rewarded_set_update_notify,
validator_cache.clone(),
storage,
);
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.");
}
// spawn rewarded set updater
tokio::spawn(async move { rewarded_set_updater.run().await.unwrap() });
} else {
let nymd_client = Client::new_query(&config);
let validator_cache_refresher = ValidatorCacheRefresher::new(
+11 -9
View File
@@ -7,6 +7,7 @@ use crate::node_status_api::models::{
};
use crate::storage::ValidatorApiStorage;
use crate::ValidatorCache;
use mixnet_contract_common::reward_params::{NodeRewardParams, RewardParams};
use rocket::http::Status;
use rocket::serde::json::Json;
use rocket::State;
@@ -115,7 +116,7 @@ pub(crate) async fn get_mixnode_reward_estimation(
) -> Result<Json<RewardEstimationResponse>, ErrorResponse> {
let (bond, status) = cache.mixnode_details(&identity).await;
if let Some(bond) = bond {
let interval_reward_params = cache.interval_reward_params().await;
let interval_reward_params = cache.epoch_reward_params().await;
let as_at = interval_reward_params.timestamp();
let interval_reward_params = interval_reward_params.into_inner();
@@ -128,8 +129,9 @@ pub(crate) async fn get_mixnode_reward_estimation(
)
.await
.map_err(|err| ErrorResponse::new(err.to_string(), Status::NotFound))?;
match interval_reward_params.estimate_reward(&bond, uptime.u8(), status.is_active()) {
let node_reward_params = NodeRewardParams::new(0, uptime.u8() as u128, status.is_active());
let reward_params = RewardParams::new(interval_reward_params, node_reward_params);
match bond.estimate_reward(&reward_params) {
Ok((
estimated_total_node_reward,
estimated_operator_reward,
@@ -166,13 +168,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 interval_reward_params = cache.interval_reward_params().await;
let interval_reward_params = cache.epoch_reward_params().await;
let as_at = interval_reward_params.timestamp();
let interval_reward_params = interval_reward_params.into_inner();
let saturation = bond.stake_saturation(
interval_reward_params.circulating_supply,
interval_reward_params.rewarded_set_size,
interval_reward_params.circulating_supply(),
interval_reward_params.rewarded_set_size() as u32,
);
Ok(Json(StakeSaturationResponse {
@@ -200,9 +202,9 @@ pub(crate) async fn get_mixnode_inclusion_probability(
.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 rewarding_params = cache.epoch_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;
+45 -78
View File
@@ -2,12 +2,12 @@
// SPDX-License-Identifier: Apache-2.0
use crate::config::Config;
use crate::rewarding::{error::RewardingError, IntervalRewardParams, MixnodeToReward};
use crate::rewarded_set_updater::{error::RewardingError, MixnodeToReward};
use config::defaults::{DEFAULT_NETWORK, DEFAULT_VALIDATOR_API_PORT};
use mixnet_contract_common::{
ContractStateParams, Delegation, ExecuteMsg, GatewayBond, IdentityKey, Interval, MixNodeBond,
MixnodeRewardingStatusResponse, RewardedSetNodeStatus, RewardedSetUpdateDetails,
MIXNODE_DELEGATORS_PAGE_LIMIT,
reward_params::EpochRewardParams, ContractStateParams, Delegation, ExecuteMsg, GatewayBond,
IdentityKey, Interval, MixNodeBond, MixnodeRewardingStatusResponse, RewardedSetNodeStatus,
RewardedSetUpdateDetails,
};
use serde::Serialize;
use std::sync::Arc;
@@ -95,6 +95,7 @@ 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>
@@ -145,9 +146,9 @@ impl<C> Client<C> {
self.0.read().await.get_contract_settings().await
}
pub(crate) async fn get_current_interval_reward_params(
pub(crate) async fn get_current_epoch_reward_params(
&self,
) -> Result<IntervalRewardParams, ValidatorClientError>
) -> Result<EpochRewardParams, ValidatorClientError>
where
C: CosmWasmClient + Sync,
{
@@ -157,19 +158,20 @@ impl<C> Client<C> {
let reward_pool = this.get_reward_pool().await?;
let interval_reward_percent = this.get_interval_reward_percent().await?;
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) * interval_reward_percent as u128,
active_set_work_factor: this.get_active_set_work_factor().await?,
};
let epoch_reward_params = EpochRewardParams::new(
(reward_pool / 100 / this.get_epochs_in_interval().await? as u128)
* interval_reward_percent as u128,
state.mixnode_rewarded_set_size as u128,
state.mixnode_active_set_size as u128,
this.get_circulating_supply().await?,
this.get_sybil_resistance_percent().await?,
this.get_active_set_work_factor().await?,
);
Ok(interval_reward_params)
Ok(epoch_reward_params)
}
#[allow(dead_code)]
pub(crate) async fn get_rewarding_status(
&self,
mix_identity: mixnet_contract_common::IdentityKey,
@@ -207,6 +209,7 @@ impl<C> Client<C> {
Ok(hash)
}
#[allow(dead_code)]
pub(crate) async fn get_mixnode_delegations(
&self,
identity: IdentityKey,
@@ -247,6 +250,7 @@ impl<C> Client<C> {
.await
}
#[allow(dead_code)]
pub(crate) async fn advance_current_interval(&self) -> Result<(), ValidatorClientError>
where
C: SigningCosmWasmClient + Sync,
@@ -255,6 +259,30 @@ impl<C> Client<C> {
Ok(())
}
pub(crate) async fn advance_current_epoch(&self) -> Result<(), ValidatorClientError>
where
C: SigningCosmWasmClient + Sync,
{
self.0.write().await.nymd.advance_current_epoch().await?;
Ok(())
}
pub(crate) async fn checkpoint_mixnodes(&self) -> Result<(), ValidatorClientError>
where
C: SigningCosmWasmClient + Sync,
{
self.0.write().await.nymd.checkpoint_mixnodes().await?;
Ok(())
}
pub(crate) async fn reconcile_delegations(&self) -> Result<(), ValidatorClientError>
where
C: SigningCosmWasmClient + Sync,
{
self.0.write().await.nymd.reconcile_delegations().await?;
Ok(())
}
pub(crate) async fn write_rewarded_set(
&self,
rewarded_set: Vec<IdentityKey>,
@@ -272,68 +300,7 @@ impl<C> Client<C> {
Ok(())
}
pub(crate) async fn reward_mixnode_and_all_delegators(
&self,
node: &MixnodeToReward,
interval_id: u32,
) -> Result<(), RewardingError>
where
C: SigningCosmWasmClient + Sync,
{
// start with the base call to reward operator and first page of delegators
let msgs = vec![(node.to_reward_execute_msg(interval_id), vec![])];
let memo = format!(
"operator + {} delegators rewarding",
MIXNODE_DELEGATORS_PAGE_LIMIT
);
self.execute_multiple_with_retry(msgs, Default::default(), memo)
.await?;
// reward rest of delegators (if applicable)
if node.total_delegations > MIXNODE_DELEGATORS_PAGE_LIMIT {
// the first 'batch' of delegators has been rewarded while rewarding the operator
let mut remaining_delegators = node.total_delegations - MIXNODE_DELEGATORS_PAGE_LIMIT;
let delegator_rewarding_msg = (
node.to_next_delegator_reward_execute_msg(interval_id),
vec![],
);
while remaining_delegators > 0 {
let delegators_in_call = remaining_delegators.min(MIXNODE_DELEGATORS_PAGE_LIMIT);
let msgs = vec![delegator_rewarding_msg.clone()];
let memo = format!("rewarding another {} delegators", delegators_in_call);
self.execute_multiple_with_retry(msgs, Default::default(), memo)
.await?;
remaining_delegators =
remaining_delegators.saturating_sub(MIXNODE_DELEGATORS_PAGE_LIMIT)
}
}
Ok(())
}
pub(crate) async fn reward_mix_delegators(
&self,
node: &MixnodeToReward,
interval_id: u32,
) -> Result<(), RewardingError>
where
C: SigningCosmWasmClient + Sync,
{
// 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(interval_id),
vec![],
);
let memo = "rewarding delegators".to_string();
self.execute_multiple_with_retry(vec![delegator_rewarding_msg], Default::default(), memo)
.await
}
pub(crate) async fn reward_mixnodes_with_single_page_of_delegators(
pub(crate) async fn reward_mixnodes(
&self,
nodes: &[MixnodeToReward],
interval_id: u32,
@@ -7,13 +7,12 @@ use validator_client::nymd::error::NymdError;
use validator_client::ValidatorClientError;
#[derive(Debug, Error)]
pub(crate) enum RewardingError {
pub enum RewardingError {
#[error("Could not distribute rewards as the contract address was unspecified")]
UnspecifiedContractAddress,
#[error("There were no mixnodes to reward (network is dead)")]
NoMixnodesToReward,
// #[error("There were no mixnodes to reward (network is dead)")]
// NoMixnodesToReward,
#[error("Failed to execute the smart contract - {0}")]
ContractExecutionFailure(NymdError),
+273 -9
View File
@@ -14,17 +14,66 @@
use crate::contract_cache::ValidatorCache;
use crate::nymd_client::Client;
use mixnet_contract_common::{IdentityKey, MixNodeBond};
use crate::storage::models::{
FailedMixnodeRewardChunk, PossiblyUnrewardedMixnode, RewardingReport,
};
use crate::storage::ValidatorApiStorage;
use mixnet_contract_common::reward_params::NodeRewardParams;
use mixnet_contract_common::ExecuteMsg;
use mixnet_contract_common::{IdentityKey, Interval, MixNodeBond};
use rand::prelude::SliceRandom;
use rand::rngs::OsRng;
use std::collections::HashSet;
use std::sync::Arc;
use std::time::Duration;
use time::OffsetDateTime;
use tokio::sync::Notify;
use tokio::time::sleep;
use validator_client::nymd::SigningNymdClient;
pub(crate) mod error;
use error::RewardingError;
#[derive(Debug, Clone)]
pub(crate) struct MixnodeToReward {
pub(crate) identity: IdentityKey,
/// Total number of individual addresses that have delegated to this particular node
// pub(crate) total_delegations: usize,
/// Node absolute uptime over total active set uptime
pub(crate) params: NodeRewardParams,
}
impl MixnodeToReward {
fn params(&self) -> NodeRewardParams {
self.params
}
pub(crate) fn to_reward_execute_msg(&self, interval_id: u32) -> ExecuteMsg {
ExecuteMsg::RewardMixnode {
identity: self.identity.clone(),
params: self.params(),
interval_id,
}
}
}
pub(crate) struct FailedMixnodeRewardChunkDetails {
possibly_unrewarded: Vec<MixnodeToReward>,
error_message: String,
}
// Epoch has all the same semantics as interval, but has a lower set duration
type Epoch = Interval;
pub struct RewardedSetUpdater {
nymd_client: Client<SigningNymdClient>,
update_rewarded_set_notify: Arc<Notify>,
validator_cache: ValidatorCache,
storage: ValidatorApiStorage,
epoch: Epoch,
}
impl RewardedSetUpdater {
@@ -32,11 +81,15 @@ impl RewardedSetUpdater {
nymd_client: Client<SigningNymdClient>,
update_rewarded_set_notify: Arc<Notify>,
validator_cache: ValidatorCache,
storage: ValidatorApiStorage,
) -> Self {
let epoch = Epoch::new(0, OffsetDateTime::now_utc(), Duration::from_secs(3600));
RewardedSetUpdater {
nymd_client,
update_rewarded_set_notify,
validator_cache,
storage,
epoch,
}
}
@@ -75,17 +128,220 @@ impl RewardedSetUpdater {
.collect()
}
async fn update_rewarded_set(&self) {
async fn rewarding_happened_at_epoch(&self) -> Result<bool, RewardingError> {
if let Some(entry) = self
.storage
.get_interval_rewarding_entry(self.epoch.start_unix_timestamp())
.await?
{
// log error if the attempt wasn't finished. This error implies the process has crashed
// during the rewards distribution
if !entry.finished {
error!(
"It seems that we haven't successfully finished distributing rewards at {}",
self.epoch
)
}
Ok(true)
} else {
Ok(false)
}
}
async fn reward_current_rewarded_set(&self) -> Result<(), RewardingError> {
let to_reward = self.nodes_to_reward().await?;
self.storage
.insert_started_epoch_rewarding(self.epoch)
.await?;
let failure_data = self.distribute_rewards(&to_reward, false).await;
let mut rewarding_report = RewardingReport {
interval_rewarding_id: self.epoch.id() as i64,
eligible_mixnodes: to_reward.len() as i64,
possibly_unrewarded_mixnodes: 0,
};
if let Some(failure_data) = failure_data {
rewarding_report.possibly_unrewarded_mixnodes =
failure_data.possibly_unrewarded.len() as i64;
if let Err(err) = self
.save_failure_information(failure_data, self.epoch.id() as i64)
.await
{
error!("failed to save information about rewarding failures!");
// TODO: should we just terminate the process here?
return Err(err);
}
}
self.storage
.finish_rewarding_interval_and_insert_report(rewarding_report)
.await?;
Ok(())
}
async fn save_failure_information(
&self,
failed_chunk: FailedMixnodeRewardChunkDetails,
interval_rewarding_id: i64,
) -> Result<(), RewardingError> {
// save the chunk
let chunk_id = self
.storage
.insert_failed_mixnode_reward_chunk(FailedMixnodeRewardChunk {
interval_rewarding_id,
error_message: failed_chunk.error_message,
})
.await?;
// and then all associated nodes
for node in failed_chunk.possibly_unrewarded.into_iter() {
self.storage
.insert_possibly_unrewarded_mixnode(PossiblyUnrewardedMixnode {
chunk_id,
identity: node.identity,
uptime: node.params.uptime() as u8,
})
.await?;
}
Ok(())
}
async fn distribute_rewards(
&self,
eligible_mixnodes: &[MixnodeToReward],
retry: bool,
) -> Option<FailedMixnodeRewardChunkDetails> {
if retry {
info!(
"Attempting to retry rewarding {} mixnodes...",
eligible_mixnodes.len()
)
} else {
info!(
"Attempting to reward {} mixnodes...",
eligible_mixnodes.len()
)
}
let mut failed_chunks = None;
let num_retries = 5;
let mut retry = 0;
let mut success = false;
loop {
match self
.nymd_client
.reward_mixnodes(eligible_mixnodes, self.epoch.id())
.await
{
Ok(_) => {
let total_rewarded = eligible_mixnodes.len();
info!("Rewarded {} mixnodes", total_rewarded);
success = false;
break;
}
Err(err) => {
if num_retries <= retry {
break;
}
retry += 1;
// this is a super weird edge case that we didn't catch change to sequence and
// resent rewards unnecessarily, but the mempool saved us from executing it again
// however, still we want to wait until we're sure we're into the next block
if !err.is_tendermint_duplicate() {
error!("failed to reward mixnodes... - {}", err);
failed_chunks = Some(FailedMixnodeRewardChunkDetails {
possibly_unrewarded: eligible_mixnodes.to_vec(),
error_message: err.to_string(),
});
}
sleep(Duration::from_secs(11)).await;
}
}
}
// Its all or nothing since we do not chunk
if success {
failed_chunks = None
}
failed_chunks
}
async fn nodes_to_reward(&self) -> Result<Vec<MixnodeToReward>, RewardingError> {
let active_set = self
.validator_cache
.active_set()
.await
.into_inner()
.into_iter()
.map(|bond| bond.mix_node.identity_key)
.collect::<HashSet<_>>();
let rewarded_set = self.validator_cache.rewarded_set().await.into_inner();
let mut eligible_nodes = Vec::with_capacity(rewarded_set.len());
for rewarded_node in rewarded_set.into_iter() {
let uptime = self
.storage
.get_average_mixnode_uptime_in_interval(
rewarded_node.identity(),
self.epoch.start_unix_timestamp(),
self.epoch.end_unix_timestamp(),
)
.await?;
let node_reward_params = NodeRewardParams::new(
0,
uptime.u8().into(),
active_set.contains(rewarded_node.identity()),
);
eligible_nodes.push(MixnodeToReward {
identity: rewarded_node.identity().clone(),
params: node_reward_params,
})
}
Ok(eligible_nodes)
}
// This is where the epoch gets advanced, and all epoch related transactions originate
async fn update_rewarded_set(&self) -> Result<(), RewardingError> {
// 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
let epoch_reward_params = self
.validator_cache
.interval_reward_params()
.epoch_reward_params()
.await
.into_inner();
let rewarded_set_size = rewarding_params.rewarded_set_size;
let active_set_size = rewarding_params.active_set_size;
// Reward all the nodes in the still current, soon to be previous rewarded set
if !self.rewarding_happened_at_epoch().await? {
self.reward_current_rewarded_set().await?;
}
// Reconcile delegations from the previous epoch
if let Err(err) = self.nymd_client.reconcile_delegations().await {
log::error!("failed to reconcile delegations - {}", err);
}
// Snapshot mixnodes for the next epoch
if let Err(err) = self.nymd_client.checkpoint_mixnodes().await {
log::error!("failed to checkpoint mixnodes - {}", err);
}
// Snapshot mixnodes for the next epoch
if let Err(err) = self.nymd_client.advance_current_epoch().await {
log::error!("failed to advance_epoch - {}", err);
}
let rewarded_set_size = epoch_reward_params.rewarded_set_size() as u32;
let active_set_size = epoch_reward_params.active_set_size() as u32;
// note that top k nodes are in the active set
let new_rewarded_set = self.determine_rewarded_set(all_nodes, rewarded_set_size);
@@ -94,19 +350,27 @@ impl RewardedSetUpdater {
.write_rewarded_set(new_rewarded_set, active_set_size)
.await
{
log::error!("failed to update the rewarded set - {}", err)
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
}
let cutoff = (self.epoch.end() - Duration::from_secs(86400)).unix_timestamp();
self.storage.purge_old_statuses(cutoff).await?;
Ok(())
}
pub(crate) async fn run(&self) {
pub(crate) async fn run(&mut self) -> Result<(), RewardingError> {
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;
self.epoch = self.epoch.next();
self.update_rewarded_set().await?;
}
#[allow(unreachable_code)]
Ok(())
}
}
-835
View File
@@ -1,835 +0,0 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::contract_cache::ValidatorCache;
use crate::node_status_api::ONE_DAY;
use crate::nymd_client::Client;
use crate::rewarding::error::RewardingError;
use crate::storage::models::{
FailedMixnodeRewardChunk, PossiblyUnrewardedMixnode, RewardingReport,
};
use crate::storage::ValidatorApiStorage;
use config::defaults::DEFAULT_NETWORK;
use log::{error, info};
use mixnet_contract_common::mixnode::NodeRewardParams;
use mixnet_contract_common::{
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 error;
#[derive(Copy, Clone)]
pub(crate) struct IntervalRewardParams {
pub(crate) reward_pool: u128,
pub(crate) circulating_supply: u128,
pub(crate) sybil_resistance_percent: u8,
pub(crate) rewarded_set_size: u32,
pub(crate) active_set_size: u32,
pub(crate) period_reward_pool: u128,
pub(crate) active_set_work_factor: u8,
}
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 {
IntervalRewardParams {
reward_pool: 0,
circulating_supply: 0,
sybil_resistance_percent: 0,
rewarded_set_size: 0,
active_set_size: 0,
period_reward_pool: 0,
active_set_work_factor: 0,
}
}
pub(crate) fn estimate_reward(
&self,
node: &MixNodeBond,
uptime: u8,
in_active_set: bool,
) -> Result<(u64, u64, u64), RewardingError> {
let node_reward_params = NodeRewardParams::new(
self.period_reward_pool,
self.rewarded_set_size.into(),
self.active_set_size.into(),
0,
self.circulating_supply,
uptime.into(),
self.sybil_resistance_percent,
in_active_set,
self.active_set_work_factor,
);
let total_node_reward = node.reward(&node_reward_params);
let operator_reward = node.operator_reward(&node_reward_params);
let delegators_reward =
node.reward_delegation(node.total_delegation().amount, &node_reward_params);
Ok((
total_node_reward
.reward()
.checked_to_num::<u128>()
.unwrap_or_default()
.try_into()?,
operator_reward.try_into()?,
delegators_reward.try_into()?,
))
}
}
#[derive(Debug, Clone)]
pub(crate) struct MixnodeToReward {
pub(crate) identity: IdentityKey,
/// Total number of individual addresses that have delegated to this particular node
pub(crate) total_delegations: usize,
/// Node absolute uptime over total active set uptime
pub(crate) params: NodeRewardParams,
}
impl MixnodeToReward {
fn params(&self) -> NodeRewardParams {
self.params
}
}
impl MixnodeToReward {
pub(crate) fn to_reward_execute_msg(&self, interval_id: u32) -> ExecuteMsg {
ExecuteMsg::RewardMixnode {
identity: self.identity.clone(),
params: self.params(),
interval_id,
}
}
pub(crate) fn to_next_delegator_reward_execute_msg(&self, interval_id: u32) -> ExecuteMsg {
ExecuteMsg::RewardNextMixDelegators {
mix_identity: self.identity.clone(),
interval_id,
}
}
}
pub(crate) struct FailedMixnodeRewardChunkDetails {
possibly_unrewarded: Vec<MixnodeToReward>,
error_message: String,
}
#[derive(Default)]
pub(crate) struct FailureData {
mixnodes: Option<Vec<FailedMixnodeRewardChunkDetails>>,
}
pub(crate) struct Rewarder {
nymd_client: Client<SigningNymdClient>,
validator_cache: ValidatorCache,
storage: ValidatorApiStorage,
/// 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_interval_monitor_runs: usize,
/// Minimum percentage of network monitor test runs reports required in order to distribute
/// rewards.
minimum_interval_monitor_threshold: u8,
}
impl Rewarder {
pub(crate) fn new(
nymd_client: Client<SigningNymdClient>,
validator_cache: ValidatorCache,
storage: ValidatorApiStorage,
expected_interval_monitor_runs: usize,
minimum_interval_monitor_threshold: u8,
) -> Self {
Rewarder {
nymd_client,
validator_cache,
storage,
expected_interval_monitor_runs,
minimum_interval_monitor_threshold,
}
}
/// Obtains the current number of delegators that have delegated their stake towards this particular mixnode.
///
/// # Arguments
///
/// * `identity`: identity key of the mixnode
async fn get_mixnode_delegators_count(
&self,
identity: IdentityKey,
) -> Result<usize, RewardingError> {
Ok(self
.nymd_client
.get_mixnode_delegations(identity)
.await?
.len())
}
/// 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
/// approximate the required gas fees when distributing the rewards.
///
/// # Arguments
///
/// * `interval`: current rewarding interval
async fn determine_eligible_mixnodes(
&self,
interval: Interval,
) -> Result<Vec<MixnodeToReward>, RewardingError> {
let interval_reward_params = self
.nymd_client
.get_current_interval_reward_params()
.await?;
info!("Rewarding pool stats");
info!(
"-- Reward pool: {} {}",
interval_reward_params.reward_pool,
DEFAULT_NETWORK.denom()
);
info!(
"---- Interval reward pool: {} {}",
interval_reward_params.period_reward_pool,
DEFAULT_NETWORK.denom()
);
info!(
"-- Circulating supply: {} {}",
interval_reward_params.circulating_supply,
DEFAULT_NETWORK.denom()
);
// 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 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(node.mix_node.identity_key.clone())
.await?;
nodes_with_delegations.push((node, delegator_count));
}
let mut eligible_nodes = Vec::with_capacity(nodes_with_delegations.len());
for (rewarded_node, total_delegations) in nodes_with_delegations.into_iter() {
let uptime = self
.storage
.get_average_mixnode_uptime_in_interval(
rewarded_node.identity(),
interval.start_unix_timestamp(),
interval.end_unix_timestamp(),
)
.await?;
eligible_nodes.push(MixnodeToReward {
identity: rewarded_node.identity().clone(),
total_delegations,
params: NodeRewardParams::new(
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,
interval_reward_params.circulating_supply,
uptime.u8().into(),
interval_reward_params.sybil_resistance_percent,
active_set.contains(rewarded_node.identity()),
interval_reward_params.active_set_work_factor,
),
})
}
Ok(eligible_nodes)
}
/// Check whether every node, and their delegators, on the provided list were fully rewarded
/// in the specified interval.
///
/// It is used to deal with edge cases such that mixnode had exactly full page of delegations and
/// somebody created a new delegation thus causing the "last" delegator to possibly be pushed
/// onto the next page that the validator API was not aware of.
///
/// * `eligible_mixnodes`: list of the nodes that were eligible to receive rewards.
/// * `interval_id`: nonce associated with the current rewarding interval
async fn verify_rewarding_completion(
&self,
eligible_mixnodes: &[MixnodeToReward],
current_rewarding_nonce: u32,
) -> (Vec<MixnodeToReward>, Vec<MixnodeToReward>) {
let mut unrewarded = Vec::new();
let mut further_delegators_present = Vec::new();
for mix in eligible_mixnodes {
match self
.nymd_client
.get_rewarding_status(mix.identity.clone(), current_rewarding_nonce)
.await
{
Ok(rewarding_status) => match rewarding_status.status {
// that case is super weird, it implies the node hasn't been rewarded at all!
// maybe the transaction timed out twice or something? In any case, we should attempt
// the reward for the final time!
None => unrewarded.push(mix.clone()),
Some(RewardingStatus::PendingNextDelegatorPage(_)) => {
further_delegators_present.push(mix.clone())
}
Some(RewardingStatus::Complete(_)) => {}
},
Err(err) => {
error!(
"failed to query rewarding status of {} - {}",
mix.identity, err
)
}
}
}
(unrewarded, further_delegators_present)
}
// Utility function to print to the stdout rewarding progress
fn print_rewarding_progress(&self, total_rewarded: usize, out_of: usize, is_retry: bool) {
let percentage = total_rewarded as f32 * 100.0 / out_of as f32;
if is_retry {
info!(
"Resent rewarding transaction for {} / {} mixnodes\t{:.2}%",
total_rewarded, out_of, percentage
);
} else {
info!(
"Sent rewarding transaction for {} / {} mixnodes\t{:.2}%",
total_rewarded, out_of, percentage
);
}
}
/// Using the list of mixnodes eligible for rewards, chunks it into pre-defined sized-chunks
/// and gives out the rewards by calling the smart contract.
///
/// Returns an optional vector containing list of chunks that experienced a smart contract
/// execution error during reward distribution. However, it does not necessarily imply they
/// were not rewarded. There are some edge cases where we time out waiting for block to be included
/// yet the transactions went through.
///
/// Only returns errors for problems originating from before smart contract was called, i.e.
/// we know for sure not a single node has been rewarded.
///
/// # Arguments
///
/// * `eligible_mixnodes`: list of the nodes that are eligible to receive rewards.
/// * `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],
interval_id: u32,
retry: bool,
) -> Option<Vec<FailedMixnodeRewardChunkDetails>> {
if retry {
info!(
"Attempting to retry rewarding {} mixnodes...",
eligible_mixnodes.len()
)
} else {
info!(
"Attempting to reward {} mixnodes...",
eligible_mixnodes.len()
)
}
let mut failed_chunks = Vec::new();
// construct chunks such that we reward at most MIXNODE_DELEGATORS_PAGE_LIMIT delegators per block
// nodes with > MIXNODE_DELEGATORS_PAGE_LIMIT delegators that have to be treated in a special way,
// because we cannot batch them together
let mut individually_rewarded = Vec::new();
// sets of nodes that together they have < MIXNODE_DELEGATORS_PAGE_LIMIT delegators
let mut batch_rewarded = vec![vec![]];
let mut current_batch_i = 0;
let mut current_batch_total = 0;
// right now put mixes into batches super naively, if it doesn't fit into the current one,
// create a new one.
for mix in eligible_mixnodes {
// if mixnode has uptime of 0, no rewarding will actually happen regardless of number of delegators,
// so we can just batch it with the current batch
if mix.params.uptime() == 0 {
batch_rewarded[current_batch_i].push(mix.clone());
continue;
}
if mix.total_delegations > MIXNODE_DELEGATORS_PAGE_LIMIT {
individually_rewarded.push(mix)
} else if current_batch_total + mix.total_delegations < MIXNODE_DELEGATORS_PAGE_LIMIT {
batch_rewarded[current_batch_i].push(mix.clone());
current_batch_total += mix.total_delegations;
} else {
batch_rewarded.push(vec![mix.clone()]);
current_batch_i += 1;
current_batch_total = 0;
}
}
let mut total_rewarded = 0;
// start rewarding, first the nodes that are dealt with individually, i.e. nodes that
// need to have their own special blocks due to number of delegators
for mix in individually_rewarded {
if let Err(err) = self
.nymd_client
.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
// resent rewards unnecessarily, but the mempool saved us from executing it again
// however, still we want to wait until we're sure we're into the next block
if !err.is_tendermint_duplicate() {
error!("failed to reward mixnode with all delegators... - {}", err);
failed_chunks.push(FailedMixnodeRewardChunkDetails {
possibly_unrewarded: vec![mix.clone()],
error_message: err.to_string(),
});
}
sleep(Duration::from_secs(11)).await;
}
total_rewarded += 1;
self.print_rewarding_progress(total_rewarded, eligible_mixnodes.len(), retry);
}
// then we move onto the chunks
for mix_chunk in batch_rewarded {
if mix_chunk.is_empty() {
continue;
}
if let Err(err) = self
.nymd_client
.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
// resent rewards unnecessarily, but the mempool saved us from executing it again
// however, still we want to wait until we're sure we're into the next block
if !err.is_tendermint_duplicate() {
error!("failed to reward mixnodes... - {}", err);
failed_chunks.push(FailedMixnodeRewardChunkDetails {
possibly_unrewarded: mix_chunk.to_vec(),
error_message: err.to_string(),
});
}
sleep(Duration::from_secs(11)).await;
}
total_rewarded += mix_chunk.len();
self.print_rewarding_progress(total_rewarded, eligible_mixnodes.len(), retry);
}
if failed_chunks.is_empty() {
None
} else {
Some(failed_chunks)
}
}
/// For each mixnode on the list, try to "continue" rewarding its delegators.
/// Note: due to the checks inside the smart contract, it's impossible to accidentally
/// reward the same mixnode (or delegator) twice during particular rewarding interval.
///
/// 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 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;
info!(
"Sending rewarding transaction for missed delegators ({} / {} mixnodes re-checked)",
total_resent,
nodes.len()
);
if let Err(err) = self
.nymd_client
.reward_mix_delegators(missed_node, interval_id)
.await
{
warn!(
"failed to attempt to reward missed delegators of node {} - {}",
missed_node.identity, err
)
}
}
}
/// Using the list of active mixnode and gateways, determine which of them are eligible for
/// rewarding and distribute the rewards.
///
/// # Arguments
///
/// * `interval_rewarding_id`: id of the current interval rewarding as stored in the database.
/// * `interval`: current rewarding interval
async fn distribute_rewards(
&self,
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(interval).await?;
if eligible_mixnodes.is_empty() {
return Err(RewardingError::NoMixnodesToReward);
}
let total_eligible = eligible_mixnodes.len();
failure_data.mixnodes = self
.distribute_rewards_to_mixnodes(&eligible_mixnodes, interval.id(), false)
.await;
let mut nodes_to_verify = eligible_mixnodes;
// if there's some underlying networking error or something, don't keep retrying forever
let mut retries_allowed = 5;
loop {
if retries_allowed <= 0 {
break;
}
let (unrewarded, mut pending_delegators) = self
.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
break;
}
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, interval.id(), true)
.await;
}
if !pending_delegators.is_empty() {
self.reward_missed_delegators(&pending_delegators, interval.id())
.await;
}
// no point in verifying EVERYTHING again, just check the nodes that went through retries
nodes_to_verify = unrewarded;
nodes_to_verify.append(&mut pending_delegators);
retries_allowed -= 1;
}
let report = RewardingReport {
interval_rewarding_id: interval_rewarding_database_id,
eligible_mixnodes: total_eligible as i64,
possibly_unrewarded_mixnodes: failure_data
.mixnodes
.as_ref()
.map(|chunks| {
chunks
.iter()
.map(|chunk| chunk.possibly_unrewarded.len())
.sum::<usize>() as i64
})
.unwrap_or_default(),
};
self.nymd_client.advance_current_interval().await?;
if failure_data.mixnodes.is_none() {
Ok((report, None))
} else {
Ok((report, Some(failure_data)))
}
}
/// Saves information about possibly failed rewarding for future manual inspection.
///
/// Currently there is no automated recovery mechanism.
///
/// # Arguments
///
/// * `failure_data`: information regarding nodes that might have not received reward this interval.
///
/// * `interval_rewarding_id`: id of the current interval rewarding.
async fn save_failure_information(
&self,
failure_data: FailureData,
interval_rewarding_id: i64,
) -> Result<(), RewardingError> {
if let Some(failed_mixnode_chunks) = failure_data.mixnodes {
for failed_chunk in failed_mixnode_chunks.into_iter() {
// save the chunk
let chunk_id = self
.storage
.insert_failed_mixnode_reward_chunk(FailedMixnodeRewardChunk {
interval_rewarding_id,
error_message: failed_chunk.error_message,
})
.await?;
// and then all associated nodes
for node in failed_chunk.possibly_unrewarded.into_iter() {
self.storage
.insert_possibly_unrewarded_mixnode(PossiblyUnrewardedMixnode {
chunk_id,
identity: node.identity,
uptime: node.params.uptime() as u8,
})
.await?;
}
}
}
Ok(())
}
/// Determines whether this validator has already distributed rewards for the specified interval
/// so that it wouldn't accidentally attempt to do it again.
///
/// # Arguments
///
/// * `interval`: interval to check
async fn check_if_rewarding_happened_at_interval(
&self,
interval: Interval,
) -> Result<bool, RewardingError> {
if let Some(entry) = self
.storage
.get_interval_rewarding_entry(interval.start_unix_timestamp())
.await?
{
// log error if the attempt wasn't finished. This error implies the process has crashed
// during the rewards distribution
if !entry.finished {
error!(
"It seems that we haven't successfully finished distributing rewards at {}",
interval
)
}
Ok(true)
} else {
Ok(false)
}
}
/// 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
///
/// * `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 interval and we have enough monitor test data
Ok(true)
}
}
/// Distribute rewards to all eligible mixnodes and gateways on the network.
///
/// # Arguments
///
/// * `interval`: current rewarding interval
async fn perform_rewarding(&self, interval: Interval) -> Result<(), RewardingError> {
info!(
"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 interval_rewarding_id = self
.storage
.insert_started_interval_rewarding(
interval.start_unix_timestamp(),
interval.end_unix_timestamp(),
)
.await?;
let (report, failure_data) = self
.distribute_rewards(interval_rewarding_id, interval)
.await?;
self.storage
.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, interval_rewarding_id)
.await
{
error!("failed to save information about rewarding failures!");
// TODO: should we just terminate the process here?
return Err(err);
}
}
// 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 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 interval.
///
/// # Arguments
///
/// * `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 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)
}
async fn sync_up_rewarding_intervals(&self) -> Result<(), RewardingError> {
let mut 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,
};
// 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 {
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;
}
}
}
}
+13 -11
View File
@@ -1,6 +1,8 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use mixnet_contract_common::Interval;
use crate::network_monitor::monitor::summary_producer::NodeResult;
use crate::node_status_api::models::{HistoricalUptime, Uptime};
use crate::node_status_api::utils::ActiveNodeStatuses;
@@ -673,18 +675,21 @@ impl StorageManager {
///
/// * `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(
pub(super) async fn insert_new_epoch_rewarding(
&self,
interval_start_timestamp: i64,
interval_end_timestamp: i64,
epoch: Interval,
) -> Result<i64, sqlx::Error> {
let id = epoch.id();
let start = epoch.start_unix_timestamp();
let end = epoch.end_unix_timestamp();
let res = sqlx::query!(
r#"
INSERT INTO interval_rewarding (interval_start_timestamp, interval_end_timestamp, finished)
VALUES (?, ?, 0)
INSERT INTO interval_rewarding (id, interval_start_timestamp, interval_end_timestamp, finished)
VALUES (?, ?, ?, 0)
"#,
interval_start_timestamp,
interval_end_timestamp,
id,
start,
end,
)
.execute(&self.connection_pool)
.await?;
@@ -697,10 +702,7 @@ impl StorageManager {
/// # Arguments
///
/// * `id`: id of the entry we want to update.
pub(super) async fn update_finished_interval_rewarding(
&self,
id: i64,
) -> Result<(), sqlx::Error> {
pub async fn update_finished_interval_rewarding(&self, id: i64) -> Result<(), sqlx::Error> {
sqlx::query!(
r#"
UPDATE interval_rewarding
+5 -5
View File
@@ -13,6 +13,7 @@ use crate::storage::models::{
FailedMixnodeRewardChunk, IntervalRewarding, NodeStatus, PossiblyUnrewardedMixnode,
RewardingReport, TestingRoute,
};
use mixnet_contract_common::Interval;
use rocket::fairing::{self, AdHoc};
use rocket::{Build, Rocket};
use sqlx::ConnectOptions;
@@ -25,7 +26,7 @@ pub(crate) mod models;
// note that clone here is fine as upon cloning the same underlying pool will be used
#[derive(Clone)]
pub(crate) struct ValidatorApiStorage {
manager: StorageManager,
pub manager: StorageManager,
}
impl ValidatorApiStorage {
@@ -688,13 +689,12 @@ impl ValidatorApiStorage {
///
/// * `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(
pub(crate) async fn insert_started_epoch_rewarding(
&self,
interval_start_timestamp: i64,
interval_end_timestamp: i64,
epoch: Interval,
) -> Result<i64, ValidatorApiStorageError> {
self.manager
.insert_new_interval_rewarding(interval_start_timestamp, interval_end_timestamp)
.insert_new_epoch_rewarding(epoch)
.await
.map_err(|_| ValidatorApiStorageError::InternalDatabaseError)
}