Feature/rewarding interval updates (#880)

* Introduced rewarding_interval_nonce to contract state

* Queries for ibid.

* Mixnode demanded set size

* Routes for obtaining demanded/active mixnode sets

* Testing only demanded nodes

* Typo

* Initial state

* Feature-locking unused imports

* Generating pseudorandom (with deterministic seed) demanded mixnodes set

* cargo fmt

* Fixed tauri state

* Renamed network monitor address to the rewarding validator

* [ci skip] Generate TS types

* Notice for the future

* Transactions to begin/finish mixnode rewarding + double rewarding protection

* Validator API using new contract calls

* Removed dead code from an old experiment

* [ci skip] Generate TS types

* Removed unused import

* Renamed 'demanded' set to 'rewarded' set

* Some renaming action

* [ci skip] Generate TS types

* Fixed post-merge dependency issue in tests

* Post merge test fix

Co-authored-by: jstuczyn <jstuczyn@users.noreply.github.com>
This commit is contained in:
Jędrzej Stuczyński
2021-11-10 15:28:16 +00:00
committed by GitHub
parent 04636a569a
commit 7e1cf2f105
15 changed files with 1032 additions and 131 deletions
@@ -23,10 +23,11 @@ pub enum Operation {
BondGateway,
UnbondGateway,
DelegateToGateway,
UndelegateFromGateway,
UpdateStateParams,
BeginMixnodeRewarding,
FinishMixnodeRewarding,
}
pub(crate) fn calculate_fee(gas_price: &GasPrice, gas_limit: Gas) -> Coin {
@@ -43,13 +44,13 @@ impl fmt::Display for Operation {
Operation::Send => f.write_str("Send"),
Operation::BondMixnode => f.write_str("BondMixnode"),
Operation::UnbondMixnode => f.write_str("UnbondMixnode"),
Operation::DelegateToMixnode => f.write_str("DelegateToMixnode"),
Operation::UndelegateFromMixnode => f.write_str("UndelegateFromMixnode"),
Operation::BondGateway => f.write_str("BondGateway"),
Operation::UnbondGateway => f.write_str("UnbondGateway"),
Operation::DelegateToGateway => f.write_str("DelegateToGateway"),
Operation::UndelegateFromGateway => f.write_str("UndelegateFromGateway"),
Operation::DelegateToMixnode => f.write_str("DelegateToMixnode"),
Operation::UndelegateFromMixnode => f.write_str("UndelegateFromMixnode"),
Operation::UpdateStateParams => f.write_str("UpdateStateParams"),
Operation::BeginMixnodeRewarding => f.write_str("BeginMixnodeRewarding"),
Operation::FinishMixnodeRewarding => f.write_str("FinishMixnodeRewarding"),
}
}
}
@@ -71,10 +72,10 @@ impl Operation {
Operation::BondGateway => 175_000u64.into(),
Operation::UnbondGateway => 175_000u64.into(),
Operation::DelegateToGateway => 175_000u64.into(),
Operation::UndelegateFromGateway => 175_000u64.into(),
Operation::UpdateStateParams => 175_000u64.into(),
Operation::BeginMixnodeRewarding => 175_000u64.into(),
Operation::FinishMixnodeRewarding => 175_000u64.into(),
}
}
@@ -711,6 +711,54 @@ impl<C> NymdClient<C> {
)
.await
}
pub async fn begin_mixnode_rewarding(
&self,
rewarding_interval_nonce: u32,
) -> Result<ExecuteResult, NymdError>
where
C: SigningCosmWasmClient + Sync,
{
let fee = self.get_fee(Operation::BeginMixnodeRewarding);
let req = ExecuteMsg::BeginMixnodeRewarding {
rewarding_interval_nonce,
};
self.client
.execute(
self.address(),
self.contract_address()?,
&req,
fee,
"Beginning mixnode rewarding procedure",
Vec::new(),
)
.await
}
pub async fn finish_mixnode_rewarding(
&self,
rewarding_interval_nonce: u32,
) -> Result<ExecuteResult, NymdError>
where
C: SigningCosmWasmClient + Sync,
{
let fee = self.get_fee(Operation::FinishMixnodeRewarding);
let req = ExecuteMsg::FinishMixnodeRewarding {
rewarding_interval_nonce,
};
self.client
.execute(
self.address(),
self.contract_address()?,
&req,
fee,
"Finishing mixnode rewarding procedure",
Vec::new(),
)
.await
}
}
fn cosmwasm_coin_to_cosmos_coin(coin: Coin) -> CosmosCoin {
+16
View File
@@ -32,16 +32,32 @@ pub enum ExecuteMsg {
mix_identity: IdentityKey,
},
BeginMixnodeRewarding {
// nonce of the current rewarding interval
rewarding_interval_nonce: u32,
},
RewardMixnode {
identity: IdentityKey,
// percentage value in range 0-100
uptime: u32,
// nonce of the current rewarding interval
rewarding_interval_nonce: u32,
},
FinishMixnodeRewarding {
// nonce of the current rewarding interval
rewarding_interval_nonce: u32,
},
RewardMixnodeV2 {
identity: IdentityKey,
// percentage value in range 0-100
params: NodeRewardParams,
// nonce of the current rewarding interval
rewarding_interval_nonce: u32,
},
}
+1
View File
@@ -29,6 +29,7 @@ impl LayerDistribution {
#[derive(Debug, Default, Serialize, Deserialize, Copy, Clone, Eq, PartialEq)]
pub struct RewardingIntervalResponse {
pub current_rewarding_interval_starting_block: u64,
pub current_rewarding_interval_nonce: u32,
pub rewarding_in_progress: bool,
}
+1 -1
View File
@@ -63,7 +63,7 @@ pub fn default_api_endpoints() -> Vec<Url> {
}
pub const DEFAULT_MIXNET_CONTRACT_ADDRESS: &str = "punk10pyejy66429refv3g35g2t7am0was7yalwrzen";
pub const NETWORK_MONITOR_ADDRESS: &str = "punk1v9qauwdq5terag6uvfsdytcs2d0sdmfdy7hgk3";
pub const REWARDING_VALIDATOR_ADDRESS: &str = "punk1v9qauwdq5terag6uvfsdytcs2d0sdmfdy7hgk3";
/// How much bandwidth (in bytes) one token can buy
const BYTES_PER_TOKEN: u64 = 1024 * 1024 * 1024;
+33 -8
View File
@@ -7,7 +7,7 @@ use crate::helpers::calculate_epoch_reward_rate;
use crate::state::State;
use crate::storage::{config, layer_distribution};
use crate::{error::ContractError, queries, transactions};
use config::defaults::NETWORK_MONITOR_ADDRESS;
use config::defaults::REWARDING_VALIDATOR_ADDRESS;
use cosmwasm_std::{
entry_point, to_binary, Addr, Decimal, Deps, DepsMut, Env, MessageInfo, QueryResponse,
Response, Uint128,
@@ -42,7 +42,7 @@ fn default_initial_state(owner: Addr, env: Env) -> State {
State {
owner,
network_monitor_address: Addr::unchecked(NETWORK_MONITOR_ADDRESS), // we trust our hardcoded value
rewarding_validator_address: Addr::unchecked(REWARDING_VALIDATOR_ADDRESS), // we trust our hardcoded value
params: StateParams {
epoch_length: INITIAL_DEFAULT_EPOCH_LENGTH,
minimum_mixnode_bond: INITIAL_MIXNODE_BOND,
@@ -53,6 +53,7 @@ fn default_initial_state(owner: Addr, env: Env) -> State {
mixnode_active_set_size: INITIAL_MIXNODE_ACTIVE_SET_SIZE,
},
rewarding_interval_starting_block: env.block.height,
latest_rewarding_interval_nonce: 0,
rewarding_in_progress: false,
mixnode_epoch_bond_reward: calculate_epoch_reward_rate(
INITIAL_DEFAULT_EPOCH_LENGTH,
@@ -104,18 +105,42 @@ pub fn execute(
ExecuteMsg::UpdateStateParams(params) => {
transactions::try_update_state_params(deps, info, params)
}
ExecuteMsg::RewardMixnode { identity, uptime } => {
transactions::try_reward_mixnode(deps, env, info, identity, uptime)
}
ExecuteMsg::RewardMixnodeV2 { identity, params } => {
transactions::try_reward_mixnode_v2(deps, env, info, identity, params)
}
ExecuteMsg::RewardMixnode {
identity,
uptime,
rewarding_interval_nonce,
} => transactions::try_reward_mixnode(
deps,
env,
info,
identity,
uptime,
rewarding_interval_nonce,
),
ExecuteMsg::RewardMixnodeV2 {
identity,
params,
rewarding_interval_nonce,
} => transactions::try_reward_mixnode_v2(
deps,
env,
info,
identity,
params,
rewarding_interval_nonce,
),
ExecuteMsg::DelegateToMixnode { mix_identity } => {
transactions::try_delegate_to_mixnode(deps, env, info, mix_identity)
}
ExecuteMsg::UndelegateFromMixnode { mix_identity } => {
transactions::try_remove_delegation_from_mixnode(deps, info, mix_identity)
}
ExecuteMsg::BeginMixnodeRewarding {
rewarding_interval_nonce,
} => transactions::try_begin_mixnode_rewarding(deps, env, info, rewarding_interval_nonce),
ExecuteMsg::FinishMixnodeRewarding {
rewarding_interval_nonce,
} => transactions::try_finish_mixnode_rewarding(deps, info, rewarding_interval_nonce),
}
}
+12
View File
@@ -88,4 +88,16 @@ pub enum ContractError {
#[error("Invalid ratio")]
Ratio(#[from] mixnet_contract::error::MixnetContractError),
#[error("Received invalid rewarding interval nonce. Expected {expected}, received {received}")]
InvalidRewardingIntervalNonce { received: u32, expected: u32 },
#[error("Rewarding distribution is currently in progress")]
RewardingInProgress,
#[error("Rewarding distribution is currently not in progress")]
RewardingNotInProgress,
#[error("Mixnode {identity} has already been rewarded during the current rewarding interval")]
MixnodeAlreadyRewarded { identity: IdentityKey },
}
+3 -1
View File
@@ -91,6 +91,7 @@ pub(crate) fn query_rewarding_interval(deps: Deps) -> RewardingIntervalResponse
let state = config_read(deps.storage).load().unwrap();
RewardingIntervalResponse {
current_rewarding_interval_starting_block: state.rewarding_interval_starting_block,
current_rewarding_interval_nonce: state.latest_rewarding_interval_nonce,
rewarding_in_progress: state.rewarding_in_progress,
}
}
@@ -578,7 +579,7 @@ pub(crate) mod tests {
let dummy_state = State {
owner: Addr::unchecked("someowner"),
network_monitor_address: Addr::unchecked("monitor"),
rewarding_validator_address: Addr::unchecked("monitor"),
params: StateParams {
epoch_length: 1,
minimum_mixnode_bond: 123u128.into(),
@@ -589,6 +590,7 @@ pub(crate) mod tests {
mixnode_active_set_size: 500,
},
rewarding_interval_starting_block: 123,
latest_rewarding_interval_nonce: 0,
rewarding_in_progress: false,
mixnode_epoch_bond_reward: "1.23".parse().unwrap(),
mixnode_epoch_delegation_reward: "7.89".parse().unwrap(),
+2 -1
View File
@@ -9,13 +9,14 @@ use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct State {
pub owner: Addr, // only the owner account can update state
pub network_monitor_address: Addr,
pub rewarding_validator_address: Addr,
pub params: StateParams,
// keep track of the changes to the current rewarding interval,
// i.e. at which block has the latest rewarding occurred
// and whether another run is already in progress
pub rewarding_interval_starting_block: u64,
pub latest_rewarding_interval_nonce: u32,
pub rewarding_in_progress: bool,
// helper values to avoid having to recalculate them on every single payment operation
+31 -16
View File
@@ -37,6 +37,8 @@ const PREFIX_GATEWAYS_OWNERS: &[u8] = b"go";
const PREFIX_MIX_DELEGATION: &[u8] = b"md";
const PREFIX_REVERSE_MIX_DELEGATION: &[u8] = b"dm";
const PREFIX_REWARDED_MIXNODES: &[u8] = b"rm";
// Contract-level stuff
// TODO Unify bucket and mixnode storage functions
@@ -103,22 +105,6 @@ pub(crate) fn read_state_params(storage: &dyn Storage) -> StateParams {
config_read(storage).load().unwrap().params
}
pub(crate) fn read_mixnode_epoch_bond_reward_rate(storage: &dyn Storage) -> Decimal {
// same justification as in `read_state_params` for the unwrap
config_read(storage)
.load()
.unwrap()
.mixnode_epoch_bond_reward
}
pub(crate) fn read_mixnode_epoch_delegation_reward_rate(storage: &dyn Storage) -> Decimal {
// same justification as in `read_state_params` for the unwrap
config_read(storage)
.load()
.unwrap()
.mixnode_epoch_delegation_reward
}
pub fn layer_distribution(storage: &mut dyn Storage) -> Singleton<LayerDistribution> {
singleton(storage, LAYER_DISTRIBUTION_KEY)
}
@@ -196,6 +182,35 @@ pub fn mixnodes_owners_read(storage: &dyn Storage) -> ReadonlyBucket<IdentityKey
bucket_read(storage, PREFIX_MIXNODES_OWNERS)
}
// we want to treat this bucket as a set so we don't really care about what type of data is being stored.
// I went with u8 as after serialization it takes only a single byte of space, while if a `()` was used,
// it would have taken 4 bytes (representation of 'null')
pub fn rewarded_mixnodes(storage: &mut dyn Storage, rewarding_interval_nonce: u32) -> Bucket<u8> {
Bucket::multilevel(
storage,
&[
rewarding_interval_nonce.to_be_bytes().as_ref(),
PREFIX_REWARDED_MIXNODES,
],
)
}
// we want to treat this bucket as a set so we don't really care about what type of data is being stored.
// I went with u8 as after serialization it takes only a single byte of space, while if a `()` was used,
// it would have taken 4 bytes (representation of 'null')
pub fn rewarded_mixnodes_read(
storage: &dyn Storage,
rewarding_interval_nonce: u32,
) -> ReadonlyBucket<u8> {
ReadonlyBucket::multilevel(
storage,
&[
rewarding_interval_nonce.to_be_bytes().as_ref(),
PREFIX_REWARDED_MIXNODES,
],
)
}
// helpers
pub(crate) fn increase_mix_delegated_stakes(
storage: &mut dyn Storage,
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -10,6 +10,6 @@ export type Operation =
| "UndelegateFromMixnode"
| "BondGateway"
| "UnbondGateway"
| "DelegateToGateway"
| "UndelegateFromGateway"
| "UpdateStateParams";
| "UpdateStateParams"
| "BeginMixnodeRewarding"
| "FinishMixnodeRewarding";
+10
View File
@@ -162,6 +162,16 @@ impl ValidatorCache {
})
}
// NOTE: this does not guarantee consistent results between multiple validator APIs, because
// currently we do not guarantee the list of mixnodes (i.e. `mixnodes: &[MixNodeBond]`) will be the same -
// somebody might bond/unbond a node or change delegation between different cache refreshes.
//
// I guess that's not a problem right now and we can resolve it later. My idea for that would be as follows:
// since the demanded set changes only monthly, just write the identities of those nodes to the smart
// contract upon finished rewarding (this works under assumption of rewards being distributed by a single validator)
//
// alternatively we could have some state locking mechanism for the duration of determining the demanded set
// this could work with multiple validators via some multisig mechanism
fn determine_rewarded_set(
&self,
mixnodes: &[MixNodeBond],
+34 -1
View File
@@ -203,9 +203,42 @@ impl<C> Client<C> {
.calculate_custom_fee(total_gas_limit)
}
pub(crate) async fn begin_mixnode_rewarding(
&self,
rewarding_interval_nonce: u32,
) -> Result<(), RewardingError>
where
C: SigningCosmWasmClient + Sync,
{
self.0
.write()
.await
.nymd
.begin_mixnode_rewarding(rewarding_interval_nonce)
.await?;
Ok(())
}
pub(crate) async fn finish_mixnode_rewarding(
&self,
rewarding_interval_nonce: u32,
) -> Result<(), RewardingError>
where
C: SigningCosmWasmClient + Sync,
{
self.0
.write()
.await
.nymd
.finish_mixnode_rewarding(rewarding_interval_nonce)
.await?;
Ok(())
}
pub(crate) async fn reward_mixnodes(
&self,
nodes: &[MixnodeToReward],
rewarding_interval_nonce: u32,
) -> Result<(), RewardingError>
where
C: SigningCosmWasmClient + Sync,
@@ -216,7 +249,7 @@ impl<C> Client<C> {
.await;
let msgs: Vec<(ExecuteMsg, _)> = nodes
.iter()
.map(Into::into)
.map(|node| node.to_execute_msg(rewarding_interval_nonce))
.zip(std::iter::repeat(Vec::new()))
.collect();
+45 -21
View File
@@ -55,6 +55,8 @@ pub(crate) struct MixnodeToReward {
impl MixnodeToReward {
/// Somewhat clumsy way of feature gatting tokenomics payments. In a tokenomics scenario this will never be None at reward time. We levarage that to Into a different ExecuteMsg variant
// TODO: to re-integrate in another PR that combines rewarded/active sets with tokenomics
#[allow(dead_code)]
fn params(&self) -> Option<NodeRewardParams> {
if cfg!(feature = "tokenomics") {
self.params
@@ -64,6 +66,26 @@ impl MixnodeToReward {
}
}
impl MixnodeToReward {
pub(crate) fn to_execute_msg(&self, rewarding_interval_nonce: u32) -> ExecuteMsg {
ExecuteMsg::RewardMixnode {
identity: self.identity.clone(),
uptime: self.uptime.u8() as u32,
rewarding_interval_nonce,
}
}
// TODO: to re-integrate in another PR that combines rewarded/active sets with tokenomics
#[allow(dead_code)]
pub(crate) fn to_execute_msg_v2(&self, rewarding_interval_nonce: u32) -> ExecuteMsg {
ExecuteMsg::RewardMixnodeV2 {
identity: self.identity.clone(),
params: self.params().unwrap(),
rewarding_interval_nonce,
}
}
}
pub(crate) struct FailedMixnodeRewardChunkDetails {
possibly_unrewarded: Vec<MixnodeToReward>,
error_message: String,
@@ -74,22 +96,6 @@ pub(crate) struct FailureData {
mixnodes: Option<Vec<FailedMixnodeRewardChunkDetails>>,
}
impl<'a> From<&'a MixnodeToReward> for ExecuteMsg {
fn from(node: &MixnodeToReward) -> Self {
if let Some(params) = node.params() {
ExecuteMsg::RewardMixnodeV2 {
identity: node.identity.clone(),
params,
}
} else {
ExecuteMsg::RewardMixnode {
identity: node.identity.clone(),
uptime: node.uptime.u8() as u32,
}
}
}
}
pub(crate) struct Rewarder {
nymd_client: Client<SigningNymdClient>,
validator_cache: ValidatorCache,
@@ -285,14 +291,20 @@ impl Rewarder {
/// # Arguments
///
/// * `eligible_mixnodes`: list of the nodes that are eligible to receive non-zero rewards.
/// * `rewarding_interval_nonce`: nonce associated with the current rewarding interval
async fn distribute_rewards_to_mixnodes(
&self,
eligible_mixnodes: &[MixnodeToReward],
rewarding_interval_nonce: u32,
) -> Option<Vec<FailedMixnodeRewardChunkDetails>> {
let mut failed_chunks = Vec::new();
for (i, mix_chunk) in eligible_mixnodes.chunks(MAX_TO_REWARD_AT_ONCE).enumerate() {
if let Err(err) = self.nymd_client.reward_mixnodes(mix_chunk).await {
if let Err(err) = self
.nymd_client
.reward_mixnodes(mix_chunk, rewarding_interval_nonce)
.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
@@ -327,13 +339,13 @@ impl Rewarder {
///
/// # Arguments
///
/// * `epoch_rewarding_id`: id of the current epoch rewarding.
/// * `epoch_rewarding_id`: id of the current epoch rewarding as stored in the databse.
///
/// * `active_monitor_mixnodes`: list of the nodes that were tested at least once by the network monitor
/// in the last epoch.
async fn distribute_rewards(
&self,
epoch_rewarding_id: i64,
epoch_rewarding_database_id: i64,
active_monitor_mixnodes: &[MixnodeStatusReport],
) -> Result<(RewardingReport, Option<FailureData>), RewardingError> {
let mut failure_data = FailureData::default();
@@ -345,12 +357,20 @@ impl Rewarder {
return Err(RewardingError::NoMixnodesToReward);
}
let current_rewarding_nonce = self
.nymd_client
.get_current_rewarding_interval()
.await?
.current_rewarding_interval_nonce;
self.nymd_client
.begin_mixnode_rewarding(current_rewarding_nonce + 1)
.await?;
failure_data.mixnodes = self
.distribute_rewards_to_mixnodes(&eligible_mixnodes)
.distribute_rewards_to_mixnodes(&eligible_mixnodes, current_rewarding_nonce + 1)
.await;
let report = RewardingReport {
epoch_rewarding_id,
epoch_rewarding_id: epoch_rewarding_database_id,
eligible_mixnodes: eligible_mixnodes.len() as i64,
possibly_unrewarded_mixnodes: failure_data
.mixnodes
@@ -364,6 +384,10 @@ impl Rewarder {
.unwrap_or_default(),
};
self.nymd_client
.finish_mixnode_rewarding(current_rewarding_nonce + 1)
.await?;
if failure_data.mixnodes.is_none() {
Ok((report, None))
} else {