diff --git a/common/client-libs/validator-client/src/lib.rs b/common/client-libs/validator-client/src/lib.rs index 4cd2c4b163..a405b08f17 100644 --- a/common/client-libs/validator-client/src/lib.rs +++ b/common/client-libs/validator-client/src/lib.rs @@ -4,7 +4,9 @@ use crate::models::{QueryRequest, QueryResponse}; use crate::ValidatorClientError::ValidatorError; use core::fmt::{self, Display, Formatter}; -use mixnet_contract::{GatewayBond, HumanAddr, MixNodeBond, PagedGatewayResponse, PagedResponse}; +use mixnet_contract::{ + GatewayBond, HumanAddr, LayerDistribution, MixNodeBond, PagedGatewayResponse, PagedResponse, +}; use rand::seq::SliceRandom; use rand::thread_rng; use serde::Deserialize; @@ -238,4 +240,17 @@ impl Client { Ok(gateways) } + + pub async fn get_layer_distribution( + &mut self, + ) -> Result { + // serialization of an empty enum can't fail... + let query_content_json = + serde_json::to_string(&QueryRequest::LayerDistribution {}).unwrap(); + + // we need to encode our json request + let query_content = base64::encode(query_content_json); + + self.query_validators(query_content).await + } } diff --git a/common/client-libs/validator-client/src/models.rs b/common/client-libs/validator-client/src/models.rs index 1b4089b1ba..6264e77ea4 100644 --- a/common/client-libs/validator-client/src/models.rs +++ b/common/client-libs/validator-client/src/models.rs @@ -20,6 +20,7 @@ pub(super) enum QueryRequest { start_after: Option, limit: Option, }, + LayerDistribution {}, } #[derive(Deserialize, Debug)] diff --git a/common/mixnet-contract/src/lib.rs b/common/mixnet-contract/src/lib.rs index 0ffefd312d..04cc2ab09e 100644 --- a/common/mixnet-contract/src/lib.rs +++ b/common/mixnet-contract/src/lib.rs @@ -1,6 +1,17 @@ +use serde::{Deserialize, Serialize}; + mod gateway; mod mixnode; pub use cosmwasm_std::{Coin, HumanAddr}; pub use gateway::{Gateway, GatewayBond, GatewayOwnershipResponse, PagedGatewayResponse}; pub use mixnode::{MixNode, MixNodeBond, MixOwnershipResponse, PagedResponse}; + +#[derive(Debug, Default, Serialize, Deserialize, Copy, Clone, Eq, PartialEq)] +pub struct LayerDistribution { + pub gateways: u64, + pub layer1: u64, + pub layer2: u64, + pub layer3: u64, + pub invalid: u64, +} diff --git a/contracts/mixnet/src/contract.rs b/contracts/mixnet/src/contract.rs index fd69ed5c4d..fdacd4eb3b 100644 --- a/contracts/mixnet/src/contract.rs +++ b/contracts/mixnet/src/contract.rs @@ -1,12 +1,13 @@ use crate::helpers::calculate_epoch_reward_rate; use crate::msg::{HandleMsg, InitMsg, MigrateMsg, QueryMsg}; use crate::state::{State, StateParams}; -use crate::storage::config; +use crate::storage::{config, layer_distribution}; use crate::{error::ContractError, queries, transactions}; use cosmwasm_std::{ to_binary, Decimal, Deps, DepsMut, Env, HandleResponse, HumanAddr, InitResponse, MessageInfo, MigrateResponse, QueryResponse, Uint128, }; +use mixnet_contract::LayerDistribution; pub const INITIAL_DEFAULT_EPOCH_LENGTH: u32 = 2; @@ -67,6 +68,7 @@ pub fn init( let state = default_initial_state(info.sender); config(deps.storage).save(&state)?; + layer_distribution(deps.storage).save(&Default::default())?; Ok(InitResponse::default()) } @@ -109,27 +111,63 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> Result to_binary(&queries::query_state_params(deps)), + QueryMsg::LayerDistribution {} => to_binary(&queries::query_layer_distribution(deps)), }; Ok(query_res?) } pub fn migrate( - _deps: DepsMut, + deps: DepsMut, _env: Env, _info: MessageInfo, _msg: MigrateMsg, ) -> Result { + // load all mixnodes and gateways and build up layer distribution + let mut layers: LayerDistribution = Default::default(); + + // go through mixnodes... + let mut start_after = None; + loop { + let response = queries::query_mixnodes_paged(deps.as_ref(), start_after, None)?; + start_after = response.start_next_after; + if start_after.is_none() { + break; + } + for node in response.nodes.into_iter() { + match node.mix_node.layer { + n if n == 1 => layers.layer1 += 1, + n if n == 2 => layers.layer2 += 1, + n if n == 3 => layers.layer3 += 1, + _ => layers.invalid += 1, + } + } + } + + // go through gateways... + loop { + let response = queries::query_gateways_paged(deps.as_ref(), start_after, None)?; + start_after = response.start_next_after; + if start_after.is_none() { + break; + } + layers.gateways += response.nodes.len() as u64; + } + + layer_distribution(deps.storage).save(&layers)?; + Ok(Default::default()) } #[cfg(test)] pub mod tests { use super::*; + use crate::storage::{gateways, layer_distribution_read, mixnodes}; + use crate::support::tests::helpers; use crate::support::tests::helpers::*; use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; use cosmwasm_std::{coins, from_binary}; - use mixnet_contract::PagedResponse; + use mixnet_contract::{Gateway, GatewayBond, MixNode, MixNodeBond, PagedResponse}; #[test] fn initialize_contract() { @@ -160,4 +198,137 @@ pub mod tests { query_contract_balance(env.contract.address, deps) ); } + + // TODO: this test will have to be removed once the migration happens and we are working on yet another + // version of the contract + #[test] + fn migration_to_layer_distribution() { + let layer_ones = 42; + let layer_twos = 123; + let layer_threes = 111; + let invalid = 30; + let gateways_count = 24; + + // bond some nodes + let mut deps = helpers::init_contract(); + let env = mock_env(); + + for i in 0..layer_ones { + let owner = HumanAddr::from(format!("owner{}{}", 1, i)); + mixnodes(&mut deps.storage) + .save( + owner.clone().as_bytes(), + &MixNodeBond { + amount: coins(1000, "uhal"), + owner, + mix_node: MixNode { + host: "1.1.1.1:1111".to_string(), + layer: 1, + location: "aaaa".to_string(), + sphinx_key: "bbbb".to_string(), + identity_key: format!("identity{}{}", 1, i), + version: "0.10.1".to_string(), + }, + }, + ) + .unwrap(); + } + + for i in 0..layer_twos { + let owner = HumanAddr::from(format!("owner{}{}", 2, i)); + mixnodes(&mut deps.storage) + .save( + owner.clone().as_bytes(), + &MixNodeBond { + amount: coins(1000, "uhal"), + owner, + mix_node: MixNode { + host: "1.1.1.1:1111".to_string(), + layer: 2, + location: "aaaa".to_string(), + sphinx_key: "bbbb".to_string(), + identity_key: format!("identity{}{}", 2, i), + version: "0.10.1".to_string(), + }, + }, + ) + .unwrap(); + } + + for i in 0..layer_threes { + let owner = HumanAddr::from(format!("owner{}{}", 3, i)); + mixnodes(&mut deps.storage) + .save( + owner.clone().as_bytes(), + &MixNodeBond { + amount: coins(1000, "uhal"), + owner, + mix_node: MixNode { + host: "1.1.1.1:1111".to_string(), + layer: 3, + location: "aaaa".to_string(), + sphinx_key: "bbbb".to_string(), + identity_key: format!("identity{}{}", 3, i), + version: "0.10.1".to_string(), + }, + }, + ) + .unwrap(); + } + + for i in 0..invalid { + let owner = HumanAddr::from(format!("owner{}{}", 42, i)); + mixnodes(&mut deps.storage) + .save( + owner.clone().as_bytes(), + &MixNodeBond { + amount: coins(1000, "uhal"), + owner, + mix_node: MixNode { + host: "1.1.1.1:1111".to_string(), + layer: 42, + location: "aaaa".to_string(), + sphinx_key: "bbbb".to_string(), + identity_key: format!("identity{}{}", 42, i), + version: "0.10.1".to_string(), + }, + }, + ) + .unwrap(); + } + + for i in 0..gateways_count { + let owner = HumanAddr::from(format!("owner{}{}", "gateway", i)); + gateways(&mut deps.storage) + .save( + owner.clone().as_bytes(), + &GatewayBond { + amount: coins(1000, "uhal"), + owner, + gateway: Gateway { + mix_host: "1.1.1.1:1111".to_string(), + clients_host: "ws://1.1.1.1:1112".to_string(), + location: "aaaa".to_string(), + sphinx_key: "bbbb".to_string(), + identity_key: format!("identity{}{}", "gateway", i), + version: "0.10.1".to_string(), + }, + }, + ) + .unwrap(); + } + + let migrate_res = migrate(deps.as_mut(), env, mock_info("creator", &[]), MigrateMsg {}); + assert!(migrate_res.is_ok()); + + let layers = layer_distribution_read(&deps.storage).load().unwrap(); + let expected = LayerDistribution { + gateways: gateways_count, + layer1: layer_ones, + layer2: layer_twos, + layer3: layer_threes, + invalid, + }; + assert_eq!(expected, layers); + } } diff --git a/contracts/mixnet/src/msg.rs b/contracts/mixnet/src/msg.rs index 51bfb18690..fd4400a51a 100644 --- a/contracts/mixnet/src/msg.rs +++ b/contracts/mixnet/src/msg.rs @@ -51,6 +51,7 @@ pub enum QueryMsg { address: HumanAddr, }, StateParams {}, + LayerDistribution {}, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] diff --git a/contracts/mixnet/src/queries.rs b/contracts/mixnet/src/queries.rs index 996c59cb8c..cd95c2e865 100644 --- a/contracts/mixnet/src/queries.rs +++ b/contracts/mixnet/src/queries.rs @@ -1,12 +1,12 @@ use crate::state::StateParams; -use crate::storage::{gateways_read, mixnodes_read, read_state_params}; +use crate::storage::{gateways_read, mixnodes_read, read_layer_distribution, read_state_params}; use cosmwasm_std::Deps; use cosmwasm_std::HumanAddr; use cosmwasm_std::Order; use cosmwasm_std::StdResult; use mixnet_contract::{ - GatewayBond, GatewayOwnershipResponse, MixNodeBond, MixOwnershipResponse, PagedGatewayResponse, - PagedResponse, + GatewayBond, GatewayOwnershipResponse, LayerDistribution, MixNodeBond, MixOwnershipResponse, + PagedGatewayResponse, PagedResponse, }; const MAX_LIMIT: u32 = 100; @@ -77,6 +77,10 @@ pub(crate) fn query_state_params(deps: Deps) -> StateParams { read_state_params(deps.storage) } +pub(crate) fn query_layer_distribution(deps: Deps) -> LayerDistribution { + read_layer_distribution(deps.storage) +} + /// Adds a 0 byte to terminate the `start_after` value given. This allows CosmWasm /// to get the succeeding key as the start of the next page. fn calculate_start_value( diff --git a/contracts/mixnet/src/storage.rs b/contracts/mixnet/src/storage.rs index bf781f51da..2593328a34 100644 --- a/contracts/mixnet/src/storage.rs +++ b/contracts/mixnet/src/storage.rs @@ -4,7 +4,7 @@ use cosmwasm_storage::{ bucket, bucket_read, singleton, singleton_read, Bucket, ReadonlyBucket, ReadonlySingleton, Singleton, }; -use mixnet_contract::{GatewayBond, MixNodeBond}; +use mixnet_contract::{GatewayBond, LayerDistribution, MixNodeBond}; // Contract-level stuff const CONFIG_KEY: &[u8] = b"config"; @@ -40,6 +40,92 @@ pub(crate) fn read_gateway_epoch_reward_rate(storage: &dyn Storage) -> Decimal { .gateway_epoch_bond_reward } +const LAYER_DISTRIBUTION_KEY: &[u8] = b"layers"; + +pub fn layer_distribution(storage: &mut dyn Storage) -> Singleton { + singleton(storage, LAYER_DISTRIBUTION_KEY) +} + +pub fn layer_distribution_read(storage: &dyn Storage) -> ReadonlySingleton { + singleton_read(storage, LAYER_DISTRIBUTION_KEY) +} + +pub(crate) fn read_layer_distribution(storage: &dyn Storage) -> LayerDistribution { + // note: In any other case, I wouldn't have attempted to unwrap this result, but in here + // if we fail to load the stored state we would already be in the undefined behaviour land, + // so we better just blow up immediately. + layer_distribution_read(storage).load().unwrap() +} + +pub enum Layer { + Gateway, + One, + Two, + Three, + Invalid, +} + +impl From for Layer { + fn from(val: u64) -> Self { + match val { + n if n == 1 => Layer::One, + n if n == 2 => Layer::Two, + n if n == 3 => Layer::Three, + _ => Layer::Invalid, + } + } +} + +pub fn increment_layer_count(storage: &mut dyn Storage, layer: Layer) -> StdResult<()> { + let mut distribution = layer_distribution(storage).load()?; + match layer { + Layer::Gateway => distribution.gateways += 1, + Layer::One => distribution.layer1 += 1, + Layer::Two => distribution.layer2 += 1, + Layer::Three => distribution.layer3 += 1, + Layer::Invalid => distribution.invalid += 1, + } + layer_distribution(storage).save(&distribution) +} + +pub fn decrement_layer_count(storage: &mut dyn Storage, layer: Layer) -> StdResult<()> { + let mut distribution = layer_distribution(storage).load()?; + // It can't possibly go below zero, if it does, it means there's a serious error in the contract logic + match layer { + Layer::Gateway => { + distribution.gateways = distribution + .gateways + .checked_sub(1) + .expect("tried to subtract from unsigned zero!") + } + Layer::One => { + distribution.layer1 = distribution + .layer1 + .checked_sub(1) + .expect("tried to subtract from unsigned zero!") + } + Layer::Two => { + distribution.layer2 = distribution + .layer2 + .checked_sub(1) + .expect("tried to subtract from unsigned zero!") + } + Layer::Three => { + distribution.layer3 = distribution + .layer3 + .checked_sub(1) + .expect("tried to subtract from unsigned zero!") + } + Layer::Invalid => { + distribution.invalid = distribution + .invalid + .checked_sub(1) + .expect("tried to subtract from unsigned zero!") + } + }; + layer_distribution(storage).save(&distribution) +} + // Mixnode-related stuff const PREFIX_MIXNODES: &[u8] = b"mixnodes"; diff --git a/contracts/mixnet/src/transactions.rs b/contracts/mixnet/src/transactions.rs index cdef66efb5..8c2de44c08 100644 --- a/contracts/mixnet/src/transactions.rs +++ b/contracts/mixnet/src/transactions.rs @@ -3,10 +3,10 @@ use crate::error::ContractError; use crate::helpers::{calculate_epoch_reward_rate, scale_reward_by_uptime}; use crate::state::StateParams; use crate::storage::{ - config, config_read, gateways, gateways_owners, gateways_owners_read, gateways_read, - increase_gateway_bond, increase_mixnode_bond, mixnodes, mixnodes_owners, mixnodes_owners_read, - mixnodes_read, read_gateway_epoch_reward_rate, read_mixnode_epoch_reward_rate, - read_state_params, + config, config_read, decrement_layer_count, gateways, gateways_owners, gateways_owners_read, + gateways_read, increase_gateway_bond, increase_mixnode_bond, increment_layer_count, mixnodes, + mixnodes_owners, mixnodes_owners_read, mixnodes_read, read_gateway_epoch_reward_rate, + read_mixnode_epoch_reward_rate, read_state_params, Layer, }; use cosmwasm_std::{ attr, BankMsg, Coin, Decimal, DepsMut, Env, HandleResponse, HumanAddr, MessageInfo, Uint128, @@ -73,10 +73,11 @@ pub(crate) fn try_add_mixnode( let sender_bytes = info.sender.as_bytes(); let attributes = vec![attr("overwritten", was_present)]; - // TODO: now this can be potentially problematic. What if the first call doesn't fail but the second one does? - // can we do some rollback somehow? + // TODO: now this can be potentially problematic. What if one of the calls fails while other succeed? + // can we do some rollback somehow? Or is it already managed by the wasmvm? mixnodes(deps.storage).save(sender_bytes, &bond)?; mixnodes_owners(deps.storage).save(bond.mix_node.identity_key.as_bytes(), &info.sender)?; + increment_layer_count(deps.storage, bond.mix_node.layer.into())?; Ok(HandleResponse { messages: vec![], @@ -107,6 +108,8 @@ pub(crate) fn try_remove_mixnode( mixnodes(deps.storage).remove(info.sender.as_bytes()); // remove the node ownership mixnodes_owners(deps.storage).remove(mixnode_bond.mix_node.identity_key.as_bytes()); + // decrement layer count + decrement_layer_count(deps.storage, mixnode_bond.mix_node.layer.into())?; // log our actions let attributes = vec![attr("action", "unbond"), attr("mixnode_bond", mixnode_bond)]; @@ -178,10 +181,11 @@ pub(crate) fn try_add_gateway( let sender_bytes = info.sender.as_bytes(); let attributes = vec![attr("overwritten", was_present)]; - // TODO: now this can be potentially problematic. What if the first call doesn't fail but the second one does? - // can we do some rollback somehow? + // TODO: now this can be potentially problematic. What if one of the calls fails while other succeed? + // can we do some rollback somehow? Or is it already managed by the wasmvm? gateways(deps.storage).save(sender_bytes, &bond)?; gateways_owners(deps.storage).save(bond.gateway.identity_key.as_bytes(), &info.sender)?; + increment_layer_count(deps.storage, Layer::Gateway)?; Ok(HandleResponse { messages: vec![], @@ -219,6 +223,8 @@ pub(crate) fn try_remove_gateway( gateways(deps.storage).remove(sender_bytes); // remove the node ownership gateways_owners(deps.storage).remove(gateway_bond.gateway.identity_key.as_bytes()); + // decrement layer count + decrement_layer_count(deps.storage, Layer::Gateway)?; // log our actions let attributes = vec![ @@ -356,12 +362,15 @@ pub mod tests { use crate::helpers::calculate_epoch_reward_rate; use crate::msg::{HandleMsg, InitMsg, QueryMsg}; use crate::state::StateParams; - use crate::storage::{read_gateway_bond, read_gateway_epoch_reward_rate, read_mixnode_bond}; + use crate::storage::{ + layer_distribution_read, read_gateway_bond, read_gateway_epoch_reward_rate, + read_mixnode_bond, + }; use crate::support::tests::helpers; use crate::support::tests::helpers::{gateway_fixture, mix_node_fixture}; use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; use cosmwasm_std::{coins, from_binary, Uint128}; - use mixnet_contract::{PagedGatewayResponse, PagedResponse}; + use mixnet_contract::{LayerDistribution, PagedGatewayResponse, PagedResponse}; fn good_mixnode_bond() -> Vec { vec![Coin { @@ -642,6 +651,34 @@ pub mod tests { ); } + #[test] + fn adding_mixnode_updates_layer_distribution() { + let mut deps = helpers::init_contract(); + + assert_eq!( + LayerDistribution::default(), + layer_distribution_read(&deps.storage).load().unwrap(), + ); + + let info = mock_info("mix-owner", &good_mixnode_bond()); + let msg = HandleMsg::BondMixnode { + mix_node: MixNode { + identity_key: "mix1".to_string(), + layer: 1, + ..helpers::mix_node_fixture() + }, + }; + + handle(deps.as_mut(), mock_env(), info, msg).unwrap(); + assert_eq!( + LayerDistribution { + layer1: 1, + ..Default::default() + }, + layer_distribution_read(&deps.storage).load().unwrap() + ); + } + #[test] fn mixnode_remove() { let env = mock_env(); diff --git a/mixnode/src/commands/init.rs b/mixnode/src/commands/init.rs index a8cb910c75..6e7efaadf2 100644 --- a/mixnode/src/commands/init.rs +++ b/mixnode/src/commands/init.rs @@ -9,7 +9,6 @@ use config::NymConfig; use crypto::asymmetric::{encryption, identity}; use log::debug; use nymsphinx::params::DEFAULT_NUM_MIX_HOPS; -use std::collections::HashMap; use tokio::runtime::Runtime; pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> { @@ -93,38 +92,22 @@ async fn choose_layer( let validator_client_config = validator_client::Config::new(validator_servers, mixnet_contract); let mut validator_client = validator_client::Client::new(validator_client_config); - let mixnodes = validator_client - .get_mix_nodes() + let layer_distribution = validator_client + .get_layer_distribution() .await - .expect("failed to obtain initial network mixnodes"); + .expect("failed to obtain layer distribution of the testnet!"); - let mut nodes_distribution = HashMap::new(); - // initialise with 0 for each possible layer - for layer in 1..=max_layer { - nodes_distribution.insert(layer as u64, 0); + if layer_distribution.layer1 < layer_distribution.layer2 + && layer_distribution.layer1 < layer_distribution.layer3 + { + 1 + } else if layer_distribution.layer2 < layer_distribution.layer1 + && layer_distribution.layer2 < layer_distribution.layer3 + { + 2 + } else { + 3 } - - for node in mixnodes { - if node.mix_node.layer < 1 || node.mix_node.layer > max_layer as u64 { - debug!( - "one of bonded mixnodes is on invalid layer {}", - node.mix_node.layer - ); - continue; - } - - *nodes_distribution.entry(node.mix_node.layer).or_insert(0) += 1; - } - - // this can't be None as the hashmap is guaranteed to be non-empty since we initialised it - // with zeroes for each possible layer - let layer_with_fewest = nodes_distribution - .iter() - .min_by(|a, b| a.1.cmp(&b.1)) - .map(|(k, _v)| k) - .unwrap(); - - *layer_with_fewest } fn show_bonding_info(config: &Config) { diff --git a/mixnode/src/node/mod.rs b/mixnode/src/node/mod.rs index ac076ae2bb..27ef694950 100644 --- a/mixnode/src/node/mod.rs +++ b/mixnode/src/node/mod.rs @@ -202,7 +202,7 @@ impl MixNode { runtime.block_on(async { if let Some(duplicate_node_key) = self.check_if_same_ip_node_exists().await { if duplicate_node_key == self.identity_keypair.public_key().to_base58_string() { - warn!("You seem to have bonded your mixnode before starting it - that's highly unrecommended as in the future it will result in slashing"); + warn!("You seem to have bonded your mixnode before starting it - that's highly unrecommended as in the future it might result in slashing"); } else { log::error!( "Our announce-host is identical to an existing node's announce-host! (its key is {:?})",