diff --git a/Cargo.lock b/Cargo.lock index 00d23fa76e..11ed1c4400 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4280,7 +4280,6 @@ dependencies = [ "humantime-serde", "itertools 0.13.0", "k256", - "log", "nym-api-requests", "nym-bandwidth-controller", "nym-bin-common", @@ -4305,26 +4304,22 @@ dependencies = [ "nym-node-requests", "nym-node-tester-utils", "nym-pemstore", + "nym-serde-helpers", "nym-sphinx", "nym-task", "nym-topology", "nym-types", "nym-validator-client", "nym-vesting-contract-common", - "okapi", "pin-project", "rand", "rand_chacha", "reqwest 0.12.4", - "rocket", - "rocket_cors", - "rocket_okapi", "schemars", "serde", "serde_json", "sha2 0.9.9", "sqlx", - "tap", "tempfile", "thiserror", "time", @@ -4333,7 +4328,6 @@ dependencies = [ "tokio-util", "tower-http", "tracing", - "tracing-subscriber", "ts-rs", "url", "utoipa", @@ -4356,9 +4350,9 @@ dependencies = [ "nym-crypto", "nym-ecash-time", "nym-mixnet-contract-common", + "nym-network-defaults", "nym-node-requests", "nym-serde-helpers", - "rocket", "schemars", "serde", "serde_json", @@ -5473,6 +5467,7 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-controllers", + "cw-storage-plus", "cw2", "humantime-serde", "log", diff --git a/Cargo.toml b/Cargo.toml index 19f0ef8f00..81804b89da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -275,6 +275,7 @@ parking_lot = "0.12.3" pem = "0.8" petgraph = "0.6.5" pin-project = "1.0" +pin-project-lite = "0.2.14" pretty_env_logger = "0.4.0" publicsuffix = "2.2.3" quote = "1" diff --git a/common/client-core/src/client/topology_control/geo_aware_provider.rs b/common/client-core/src/client/topology_control/geo_aware_provider.rs index 96d52fd8d5..9967e1a705 100644 --- a/common/client-core/src/client/topology_control/geo_aware_provider.rs +++ b/common/client-core/src/client/topology_control/geo_aware_provider.rs @@ -3,11 +3,11 @@ use log::{debug, error}; use nym_explorer_client::{ExplorerClient, PrettyDetailedMixNodeBond}; use nym_network_defaults::var_names::EXPLORER_API; use nym_topology::{ - nym_topology_from_detailed, + nym_topology_from_basic_info, provider_trait::{async_trait, TopologyProvider}, NymTopology, }; -use nym_validator_client::client::MixId; +use nym_validator_client::client::NodeId; use rand::{prelude::SliceRandom, thread_rng}; use std::collections::HashMap; use tap::TapOptional; @@ -39,10 +39,10 @@ fn create_explorer_client() -> Option { fn group_mixnodes_by_country_code( mixnodes: Vec, -) -> HashMap> { +) -> HashMap> { mixnodes .into_iter() - .fold(HashMap::>::new(), |mut acc, m| { + .fold(HashMap::>::new(), |mut acc, m| { if let Some(ref location) = m.location { let country_code = location.two_letter_iso_country_code.clone(); let group_code = CountryGroup::new(country_code.as_str()); @@ -53,7 +53,7 @@ fn group_mixnodes_by_country_code( }) } -fn log_mixnode_distribution(mixnodes: &HashMap>) { +fn log_mixnode_distribution(mixnodes: &HashMap>) { let mixnode_distribution = mixnodes .iter() .map(|(k, v)| format!("{}: {}", k, v.len())) @@ -110,7 +110,7 @@ impl GeoAwareTopologyProvider { } async fn get_topology(&self) -> Option { - let mixnodes = match self.validator_client.get_cached_active_mixnodes().await { + let mixnodes = match self.validator_client.get_basic_mixnodes(None).await { Err(err) => { error!("failed to get network mixnodes - {err}"); return None; @@ -118,7 +118,7 @@ impl GeoAwareTopologyProvider { Ok(mixes) => mixes, }; - let gateways = match self.validator_client.get_cached_gateways().await { + let gateways = match self.validator_client.get_basic_gateways(None).await { Err(err) => { error!("failed to get network gateways - {err}"); return None; @@ -182,10 +182,10 @@ impl GeoAwareTopologyProvider { let mixnodes = mixnodes .into_iter() - .filter(|m| filtered_mixnode_ids.contains(&m.mix_id())) + .filter(|m| filtered_mixnode_ids.contains(&m.node_id)) .collect::>(); - let topology = nym_topology_from_detailed(mixnodes, gateways) + let topology = nym_topology_from_basic_info(&mixnodes, &gateways) .filter_system_version(&self.client_version); // TODO: return real error type diff --git a/common/client-core/src/error.rs b/common/client-core/src/error.rs index 653256df1f..4d412902e0 100644 --- a/common/client-core/src/error.rs +++ b/common/client-core/src/error.rs @@ -187,16 +187,6 @@ pub enum ClientCoreError { source: Ed25519RecoveryError, }, - #[error("the account owner of gateway {gateway_id} ({raw_owner}) is malformed: {err}")] - MalformedGatewayOwnerAccountAddress { - gateway_id: String, - - raw_owner: String, - - // just use the string formatting as opposed to underlying type to avoid having to import cosmrs - err: String, - }, - #[error( "the listening address of gateway {gateway_id} ({raw_listener}) is malformed: {source}" )] diff --git a/common/client-core/src/init/helpers.rs b/common/client-core/src/init/helpers.rs index c6d17fc16d..ac2b01bf6f 100644 --- a/common/client-core/src/init/helpers.rs +++ b/common/client-core/src/init/helpers.rs @@ -53,7 +53,7 @@ pub trait ConnectableGateway { fn is_wss(&self) -> bool; } -impl ConnectableGateway for gateway::Node { +impl ConnectableGateway for gateway::LegacyNode { fn identity(&self) -> &identity::PublicKey { self.identity() } @@ -82,7 +82,7 @@ pub async fn current_gateways( rng: &mut R, nym_apis: &[Url], user_agent: Option, -) -> Result, ClientCoreError> { +) -> Result, ClientCoreError> { let nym_api = nym_apis .choose(rng) .ok_or(ClientCoreError::ListOfNymApisIsEmpty)?; @@ -94,14 +94,14 @@ pub async fn current_gateways( log::debug!("Fetching list of gateways from: {nym_api}"); - let gateways = client.get_cached_described_gateways().await?; + let gateways = client.get_basic_gateways(None).await?; log::debug!("Found {} gateways", gateways.len()); log::trace!("Gateways: {:#?}", gateways); let valid_gateways = gateways - .into_iter() + .iter() .filter_map(|gateway| gateway.try_into().ok()) - .collect::>(); + .collect::>(); log::debug!("Ater checking validity: {}", valid_gateways.len()); log::trace!("Valid gateways: {:#?}", valid_gateways); @@ -118,7 +118,7 @@ pub async fn current_gateways( pub async fn current_mixnodes( rng: &mut R, nym_apis: &[Url], -) -> Result, ClientCoreError> { +) -> Result, ClientCoreError> { let nym_api = nym_apis .choose(rng) .ok_or(ClientCoreError::ListOfNymApisIsEmpty)?; @@ -126,11 +126,11 @@ pub async fn current_mixnodes( log::trace!("Fetching list of mixnodes from: {nym_api}"); - let mixnodes = client.get_cached_mixnodes().await?; + let mixnodes = client.get_basic_mixnodes(None).await?; let valid_mixnodes = mixnodes - .into_iter() - .filter_map(|mixnode| (&mixnode.bond_information).try_into().ok()) - .collect::>(); + .iter() + .filter_map(|mixnode| mixnode.try_into().ok()) + .collect::>(); // we were always filtering by version so I'm not removing that 'feature' let filtered_mixnodes = valid_mixnodes.filter_by_version(env!("CARGO_PKG_VERSION")); @@ -273,9 +273,9 @@ fn filter_by_tls( pub(super) fn uniformly_random_gateway( rng: &mut R, - gateways: &[gateway::Node], + gateways: &[gateway::LegacyNode], must_use_tls: bool, -) -> Result { +) -> Result { filter_by_tls(gateways, must_use_tls)? .choose(rng) .ok_or(ClientCoreError::NoGatewaysOnNetwork) @@ -284,9 +284,9 @@ pub(super) fn uniformly_random_gateway( pub(super) fn get_specified_gateway( gateway_identity: IdentityKeyRef, - gateways: &[gateway::Node], + gateways: &[gateway::LegacyNode], must_use_tls: bool, -) -> Result { +) -> Result { log::debug!("Requesting specified gateway: {}", gateway_identity); let user_gateway = identity::PublicKey::from_base58_string(gateway_identity) .map_err(ClientCoreError::UnableToCreatePublicKeyFromGatewayId)?; diff --git a/common/client-core/src/init/mod.rs b/common/client-core/src/init/mod.rs index 479d73be1a..8e93babbf2 100644 --- a/common/client-core/src/init/mod.rs +++ b/common/client-core/src/init/mod.rs @@ -50,7 +50,7 @@ async fn setup_new_gateway( key_store: &K, details_store: &D, selection_specification: GatewaySelectionSpecification, - available_gateways: Vec, + available_gateways: Vec, ) -> Result where K: KeyStore, diff --git a/common/client-core/src/init/types.rs b/common/client-core/src/init/types.rs index 2ba78dd6d1..1aa4a0d24b 100644 --- a/common/client-core/src/init/types.rs +++ b/common/client-core/src/init/types.rs @@ -18,7 +18,6 @@ use nym_validator_client::client::IdentityKey; use nym_validator_client::nyxd::AccountId; use serde::Serialize; use std::fmt::Display; -use std::str::FromStr; use std::sync::Arc; use time::OffsetDateTime; use url::Url; @@ -39,7 +38,7 @@ pub enum SelectedGateway { impl SelectedGateway { pub fn from_topology_node( - node: gateway::Node, + node: gateway::LegacyNode, must_use_tls: bool, ) -> Result { let gateway_listener = if must_use_tls { @@ -51,20 +50,6 @@ impl SelectedGateway { node.clients_address() }; - let gateway_owner_address = node - .owner - .as_ref() - .map(|raw_owner| { - AccountId::from_str(raw_owner).map_err(|source| { - ClientCoreError::MalformedGatewayOwnerAccountAddress { - gateway_id: node.identity_key.to_base58_string(), - raw_owner: raw_owner.clone(), - err: source.to_string(), - } - }) - }) - .transpose()?; - let gateway_listener = Url::parse(&gateway_listener).map_err(|source| ClientCoreError::MalformedListener { gateway_id: node.identity_key.to_base58_string(), @@ -74,7 +59,7 @@ impl SelectedGateway { Ok(SelectedGateway::Remote { gateway_id: node.identity_key, - gateway_owner_address, + gateway_owner_address: None, gateway_listener, }) } @@ -215,7 +200,7 @@ pub enum GatewaySetup { specification: GatewaySelectionSpecification, // TODO: seems to be a bit inefficient to pass them by value - available_gateways: Vec, + available_gateways: Vec, }, ReuseConnection { diff --git a/common/client-libs/validator-client/src/client.rs b/common/client-libs/validator-client/src/client.rs index 32cfd9ae2f..eee67ba509 100644 --- a/common/client-libs/validator-client/src/client.rs +++ b/common/client-libs/validator-client/src/client.rs @@ -17,11 +17,12 @@ use nym_api_requests::ecash::{ BlindSignRequestBody, BlindedSignatureResponse, PartialCoinIndicesSignatureResponse, PartialExpirationDateSignatureResponse, VerificationKeyResponse, }; -use nym_api_requests::models::{DescribedGateway, MixNodeBondAnnotated}; +use nym_api_requests::legacy::LegacyGatewayBondWithId; use nym_api_requests::models::{ GatewayCoreStatusResponse, MixnodeCoreStatusResponse, MixnodeStatusResponse, RewardEstimationResponse, StakeSaturationResponse, }; +use nym_api_requests::models::{LegacyDescribedGateway, MixNodeBondAnnotated}; use nym_api_requests::nym_nodes::SkimmedNode; use nym_coconut_dkg_common::types::EpochId; use nym_http_api_client::UserAgent; @@ -31,7 +32,7 @@ use url::Url; pub use crate::nym_api::NymApiClientExt; pub use nym_mixnet_contract_common::{ - mixnode::MixNodeDetails, GatewayBond, IdentityKey, IdentityKeyRef, MixId, + mixnode::MixNodeDetails, GatewayBond, IdentityKey, IdentityKeyRef, NodeId, }; // re-export the type to not break existing imports @@ -239,7 +240,9 @@ impl Client { Ok(self.nym_api.get_active_mixnodes_detailed().await?) } - pub async fn get_cached_gateways(&self) -> Result, ValidatorClientError> { + pub async fn get_cached_gateways( + &self, + ) -> Result, ValidatorClientError> { Ok(self.nym_api.get_gateways().await?) } @@ -321,13 +324,15 @@ impl NymApiClient { Ok(self.nym_api.get_mixnodes().await?) } - pub async fn get_cached_gateways(&self) -> Result, ValidatorClientError> { + pub async fn get_cached_gateways( + &self, + ) -> Result, ValidatorClientError> { Ok(self.nym_api.get_gateways().await?) } pub async fn get_cached_described_gateways( &self, - ) -> Result, ValidatorClientError> { + ) -> Result, ValidatorClientError> { Ok(self.nym_api.get_gateways_described().await?) } @@ -344,7 +349,7 @@ impl NymApiClient { pub async fn get_mixnode_core_status_count( &self, - mix_id: MixId, + mix_id: NodeId, since: Option, ) -> Result { Ok(self @@ -355,21 +360,21 @@ impl NymApiClient { pub async fn get_mixnode_status( &self, - mix_id: MixId, + mix_id: NodeId, ) -> Result { Ok(self.nym_api.get_mixnode_status(mix_id).await?) } pub async fn get_mixnode_reward_estimation( &self, - mix_id: MixId, + mix_id: NodeId, ) -> Result { Ok(self.nym_api.get_mixnode_reward_estimation(mix_id).await?) } pub async fn get_mixnode_stake_saturation( &self, - mix_id: MixId, + mix_id: NodeId, ) -> Result { Ok(self.nym_api.get_mixnode_stake_saturation(mix_id).await?) } diff --git a/common/client-libs/validator-client/src/nym_api/mod.rs b/common/client-libs/validator-client/src/nym_api/mod.rs index 1ebfea7284..6c5337afaf 100644 --- a/common/client-libs/validator-client/src/nym_api/mod.rs +++ b/common/client-libs/validator-client/src/nym_api/mod.rs @@ -10,8 +10,8 @@ use nym_api_requests::ecash::models::{ VerifyEcashTicketBody, }; use nym_api_requests::ecash::VerificationKeyResponse; -use nym_api_requests::models::DescribedMixNode; -use nym_api_requests::nym_nodes::{CachedNodesResponse, SkimmedNode}; +use nym_api_requests::legacy::LegacyGatewayBondWithId; +use nym_api_requests::models::LegacyDescribedMixNode; pub use nym_api_requests::{ ecash::{ models::{ @@ -23,19 +23,20 @@ pub use nym_api_requests::{ VerifyEcashCredentialBody, }, models::{ - ComputeRewardEstParam, DescribedGateway, GatewayBondAnnotated, GatewayCoreStatusResponse, + ComputeRewardEstParam, GatewayBondAnnotated, GatewayCoreStatusResponse, GatewayStatusReportResponse, GatewayUptimeHistoryResponse, InclusionProbabilityResponse, - MixNodeBondAnnotated, MixnodeCoreStatusResponse, MixnodeStatusReportResponse, - MixnodeStatusResponse, MixnodeUptimeHistoryResponse, RewardEstimationResponse, - StakeSaturationResponse, UptimeResponse, + LegacyDescribedGateway, MixNodeBondAnnotated, MixnodeCoreStatusResponse, + MixnodeStatusReportResponse, MixnodeStatusResponse, MixnodeUptimeHistoryResponse, + RewardEstimationResponse, StakeSaturationResponse, UptimeResponse, }, + nym_nodes::{CachedNodesResponse, SkimmedNode}, }; pub use nym_coconut_dkg_common::types::EpochId; use nym_contracts_common::IdentityKey; pub use nym_http_api_client::Client; use nym_http_api_client::{ApiClient, NO_PARAMS}; use nym_mixnet_contract_common::mixnode::MixNodeDetails; -use nym_mixnet_contract_common::{GatewayBond, IdentityKeyRef, MixId}; +use nym_mixnet_contract_common::{IdentityKeyRef, NodeId}; use time::format_description::BorrowedFormatItem; use time::Date; @@ -95,12 +96,12 @@ pub trait NymApiClientExt: ApiClient { .await } - async fn get_gateways(&self) -> Result, NymAPIError> { + async fn get_gateways(&self) -> Result, NymAPIError> { self.get_json(&[routes::API_VERSION, routes::GATEWAYS], NO_PARAMS) .await } - async fn get_gateways_described(&self) -> Result, NymAPIError> { + async fn get_gateways_described(&self) -> Result, NymAPIError> { self.get_json( &[routes::API_VERSION, routes::GATEWAYS, routes::DESCRIBED], NO_PARAMS, @@ -108,7 +109,7 @@ pub trait NymApiClientExt: ApiClient { .await } - async fn get_mixnodes_described(&self) -> Result, NymAPIError> { + async fn get_mixnodes_described(&self) -> Result, NymAPIError> { self.get_json( &[routes::API_VERSION, routes::MIXNODES, routes::DESCRIBED], NO_PARAMS, @@ -194,7 +195,7 @@ pub trait NymApiClientExt: ApiClient { async fn get_mixnode_report( &self, - mix_id: MixId, + mix_id: NodeId, ) -> Result { self.get_json( &[ @@ -228,7 +229,7 @@ pub trait NymApiClientExt: ApiClient { async fn get_mixnode_history( &self, - mix_id: MixId, + mix_id: NodeId, ) -> Result { self.get_json( &[ @@ -309,7 +310,7 @@ pub trait NymApiClientExt: ApiClient { async fn get_mixnode_core_status_count( &self, - mix_id: MixId, + mix_id: NodeId, since: Option, ) -> Result { if let Some(since) = since { @@ -341,7 +342,7 @@ pub trait NymApiClientExt: ApiClient { async fn get_mixnode_status( &self, - mix_id: MixId, + mix_id: NodeId, ) -> Result { self.get_json( &[ @@ -358,7 +359,7 @@ pub trait NymApiClientExt: ApiClient { async fn get_mixnode_reward_estimation( &self, - mix_id: MixId, + mix_id: NodeId, ) -> Result { self.get_json( &[ @@ -375,7 +376,7 @@ pub trait NymApiClientExt: ApiClient { async fn compute_mixnode_reward_estimation( &self, - mix_id: MixId, + mix_id: NodeId, request_body: &ComputeRewardEstParam, ) -> Result { self.post_json( @@ -394,7 +395,7 @@ pub trait NymApiClientExt: ApiClient { async fn get_mixnode_stake_saturation( &self, - mix_id: MixId, + mix_id: NodeId, ) -> Result { self.get_json( &[ @@ -411,7 +412,7 @@ pub trait NymApiClientExt: ApiClient { async fn get_mixnode_inclusion_probability( &self, - mix_id: MixId, + mix_id: NodeId, ) -> Result { self.get_json( &[ @@ -426,7 +427,7 @@ pub trait NymApiClientExt: ApiClient { .await } - async fn get_mixnode_avg_uptime(&self, mix_id: MixId) -> Result { + async fn get_mixnode_avg_uptime(&self, mix_id: NodeId) -> Result { self.get_json( &[ routes::API_VERSION, @@ -440,7 +441,7 @@ pub trait NymApiClientExt: ApiClient { .await } - async fn get_mixnodes_blacklisted(&self) -> Result, NymAPIError> { + async fn get_mixnodes_blacklisted(&self) -> Result, NymAPIError> { self.get_json( &[routes::API_VERSION, routes::MIXNODES, routes::BLACKLISTED], NO_PARAMS, diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_query_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_query_client.rs index 47b67d8f16..2f1c3cf0e6 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_query_client.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_query_client.rs @@ -7,29 +7,35 @@ use crate::nyxd::error::NyxdError; use crate::nyxd::CosmWasmClient; use async_trait::async_trait; use cosmrs::AccountId; +use nym_api_requests::models::StakeSaturationResponse; use nym_contracts_common::signing::Nonce; +use nym_mixnet_contract_common::gateway::{PreassignedGatewayIdsResponse, PreassignedId}; +use nym_mixnet_contract_common::nym_node::{ + EpochAssignmentResponse, NodeDetailsByIdentityResponse, NodeOwnershipResponse, + NodeRewardingDetailsResponse, PagedNymNodeBondsResponse, PagedNymNodeDetailsResponse, + PagedUnbondedNymNodesResponse, Role, RolesMetadataResponse, UnbondedNodeResponse, + UnbondedNymNode, +}; +use nym_mixnet_contract_common::reward_params::WorkFactor; use nym_mixnet_contract_common::{ delegation, - delegation::{MixNodeDelegationResponse, OwnerProxySubKey}, - families::{Family, FamilyHead}, + delegation::{NodeDelegationResponse, OwnerProxySubKey}, mixnode::{ - MixnodeRewardingDetailsResponse, PagedMixnodesDetailsResponse, - PagedUnbondedMixnodesResponse, StakeSaturationResponse, UnbondedMixnodeResponse, + MixStakeSaturationResponse, MixnodeRewardingDetailsResponse, PagedMixnodesDetailsResponse, + PagedUnbondedMixnodesResponse, UnbondedMixnodeResponse, }, reward_params::{Performance, RewardingParams}, rewarding::{EstimatedCurrentEpochRewardResponse, PendingRewardResponse}, ContractBuildInformation, ContractState, ContractStateParams, CurrentIntervalResponse, - Delegation, EpochEventId, EpochStatus, FamilyByHeadResponse, FamilyByLabelResponse, - FamilyMembersByHeadResponse, FamilyMembersByLabelResponse, GatewayBond, GatewayBondResponse, - GatewayOwnershipResponse, IdentityKey, IdentityKeyRef, IntervalEventId, LayerDistribution, - MixId, MixNodeBond, MixNodeDetails, MixOwnershipResponse, MixnodeDetailsByIdentityResponse, - MixnodeDetailsResponse, NumberOfPendingEventsResponse, PagedAllDelegationsResponse, - PagedDelegatorDelegationsResponse, PagedFamiliesResponse, PagedGatewayResponse, - PagedMembersResponse, PagedMixNodeDelegationsResponse, PagedMixnodeBondsResponse, - PagedRewardedSetResponse, PendingEpochEvent, PendingEpochEventResponse, - PendingEpochEventsResponse, PendingIntervalEvent, PendingIntervalEventResponse, - PendingIntervalEventsResponse, QueryMsg as MixnetQueryMsg, RewardedSetNodeStatus, - UnbondedMixnode, + Delegation, EpochEventId, EpochStatus, GatewayBond, GatewayBondResponse, + GatewayOwnershipResponse, IdentityKey, IdentityKeyRef, IntervalEventId, MixNodeBond, + MixNodeDetails, MixOwnershipResponse, MixnodeDetailsByIdentityResponse, MixnodeDetailsResponse, + NodeId, NumberOfPendingEventsResponse, NymNodeBond, NymNodeDetails, + PagedAllDelegationsResponse, PagedDelegatorDelegationsResponse, PagedGatewayResponse, + PagedMixnodeBondsResponse, PagedNodeDelegationsResponse, PendingEpochEvent, + PendingEpochEventResponse, PendingEpochEventsResponse, PendingIntervalEvent, + PendingIntervalEventResponse, PendingIntervalEventsResponse, QueryMsg as MixnetQueryMsg, + RewardedSet, UnbondedMixnode, }; use serde::Deserialize; @@ -91,56 +97,11 @@ pub trait MixnetQueryClient { .await } - async fn get_rewarded_set_paged( - &self, - start_after: Option, - limit: Option, - ) -> Result { - self.query_mixnet_contract(MixnetQueryMsg::GetRewardedSet { limit, start_after }) - .await - } - - async fn get_all_node_families_paged( - &self, - start_after: Option, - limit: Option, - ) -> Result { - self.query_mixnet_contract(MixnetQueryMsg::GetAllFamiliesPaged { limit, start_after }) - .await - } - - async fn get_all_family_members_paged( - &self, - start_after: Option, - limit: Option, - ) -> Result { - self.query_mixnet_contract(MixnetQueryMsg::GetAllMembersPaged { limit, start_after }) - .await - } - - async fn get_family_members_by_head + Send>( - &self, - head: S, - ) -> Result { - self.query_mixnet_contract(MixnetQueryMsg::GetFamilyMembersByHead { head: head.into() }) - .await - } - - async fn get_family_members_by_label + Send>( - &self, - label: S, - ) -> Result { - self.query_mixnet_contract(MixnetQueryMsg::GetFamilyMembersByLabel { - label: label.into(), - }) - .await - } - // mixnode-related: async fn get_mixnode_bonds_paged( &self, - start_after: Option, + start_after: Option, limit: Option, ) -> Result { self.query_mixnet_contract(MixnetQueryMsg::GetMixNodeBonds { limit, start_after }) @@ -149,26 +110,26 @@ pub trait MixnetQueryClient { async fn get_mixnodes_detailed_paged( &self, - start_after: Option, + start_after: Option, limit: Option, ) -> Result { self.query_mixnet_contract(MixnetQueryMsg::GetMixNodesDetailed { limit, start_after }) .await } - async fn get_unbonded_paged( + async fn get_unbonded_mixnodes_paged( &self, - start_after: Option, + start_after: Option, limit: Option, ) -> Result { self.query_mixnet_contract(MixnetQueryMsg::GetUnbondedMixNodes { limit, start_after }) .await } - async fn get_unbonded_by_owner_paged( + async fn get_unbonded_mixnodes_by_owner_paged( &self, owner: &AccountId, - start_after: Option, + start_after: Option, limit: Option, ) -> Result { self.query_mixnet_contract(MixnetQueryMsg::GetUnbondedMixNodesByOwner { @@ -179,10 +140,10 @@ pub trait MixnetQueryClient { .await } - async fn get_unbonded_by_identity_paged( + async fn get_unbonded_mixnodes_by_identity_paged( &self, identity_key: IdentityKeyRef<'_>, - start_after: Option, + start_after: Option, limit: Option, ) -> Result { self.query_mixnet_contract(MixnetQueryMsg::GetUnbondedMixNodesByIdentityKey { @@ -205,7 +166,7 @@ pub trait MixnetQueryClient { async fn get_mixnode_details( &self, - mix_id: MixId, + mix_id: NodeId, ) -> Result { self.query_mixnet_contract(MixnetQueryMsg::GetMixnodeDetails { mix_id }) .await @@ -223,7 +184,7 @@ pub trait MixnetQueryClient { async fn get_mixnode_rewarding_details( &self, - mix_id: MixId, + mix_id: NodeId, ) -> Result { self.query_mixnet_contract(MixnetQueryMsg::GetMixnodeRewardingDetails { mix_id }) .await @@ -231,24 +192,24 @@ pub trait MixnetQueryClient { async fn get_mixnode_stake_saturation( &self, - mix_id: MixId, - ) -> Result { + mix_id: NodeId, + ) -> Result { self.query_mixnet_contract(MixnetQueryMsg::GetStakeSaturation { mix_id }) .await } async fn get_unbonded_mixnode_information( &self, - mix_id: MixId, + mix_id: NodeId, ) -> Result { self.query_mixnet_contract(MixnetQueryMsg::GetUnbondedMixNodeInformation { mix_id }) .await } - async fn get_layer_distribution(&self) -> Result { - self.query_mixnet_contract(MixnetQueryMsg::GetLayerDistribution {}) - .await - } + // async fn get_layer_distribution(&self) -> Result { + // self.query_mixnet_contract(MixnetQueryMsg::GetRoleDistribution {}) + // .await + // } // gateway-related: @@ -281,17 +242,142 @@ pub trait MixnetQueryClient { .await } + async fn get_preassigned_gateway_ids_paged( + &self, + start_after: Option, + limit: Option, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetPreassignedGatewayIds { start_after, limit }) + .await + } + + // nym-nodes related: + async fn get_nymnode_bonds_paged( + &self, + start_after: Option, + limit: Option, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetNymNodeBondsPaged { limit, start_after }) + .await + } + + async fn get_nymnodes_detailed_paged( + &self, + start_after: Option, + limit: Option, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetNymNodesDetailedPaged { limit, start_after }) + .await + } + + async fn get_unbonded_nymnodes_paged( + &self, + start_after: Option, + limit: Option, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetUnbondedNymNodesPaged { limit, start_after }) + .await + } + + async fn get_unbonded_nymnodes_by_owner_paged( + &self, + owner: &AccountId, + start_after: Option, + limit: Option, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetUnbondedNymNodesByOwnerPaged { + owner: owner.to_string(), + limit, + start_after, + }) + .await + } + + async fn get_unbonded_nymnodes_by_identity_paged( + &self, + identity_key: IdentityKeyRef<'_>, + start_after: Option, + limit: Option, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetUnbondedNymNodesByIdentityKeyPaged { + identity_key: identity_key.to_string(), + limit, + start_after, + }) + .await + } + + async fn get_owned_nymnode( + &self, + address: &AccountId, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetOwnedNymNode { + address: address.to_string(), + }) + .await + } + + async fn get_nymnode_details( + &self, + node_id: NodeId, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetNymNodeDetails { node_id }) + .await + } + + async fn get_nymnode_details_by_identity( + &self, + node_identity: IdentityKey, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetNymNodeDetailsByIdentityKey { node_identity }) + .await + } + + async fn get_nymnode_rewarding_details( + &self, + node_id: NodeId, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetNodeRewardingDetails { node_id }) + .await + } + + async fn get_node_stake_saturation( + &self, + node_id: NodeId, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetNodeStakeSaturation { node_id }) + .await + } + + async fn get_unbonded_nymnode_information( + &self, + node_id: NodeId, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetUnbondedNymNode { node_id }) + .await + } + + async fn get_role_assignment(&self, role: Role) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetRoleAssignment { role }) + .await + } + + async fn get_rewarded_set_metadata(&self) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetRewardedSetMetadata {}) + .await + } + // delegation-related: /// Gets list of all delegations towards particular mixnode on particular page. async fn get_mixnode_delegations_paged( &self, - mix_id: MixId, + node_id: NodeId, start_after: Option, limit: Option, - ) -> Result { - self.query_mixnet_contract(MixnetQueryMsg::GetMixnodeDelegations { - mix_id, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetNodeDelegations { + node_id, start_after, limit, }) @@ -302,7 +388,7 @@ pub trait MixnetQueryClient { async fn get_delegator_delegations_paged( &self, delegator: &AccountId, - start_after: Option<(MixId, OwnerProxySubKey)>, + start_after: Option<(NodeId, OwnerProxySubKey)>, limit: Option, ) -> Result { self.query_mixnet_contract(MixnetQueryMsg::GetDelegatorDelegations { @@ -316,12 +402,12 @@ pub trait MixnetQueryClient { /// Checks value of delegation of given client towards particular mixnode. async fn get_delegation_details( &self, - mix_id: MixId, + node_id: NodeId, delegator: &AccountId, proxy: Option, - ) -> Result { + ) -> Result { self.query_mixnet_contract(MixnetQueryMsg::GetDelegationDetails { - mix_id, + node_id, delegator: delegator.to_string(), proxy, }) @@ -351,21 +437,21 @@ pub trait MixnetQueryClient { async fn get_pending_mixnode_operator_reward( &self, - mix_id: MixId, + node_id: NodeId, ) -> Result { - self.query_mixnet_contract(MixnetQueryMsg::GetPendingMixNodeOperatorReward { mix_id }) + self.query_mixnet_contract(MixnetQueryMsg::GetPendingNodeOperatorReward { node_id }) .await } async fn get_pending_delegator_reward( &self, delegator: &AccountId, - mix_id: MixId, + node_id: NodeId, proxy: Option, ) -> Result { self.query_mixnet_contract(MixnetQueryMsg::GetPendingDelegatorReward { address: delegator.to_string(), - mix_id, + node_id, proxy, }) .await @@ -374,12 +460,14 @@ pub trait MixnetQueryClient { // given the provided performance, estimate the reward at the end of the current epoch async fn get_estimated_current_epoch_operator_reward( &self, - mix_id: MixId, + node_id: NodeId, estimated_performance: Performance, + estimated_work: Option, ) -> Result { self.query_mixnet_contract(MixnetQueryMsg::GetEstimatedCurrentEpochOperatorReward { - mix_id, + node_id, estimated_performance, + estimated_work, }) .await } @@ -388,15 +476,15 @@ pub trait MixnetQueryClient { async fn get_estimated_current_epoch_delegator_reward( &self, delegator: &AccountId, - mix_id: MixId, - proxy: Option, + node_id: NodeId, estimated_performance: Performance, + estimated_work: Option, ) -> Result { self.query_mixnet_contract(MixnetQueryMsg::GetEstimatedCurrentEpochDelegatorReward { address: delegator.to_string(), - mix_id, - proxy, + node_id, estimated_performance, + estimated_work, }) .await } @@ -450,22 +538,6 @@ pub trait MixnetQueryClient { }) .await } - - async fn get_node_family_by_label( - &self, - label: String, - ) -> Result { - self.query_mixnet_contract(MixnetQueryMsg::GetFamilyByLabel { label }) - .await - } - - async fn get_node_family_by_head( - &self, - head: String, - ) -> Result { - self.query_mixnet_contract(MixnetQueryMsg::GetFamilyByHead { head }) - .await - } } // extension trait to the query client to deal with the paged queries @@ -473,18 +545,35 @@ pub trait MixnetQueryClient { #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] pub trait PagedMixnetQueryClient: MixnetQueryClient { - async fn get_all_node_families(&self) -> Result, NyxdError> { - collect_paged!(self, get_all_node_families_paged, families) + async fn get_all_nymnode_bonds(&self) -> Result, NyxdError> { + collect_paged!(self, get_nymnode_bonds_paged, nodes) } - async fn get_all_family_members(&self) -> Result, NyxdError> { - collect_paged!(self, get_all_family_members_paged, members) + async fn get_all_nymnodes_detailed(&self) -> Result, NyxdError> { + collect_paged!(self, get_nymnodes_detailed_paged, nodes) } - async fn get_all_rewarded_set_mixnodes( + async fn get_all_unbonded_nymnodes(&self) -> Result, NyxdError> { + collect_paged!(self, get_unbonded_nymnodes_paged, nodes) + } + + async fn get_all_unbonded_nymnodes_by_owner( &self, - ) -> Result, NyxdError> { - collect_paged!(self, get_rewarded_set_paged, nodes) + owner: &AccountId, + ) -> Result, NyxdError> { + collect_paged!(self, get_unbonded_nymnodes_by_owner_paged, nodes, owner) + } + + async fn get_all_unbonded_nymnodes_by_identity( + &self, + identity_key: IdentityKeyRef<'_>, + ) -> Result, NyxdError> { + collect_paged!( + self, + get_unbonded_nymnodes_by_identity_paged, + nodes, + identity_key + ) } async fn get_all_mixnode_bonds(&self) -> Result, NyxdError> { @@ -495,31 +584,40 @@ pub trait PagedMixnetQueryClient: MixnetQueryClient { collect_paged!(self, get_mixnodes_detailed_paged, nodes) } - async fn get_all_unbonded_mixnodes(&self) -> Result, NyxdError> { - collect_paged!(self, get_unbonded_paged, nodes) + async fn get_all_unbonded_mixnodes(&self) -> Result, NyxdError> { + collect_paged!(self, get_unbonded_mixnodes_paged, nodes) } async fn get_all_unbonded_mixnodes_by_owner( &self, owner: &AccountId, - ) -> Result, NyxdError> { - collect_paged!(self, get_unbonded_by_owner_paged, nodes, owner) + ) -> Result, NyxdError> { + collect_paged!(self, get_unbonded_mixnodes_by_owner_paged, nodes, owner) } async fn get_all_unbonded_mixnodes_by_identity( &self, identity_key: IdentityKeyRef<'_>, - ) -> Result, NyxdError> { - collect_paged!(self, get_unbonded_by_identity_paged, nodes, identity_key) + ) -> Result, NyxdError> { + collect_paged!( + self, + get_unbonded_mixnodes_by_identity_paged, + nodes, + identity_key + ) } async fn get_all_gateways(&self) -> Result, NyxdError> { collect_paged!(self, get_gateways_paged, nodes) } + async fn get_all_preassigned_gateway_ids(&self) -> Result, NyxdError> { + collect_paged!(self, get_preassigned_gateway_ids_paged, ids) + } + async fn get_all_single_mixnode_delegations( &self, - mix_id: MixId, + mix_id: NodeId, ) -> Result, NyxdError> { collect_paged!(self, get_mixnode_delegations_paged, delegations, mix_id) } @@ -554,6 +652,65 @@ pub trait PagedMixnetQueryClient: MixnetQueryClient { #[async_trait] impl PagedMixnetQueryClient for T where T: MixnetQueryClient {} +// extension help to provide extra functionalities based on existing queries: +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +pub trait MixnetQueryClientExt: MixnetQueryClient { + async fn get_rewarded_set(&self) -> Result { + let error_response = |message| Err(NyxdError::extension_query_failure("mixnet", message)); + + let metadata = self.get_rewarded_set_metadata().await?; + if !metadata.metadata.fully_assigned { + return error_response("the rewarded set hasn't been fully assigned for this epoch"); + } + let expected_epoch_id = metadata.metadata.epoch_id; + + // if we have to query those things more frequently, we could do it concurrently, + // but as it stands now, it happens so infrequently it might as well be sequential + let entry = self.get_role_assignment(Role::EntryGateway).await?; + if entry.epoch_id != expected_epoch_id { + return error_response("the nodes assigned for 'entry' returned unexpected epoch_id"); + } + + let exit = self.get_role_assignment(Role::ExitGateway).await?; + if exit.epoch_id != expected_epoch_id { + return error_response("the nodes assigned for 'exit' returned unexpected epoch_id"); + } + + let layer1 = self.get_role_assignment(Role::Layer1).await?; + if layer1.epoch_id != expected_epoch_id { + return error_response("the nodes assigned for 'layer1' returned unexpected epoch_id"); + } + + let layer2 = self.get_role_assignment(Role::Layer2).await?; + if layer2.epoch_id != expected_epoch_id { + return error_response("the nodes assigned for 'layer2' returned unexpected epoch_id"); + } + + let layer3 = self.get_role_assignment(Role::Layer3).await?; + if layer3.epoch_id != expected_epoch_id { + return error_response("the nodes assigned for 'layer3' returned unexpected epoch_id"); + } + + let standby = self.get_role_assignment(Role::Standby).await?; + if standby.epoch_id != expected_epoch_id { + return error_response("the nodes assigned for 'standby' returned unexpected epoch_id"); + } + + Ok(RewardedSet { + entry_gateways: entry.nodes, + exit_gateways: exit.nodes, + layer1: layer1.nodes, + layer2: layer2.nodes, + layer3: layer3.nodes, + standby: standby.nodes, + }) + } +} + +#[async_trait] +impl MixnetQueryClientExt for T where T: MixnetQueryClient {} + #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] impl MixnetQueryClient for C @@ -576,6 +733,7 @@ where mod tests { use super::*; use crate::nyxd::contract_traits::tests::IgnoreValue; + use nym_mixnet_contract_common::QueryMsg; // it's enough that this compiles and clippy is happy about it #[allow(dead_code)] @@ -585,24 +743,6 @@ mod tests { ) -> u32 { match msg { MixnetQueryMsg::Admin {} => client.admin().ignore(), - MixnetQueryMsg::GetAllFamiliesPaged { limit, start_after } => client - .get_all_family_members_paged(start_after, limit) - .ignore(), - MixnetQueryMsg::GetAllMembersPaged { limit, start_after } => client - .get_all_family_members_paged(start_after, limit) - .ignore(), - MixnetQueryMsg::GetFamilyByHead { head } => { - client.get_node_family_by_head(head).ignore() - } - MixnetQueryMsg::GetFamilyByLabel { label } => { - client.get_node_family_by_label(label).ignore() - } - MixnetQueryMsg::GetFamilyMembersByHead { head } => { - client.get_family_members_by_head(head).ignore() - } - MixnetQueryMsg::GetFamilyMembersByLabel { label } => { - client.get_family_members_by_label(label).ignore() - } MixnetQueryMsg::GetContractVersion {} => client.get_mixnet_contract_version().ignore(), MixnetQueryMsg::GetCW2ContractVersion {} => { client.get_mixnet_contract_cw2_version().ignore() @@ -617,31 +757,28 @@ mod tests { MixnetQueryMsg::GetCurrentIntervalDetails {} => { client.get_current_interval_details().ignore() } - MixnetQueryMsg::GetRewardedSet { limit, start_after } => { - client.get_rewarded_set_paged(start_after, limit).ignore() - } MixnetQueryMsg::GetMixNodeBonds { limit, start_after } => { client.get_mixnode_bonds_paged(start_after, limit).ignore() } MixnetQueryMsg::GetMixNodesDetailed { limit, start_after } => client .get_mixnodes_detailed_paged(start_after, limit) .ignore(), - MixnetQueryMsg::GetUnbondedMixNodes { limit, start_after } => { - client.get_unbonded_paged(start_after, limit).ignore() - } + MixnetQueryMsg::GetUnbondedMixNodes { limit, start_after } => client + .get_unbonded_mixnodes_paged(start_after, limit) + .ignore(), MixnetQueryMsg::GetUnbondedMixNodesByOwner { owner, limit, start_after, } => client - .get_unbonded_by_owner_paged(&owner.parse().unwrap(), start_after, limit) + .get_unbonded_mixnodes_by_owner_paged(&owner.parse().unwrap(), start_after, limit) .ignore(), MixnetQueryMsg::GetUnbondedMixNodesByIdentityKey { identity_key, limit, start_after, } => client - .get_unbonded_by_identity_paged(&identity_key, start_after, limit) + .get_unbonded_mixnodes_by_identity_paged(&identity_key, start_after, limit) .ignore(), MixnetQueryMsg::GetOwnedMixnode { address } => { client.get_owned_mixnode(&address.parse().unwrap()).ignore() @@ -661,7 +798,6 @@ mod tests { MixnetQueryMsg::GetBondedMixnodeDetailsByIdentity { mix_identity } => client .get_mixnode_details_by_identity(mix_identity) .ignore(), - MixnetQueryMsg::GetLayerDistribution {} => client.get_layer_distribution().ignore(), MixnetQueryMsg::GetGateways { start_after, limit } => { client.get_gateways_paged(start_after, limit).ignore() } @@ -671,8 +807,8 @@ mod tests { MixnetQueryMsg::GetOwnedGateway { address } => { client.get_owned_gateway(&address.parse().unwrap()).ignore() } - MixnetQueryMsg::GetMixnodeDelegations { - mix_id, + MixnetQueryMsg::GetNodeDelegations { + node_id: mix_id, start_after, limit, } => client @@ -686,7 +822,7 @@ mod tests { .get_delegator_delegations_paged(&delegator.parse().unwrap(), start_after, limit) .ignore(), MixnetQueryMsg::GetDelegationDetails { - mix_id, + node_id: mix_id, delegator, proxy, } => client @@ -698,33 +834,38 @@ mod tests { MixnetQueryMsg::GetPendingOperatorReward { address } => client .get_pending_operator_reward(&address.parse().unwrap()) .ignore(), - MixnetQueryMsg::GetPendingMixNodeOperatorReward { mix_id } => { + MixnetQueryMsg::GetPendingNodeOperatorReward { node_id: mix_id } => { client.get_pending_mixnode_operator_reward(mix_id).ignore() } MixnetQueryMsg::GetPendingDelegatorReward { address, - mix_id, + node_id: mix_id, proxy, } => client .get_pending_delegator_reward(&address.parse().unwrap(), mix_id, proxy) .ignore(), MixnetQueryMsg::GetEstimatedCurrentEpochOperatorReward { - mix_id, + node_id, estimated_performance, + estimated_work, } => client - .get_estimated_current_epoch_operator_reward(mix_id, estimated_performance) + .get_estimated_current_epoch_operator_reward( + node_id, + estimated_performance, + estimated_work, + ) .ignore(), MixnetQueryMsg::GetEstimatedCurrentEpochDelegatorReward { address, - mix_id, - proxy, + node_id, estimated_performance, + estimated_work, } => client .get_estimated_current_epoch_delegator_reward( &address.parse().unwrap(), - mix_id, - proxy, + node_id, estimated_performance, + estimated_work, ) .ignore(), MixnetQueryMsg::GetPendingEpochEvents { limit, start_after } => client @@ -745,6 +886,50 @@ mod tests { MixnetQueryMsg::GetSigningNonce { address } => { client.get_signing_nonce(&address.parse().unwrap()).ignore() } + QueryMsg::GetPreassignedGatewayIds { start_after, limit } => client + .get_preassigned_gateway_ids_paged(start_after, limit) + .ignore(), + QueryMsg::GetNymNodeBondsPaged { limit, start_after } => { + client.get_nymnode_bonds_paged(limit, start_after).ignore() + } + QueryMsg::GetNymNodesDetailedPaged { limit, start_after } => client + .get_nymnodes_detailed_paged(limit, start_after) + .ignore(), + QueryMsg::GetUnbondedNymNode { node_id } => { + client.get_unbonded_nymnode_information(node_id).ignore() + } + QueryMsg::GetUnbondedNymNodesPaged { limit, start_after } => client + .get_unbonded_nymnodes_paged(limit, start_after) + .ignore(), + QueryMsg::GetUnbondedNymNodesByOwnerPaged { + owner, + limit, + start_after, + } => client + .get_unbonded_nymnodes_by_owner_paged(&owner.parse().unwrap(), limit, start_after) + .ignore(), + QueryMsg::GetUnbondedNymNodesByIdentityKeyPaged { + identity_key, + limit, + start_after, + } => client + .get_unbonded_nymnodes_by_identity_paged(&identity_key, limit, start_after) + .ignore(), + QueryMsg::GetOwnedNymNode { address } => { + client.get_owned_nymnode(&address.parse().unwrap()).ignore() + } + QueryMsg::GetNymNodeDetails { node_id } => client.get_nymnode_details(node_id).ignore(), + QueryMsg::GetNymNodeDetailsByIdentityKey { node_identity } => client + .get_nymnode_details_by_identity(node_identity) + .ignore(), + QueryMsg::GetNodeRewardingDetails { node_id } => { + client.get_nymnode_rewarding_details(node_id).ignore() + } + QueryMsg::GetNodeStakeSaturation { node_id } => { + client.get_node_stake_saturation(node_id).ignore() + } + QueryMsg::GetRoleAssignment { role } => client.get_role_assignment(role).ignore(), + QueryMsg::GetRewardedSetMetadata {} => client.get_rewarded_set_metadata().ignore(), } } } diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_signing_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_signing_client.rs index f84becb55f..5a7b8fae26 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_signing_client.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_signing_client.rs @@ -10,13 +10,15 @@ use crate::signing::signer::OfflineSigner; use async_trait::async_trait; use cosmrs::AccountId; use nym_contracts_common::signing::MessageSignature; -use nym_mixnet_contract_common::families::FamilyHead; use nym_mixnet_contract_common::gateway::GatewayConfigUpdate; -use nym_mixnet_contract_common::mixnode::{MixNodeConfigUpdate, MixNodeCostParams}; -use nym_mixnet_contract_common::reward_params::{IntervalRewardingParamsUpdate, Performance}; +use nym_mixnet_contract_common::mixnode::{MixNodeConfigUpdate, NodeCostParams}; +use nym_mixnet_contract_common::nym_node::NodeConfigUpdate; +use nym_mixnet_contract_common::reward_params::{ + ActiveSetUpdate, IntervalRewardingParamsUpdate, NodeRewardingParameters, +}; use nym_mixnet_contract_common::{ - ContractStateParams, ExecuteMsg as MixnetExecuteMsg, Gateway, Layer, LayerAssignment, MixId, - MixNode, + ContractStateParams, ExecuteMsg as MixnetExecuteMsg, Gateway, MixNode, NodeId, NymNode, + RoleAssignment, }; #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] @@ -70,14 +72,14 @@ pub trait MixnetSigningClient { async fn update_active_set_size( &self, - active_set_size: u32, + update: ActiveSetUpdate, force_immediately: bool, fee: Option, ) -> Result { self.execute_mixnet_contract( fee, - MixnetExecuteMsg::UpdateActiveSetSize { - active_set_size, + MixnetExecuteMsg::UpdateActiveSetDistribution { + update, force_immediately, }, vec![], @@ -126,37 +128,6 @@ pub trait MixnetSigningClient { .await } - async fn advance_current_epoch( - &self, - new_rewarded_set: Vec, - expected_active_set_size: u32, - fee: Option, - ) -> Result { - self.execute_mixnet_contract( - fee, - MixnetExecuteMsg::AdvanceCurrentEpoch { - new_rewarded_set, - expected_active_set_size, - }, - vec![], - ) - .await - } - - async fn assign_node_layer( - &self, - mix_id: MixId, - layer: Layer, - fee: Option, - ) -> Result { - self.execute_mixnet_contract( - fee, - MixnetExecuteMsg::AssignNodeLayer { mix_id, layer }, - vec![], - ) - .await - } - async fn reconcile_epoch_events( &self, limit: Option, @@ -170,126 +141,21 @@ pub trait MixnetSigningClient { .await } - // family related - async fn create_family( + async fn assign_roles( &self, - label: String, + assignment: RoleAssignment, fee: Option, ) -> Result { - self.execute_mixnet_contract(fee, MixnetExecuteMsg::CreateFamily { label }, vec![]) + self.execute_mixnet_contract(fee, MixnetExecuteMsg::AssignRoles { assignment }, vec![]) .await } - async fn create_family_on_behalf( - &self, - owner_address: String, - label: String, - fee: Option, - ) -> Result { - self.execute_mixnet_contract( - fee, - MixnetExecuteMsg::CreateFamilyOnBehalf { - owner_address, - label, - }, - vec![], - ) - .await - } - - async fn join_family( - &self, - join_permit: MessageSignature, - family_head: FamilyHead, - fee: Option, - ) -> Result { - self.execute_mixnet_contract( - fee, - MixnetExecuteMsg::JoinFamily { - join_permit, - family_head, - }, - vec![], - ) - .await - } - - async fn join_family_on_behalf( - &self, - member_address: String, - join_permit: MessageSignature, - family_head: FamilyHead, - fee: Option, - ) -> Result { - self.execute_mixnet_contract( - fee, - MixnetExecuteMsg::JoinFamilyOnBehalf { - member_address, - join_permit, - family_head, - }, - vec![], - ) - .await - } - - async fn leave_family( - &self, - family_head: FamilyHead, - fee: Option, - ) -> Result { - self.execute_mixnet_contract(fee, MixnetExecuteMsg::LeaveFamily { family_head }, vec![]) - .await - } - - async fn leave_family_on_behalf( - &self, - member_address: String, - family_head: FamilyHead, - fee: Option, - ) -> Result { - self.execute_mixnet_contract( - fee, - MixnetExecuteMsg::LeaveFamilyOnBehalf { - member_address, - family_head, - }, - vec![], - ) - .await - } - - async fn kick_family_member( - &self, - member: String, - fee: Option, - ) -> Result { - self.execute_mixnet_contract(fee, MixnetExecuteMsg::KickFamilyMember { member }, vec![]) - .await - } - - async fn kick_family_member_on_behalf( - &self, - head_address: String, - member: String, - fee: Option, - ) -> Result { - self.execute_mixnet_contract( - fee, - MixnetExecuteMsg::KickFamilyMemberOnBehalf { - head_address, - member, - }, - vec![], - ) - .await - } // mixnode-related: async fn bond_mixnode( &self, mix_node: MixNode, - cost_params: MixNodeCostParams, + cost_params: NodeCostParams, owner_signature: MessageSignature, pledge: Coin, fee: Option, @@ -310,7 +176,7 @@ pub trait MixnetSigningClient { &self, owner: AccountId, mix_node: MixNode, - cost_params: MixNodeCostParams, + cost_params: NodeCostParams, owner_signature: MessageSignature, pledge: Coin, fee: Option, @@ -409,14 +275,14 @@ pub trait MixnetSigningClient { .await } - async fn update_mixnode_cost_params( + async fn update_cost_params( &self, - new_costs: MixNodeCostParams, + new_costs: NodeCostParams, fee: Option, ) -> Result { self.execute_mixnet_contract( fee, - MixnetExecuteMsg::UpdateMixnodeCostParams { new_costs }, + MixnetExecuteMsg::UpdateCostParams { new_costs }, vec![], ) .await @@ -425,7 +291,7 @@ pub trait MixnetSigningClient { async fn update_mixnode_cost_params_on_behalf( &self, owner: AccountId, - new_costs: MixNodeCostParams, + new_costs: NodeCostParams, fee: Option, ) -> Result { self.execute_mixnet_contract( @@ -559,26 +425,75 @@ pub trait MixnetSigningClient { .await } - // delegation-related: + // nym-node related: + async fn migrate_legacy_mixnode(&self, fee: Option) -> Result { + self.execute_mixnet_contract(fee, MixnetExecuteMsg::MigrateMixnode {}, vec![]) + .await + } - async fn delegate_to_mixnode( + async fn migrate_legacy_gateway( &self, - mix_id: MixId, - amount: Coin, + cost_params: Option, fee: Option, ) -> Result { self.execute_mixnet_contract( fee, - MixnetExecuteMsg::DelegateToMixnode { mix_id }, - vec![amount], + MixnetExecuteMsg::MigrateGateway { cost_params }, + vec![], ) .await } + async fn bond_nymnode( + &self, + node: NymNode, + cost_params: NodeCostParams, + owner_signature: MessageSignature, + pledge: Coin, + fee: Option, + ) -> Result { + self.execute_mixnet_contract( + fee, + MixnetExecuteMsg::BondNymNode { + node, + cost_params, + owner_signature, + }, + vec![pledge], + ) + .await + } + + async fn unbond_nymnode(&self, fee: Option) -> Result { + self.execute_mixnet_contract(fee, MixnetExecuteMsg::UnbondNymNode {}, vec![]) + .await + } + + async fn update_nymnode_config( + &self, + update: NodeConfigUpdate, + fee: Option, + ) -> Result { + self.execute_mixnet_contract(fee, MixnetExecuteMsg::UpdateNodeConfig { update }, vec![]) + .await + } + + // delegation-related: + + async fn delegate( + &self, + node_id: NodeId, + amount: Coin, + fee: Option, + ) -> Result { + self.execute_mixnet_contract(fee, MixnetExecuteMsg::Delegate { node_id }, vec![amount]) + .await + } + async fn delegate_to_mixnode_on_behalf( &self, delegate: AccountId, - mix_id: MixId, + mix_id: NodeId, amount: Coin, fee: Option, ) -> Result { @@ -593,23 +508,19 @@ pub trait MixnetSigningClient { .await } - async fn undelegate_from_mixnode( + async fn undelegate( &self, - mix_id: MixId, + node_id: NodeId, fee: Option, ) -> Result { - self.execute_mixnet_contract( - fee, - MixnetExecuteMsg::UndelegateFromMixnode { mix_id }, - vec![], - ) - .await + self.execute_mixnet_contract(fee, MixnetExecuteMsg::Undelegate { node_id }, vec![]) + .await } async fn undelegate_to_mixnode_on_behalf( &self, delegate: AccountId, - mix_id: MixId, + mix_id: NodeId, fee: Option, ) -> Result { self.execute_mixnet_contract( @@ -625,18 +536,15 @@ pub trait MixnetSigningClient { // reward-related - async fn reward_mixnode( + async fn reward_node( &self, - mix_id: MixId, - performance: Performance, + node_id: NodeId, + params: NodeRewardingParameters, fee: Option, ) -> Result { self.execute_mixnet_contract( fee, - MixnetExecuteMsg::RewardMixnode { - mix_id, - performance, - }, + MixnetExecuteMsg::RewardNode { node_id, params }, vec![], ) .await @@ -664,12 +572,12 @@ pub trait MixnetSigningClient { async fn withdraw_delegator_reward( &self, - mix_id: MixId, + node_id: NodeId, fee: Option, ) -> Result { self.execute_mixnet_contract( fee, - MixnetExecuteMsg::WithdrawDelegatorReward { mix_id }, + MixnetExecuteMsg::WithdrawDelegatorReward { node_id }, vec![], ) .await @@ -678,7 +586,7 @@ pub trait MixnetSigningClient { async fn withdraw_delegator_reward_on_behalf( &self, owner: AccountId, - mix_id: MixId, + mix_id: NodeId, fee: Option, ) -> Result { self.execute_mixnet_contract( @@ -699,7 +607,7 @@ pub trait MixnetSigningClient { async fn migrate_vested_delegation( &self, - mix_id: MixId, + mix_id: NodeId, fee: Option, ) -> Result { self.execute_mixnet_contract( @@ -761,6 +669,7 @@ where mod tests { use super::*; use crate::nyxd::contract_traits::tests::{mock_coin, IgnoreValue}; + use nym_mixnet_contract_common::ExecuteMsg; // it's enough that this compiles and clippy is happy about it #[allow(dead_code)] @@ -770,56 +679,17 @@ mod tests { ) { match msg { MixnetExecuteMsg::UpdateAdmin { admin } => client.update_admin(admin, None).ignore(), - MixnetExecuteMsg::AssignNodeLayer { mix_id, layer } => { - client.assign_node_layer(mix_id, layer, None).ignore() - } - MixnetExecuteMsg::CreateFamily { label } => client.create_family(label, None).ignore(), - MixnetExecuteMsg::JoinFamily { - join_permit, - family_head, - } => client.join_family(join_permit, family_head, None).ignore(), - MixnetExecuteMsg::LeaveFamily { family_head } => { - client.leave_family(family_head, None).ignore() - } - MixnetExecuteMsg::KickFamilyMember { member } => { - client.kick_family_member(member, None).ignore() - } - MixnetExecuteMsg::CreateFamilyOnBehalf { - owner_address, - label, - } => client - .create_family_on_behalf(owner_address, label, None) - .ignore(), - MixnetExecuteMsg::JoinFamilyOnBehalf { - member_address, - join_permit, - family_head, - } => client - .join_family_on_behalf(member_address, join_permit, family_head, None) - .ignore(), - MixnetExecuteMsg::LeaveFamilyOnBehalf { - member_address, - family_head, - } => client - .leave_family_on_behalf(member_address, family_head, None) - .ignore(), - MixnetExecuteMsg::KickFamilyMemberOnBehalf { - head_address, - member, - } => client - .kick_family_member_on_behalf(head_address, member, None) - .ignore(), MixnetExecuteMsg::UpdateRewardingValidatorAddress { address } => client .update_rewarding_validator_address(address.parse().unwrap(), None) .ignore(), MixnetExecuteMsg::UpdateContractStateParams { updated_parameters } => client .update_contract_state_params(updated_parameters, None) .ignore(), - MixnetExecuteMsg::UpdateActiveSetSize { - active_set_size, + MixnetExecuteMsg::UpdateActiveSetDistribution { + update, force_immediately, } => client - .update_active_set_size(active_set_size, force_immediately, None) + .update_active_set_size(update, force_immediately, None) .ignore(), MixnetExecuteMsg::UpdateRewardingParams { updated_params, @@ -842,12 +712,6 @@ mod tests { MixnetExecuteMsg::BeginEpochTransition {} => { client.begin_epoch_transition(None).ignore() } - MixnetExecuteMsg::AdvanceCurrentEpoch { - new_rewarded_set, - expected_active_set_size, - } => client - .advance_current_epoch(new_rewarded_set, expected_active_set_size, None) - .ignore(), MixnetExecuteMsg::ReconcileEpochEvents { limit } => { client.reconcile_epoch_events(limit, None).ignore() } @@ -887,8 +751,8 @@ mod tests { MixnetExecuteMsg::UnbondMixnodeOnBehalf { owner } => client .unbond_mixnode_on_behalf(owner.parse().unwrap(), None) .ignore(), - MixnetExecuteMsg::UpdateMixnodeCostParams { new_costs } => { - client.update_mixnode_cost_params(new_costs, None).ignore() + MixnetExecuteMsg::UpdateCostParams { new_costs } => { + client.update_cost_params(new_costs, None).ignore() } MixnetExecuteMsg::UpdateMixnodeCostParamsOnBehalf { new_costs, owner } => client .update_mixnode_cost_params_on_behalf(owner.parse().unwrap(), new_costs, None) @@ -928,29 +792,28 @@ mod tests { MixnetExecuteMsg::UpdateGatewayConfigOnBehalf { new_config, owner } => client .update_gateway_config_on_behalf(owner.parse().unwrap(), new_config, None) .ignore(), - MixnetExecuteMsg::DelegateToMixnode { mix_id } => client - .delegate_to_mixnode(mix_id, mock_coin(), None) - .ignore(), + MixnetExecuteMsg::Delegate { node_id: mix_id } => { + client.delegate(mix_id, mock_coin(), None).ignore() + } MixnetExecuteMsg::DelegateToMixnodeOnBehalf { mix_id, delegate } => client .delegate_to_mixnode_on_behalf(delegate.parse().unwrap(), mix_id, mock_coin(), None) .ignore(), - MixnetExecuteMsg::UndelegateFromMixnode { mix_id } => { - client.undelegate_from_mixnode(mix_id, None).ignore() + MixnetExecuteMsg::Undelegate { node_id: mix_id } => { + client.undelegate(mix_id, None).ignore() } MixnetExecuteMsg::UndelegateFromMixnodeOnBehalf { mix_id, delegate } => client .undelegate_to_mixnode_on_behalf(delegate.parse().unwrap(), mix_id, None) .ignore(), - MixnetExecuteMsg::RewardMixnode { - mix_id, - performance, - } => client.reward_mixnode(mix_id, performance, None).ignore(), + MixnetExecuteMsg::RewardNode { node_id, params } => { + client.reward_node(node_id, params, None).ignore() + } MixnetExecuteMsg::WithdrawOperatorReward {} => { client.withdraw_operator_reward(None).ignore() } MixnetExecuteMsg::WithdrawOperatorRewardOnBehalf { owner } => client .withdraw_operator_reward_on_behalf(owner.parse().unwrap(), None) .ignore(), - MixnetExecuteMsg::WithdrawDelegatorReward { mix_id } => { + MixnetExecuteMsg::WithdrawDelegatorReward { node_id: mix_id } => { client.withdraw_delegator_reward(mix_id, None).ignore() } MixnetExecuteMsg::WithdrawDelegatorRewardOnBehalf { mix_id, owner } => client @@ -963,6 +826,32 @@ mod tests { client.migrate_vested_delegation(mix_id, None).ignore() } + ExecuteMsg::AssignRoles { assignment } => { + client.assign_roles(assignment, None).ignore() + } + ExecuteMsg::MigrateMixnode {} => client.migrate_legacy_mixnode(None).ignore(), + ExecuteMsg::MigrateGateway { cost_params } => { + client.migrate_legacy_gateway(cost_params, None).ignore() + } + ExecuteMsg::BondNymNode { + node, + cost_params, + owner_signature, + } => client + .bond_nymnode(node, cost_params, owner_signature, mock_coin(), None) + .ignore(), + ExecuteMsg::UnbondNymNode {} => client.unbond_nymnode(None).ignore(), + ExecuteMsg::UpdateNodeConfig { update } => { + client.update_nymnode_config(update, None).ignore() + } + + ExecuteMsg::TestingUncheckedBondLegacyMixnode { .. } => { + todo!("purposely not implemented") + } + ExecuteMsg::TestingUncheckedBondLegacyGateway { .. } => { + todo!("purposely not implemented") + } + #[cfg(feature = "contract-testing")] MixnetExecuteMsg::TestingResolveAllPendingEvents { .. } => { client.testing_resolve_all_pending_events(None).ignore() diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/vesting_query_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/vesting_query_client.rs index fa6bd643fa..02eaf8c4b8 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/vesting_query_client.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/vesting_query_client.rs @@ -9,7 +9,7 @@ use crate::nyxd::CosmWasmClient; use async_trait::async_trait; use cosmwasm_std::{Coin as CosmWasmCoin, Timestamp}; use nym_contracts_common::ContractBuildInformation; -use nym_mixnet_contract_common::MixId; +use nym_mixnet_contract_common::NodeId; use nym_vesting_contract_common::{ messages::QueryMsg as VestingQueryMsg, Account, AccountVestingCoins, AccountsResponse, AllDelegationsResponse, BaseVestingAccountInfo, DelegationTimesResponse, @@ -238,7 +238,7 @@ pub trait VestingQueryClient { async fn get_vesting_delegation( &self, address: &str, - mix_id: MixId, + mix_id: NodeId, block_timestamp_secs: u64, ) -> Result { self.query_vesting_contract(VestingQueryMsg::GetDelegation { @@ -252,7 +252,7 @@ pub trait VestingQueryClient { async fn get_total_delegation_amount( &self, address: &str, - mix_id: MixId, + mix_id: NodeId, ) -> Result { self.query_vesting_contract(VestingQueryMsg::GetTotalDelegationAmount { address: address.to_string(), @@ -264,7 +264,7 @@ pub trait VestingQueryClient { async fn get_delegation_timestamps( &self, address: &str, - mix_id: MixId, + mix_id: NodeId, ) -> Result { self.query_vesting_contract(VestingQueryMsg::GetDelegationTimes { address: address.to_string(), @@ -275,7 +275,7 @@ pub trait VestingQueryClient { async fn get_all_vesting_delegations_paged( &self, - start_after: Option<(u32, MixId, u64)>, + start_after: Option<(u32, NodeId, u64)>, limit: Option, ) -> Result { self.query_vesting_contract(VestingQueryMsg::GetAllDelegations { start_after, limit }) diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/vesting_signing_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/vesting_signing_client.rs index 07472c4262..2f50016f91 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/vesting_signing_client.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/vesting_signing_client.rs @@ -9,10 +9,9 @@ use crate::signing::signer::OfflineSigner; use async_trait::async_trait; use cosmrs::AccountId; use nym_contracts_common::signing::MessageSignature; -use nym_mixnet_contract_common::families::FamilyHead; use nym_mixnet_contract_common::gateway::GatewayConfigUpdate; -use nym_mixnet_contract_common::mixnode::{MixNodeConfigUpdate, MixNodeCostParams}; -use nym_mixnet_contract_common::{Gateway, MixId, MixNode}; +use nym_mixnet_contract_common::mixnode::{MixNodeConfigUpdate, NodeCostParams}; +use nym_mixnet_contract_common::{Gateway, MixNode, NodeId}; use nym_vesting_contract_common::messages::ExecuteMsg as VestingExecuteMsg; use nym_vesting_contract_common::{PledgeCap, VestingSpecification}; @@ -28,7 +27,7 @@ pub trait VestingSigningClient { async fn vesting_update_mixnode_cost_params( &self, - new_costs: MixNodeCostParams, + new_costs: NodeCostParams, fee: Option, ) -> Result { self.execute_vesting_contract( @@ -124,7 +123,7 @@ pub trait VestingSigningClient { async fn vesting_bond_mixnode( &self, mix_node: MixNode, - cost_params: MixNodeCostParams, + cost_params: NodeCostParams, owner_signature: MessageSignature, pledge: Coin, fee: Option, @@ -204,7 +203,7 @@ pub trait VestingSigningClient { async fn vesting_track_undelegation( &self, address: &str, - mix_id: MixId, + mix_id: NodeId, amount: Coin, fee: Option, ) -> Result { @@ -222,7 +221,7 @@ pub trait VestingSigningClient { async fn vesting_delegate_to_mixnode( &self, - mix_id: MixId, + mix_id: NodeId, amount: Coin, on_behalf_of: Option, fee: Option, @@ -241,7 +240,7 @@ pub trait VestingSigningClient { async fn vesting_undelegate_from_mixnode( &self, - mix_id: MixId, + mix_id: NodeId, on_behalf_of: Option, fee: Option, ) -> Result { @@ -301,7 +300,7 @@ pub trait VestingSigningClient { async fn vesting_withdraw_delegator_reward( &self, - mix_id: MixId, + mix_id: NodeId, fee: Option, ) -> Result { self.execute_vesting_contract( @@ -354,50 +353,6 @@ pub trait VestingSigningClient { ) .await } - - async fn vesting_create_family( - &self, - label: String, - fee: Option, - ) -> Result { - self.execute_vesting_contract(fee, VestingExecuteMsg::CreateFamily { label }, vec![]) - .await - } - - async fn vesting_join_family( - &self, - join_permit: MessageSignature, - family_head: FamilyHead, - fee: Option, - ) -> Result { - self.execute_vesting_contract( - fee, - VestingExecuteMsg::JoinFamily { - join_permit, - family_head, - }, - vec![], - ) - .await - } - - async fn vesting_leave_family( - &self, - family_head: FamilyHead, - fee: Option, - ) -> Result { - self.execute_vesting_contract(fee, VestingExecuteMsg::LeaveFamily { family_head }, vec![]) - .await - } - - async fn vesting_kick_family_member( - &self, - member: String, - fee: Option, - ) -> Result { - self.execute_vesting_contract(fee, VestingExecuteMsg::KickFamilyMember { member }, vec![]) - .await - } } #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] @@ -446,21 +401,6 @@ mod tests { msg: VestingExecuteMsg, ) { match msg { - VestingExecuteMsg::CreateFamily { label } => { - client.vesting_create_family(label, None).ignore() - } - VestingExecuteMsg::JoinFamily { - join_permit, - family_head, - } => client - .vesting_join_family(join_permit, family_head, None) - .ignore(), - VestingExecuteMsg::LeaveFamily { family_head } => { - client.vesting_leave_family(family_head, None).ignore() - } - VestingExecuteMsg::KickFamilyMember { member } => { - client.vesting_kick_family_member(member, None).ignore() - } VestingExecuteMsg::TrackReward { amount, address } => client .vesting_track_reward(amount.into(), address, None) .ignore(), diff --git a/common/client-libs/validator-client/src/nyxd/error.rs b/common/client-libs/validator-client/src/nyxd/error.rs index c71ed596ad..e4b2f1b9e5 100644 --- a/common/client-libs/validator-client/src/nyxd/error.rs +++ b/common/client-libs/validator-client/src/nyxd/error.rs @@ -154,6 +154,23 @@ pub enum NyxdError { #[error("the response data has invalid size. got {got} bytes, but expected {expected} bytes instead")] MalformedResponseData { got: usize, expected: usize }, + + #[error( + "one of the extension query for {contract} failed with the following message: {message}" + )] + ExtensionQueryFailure { contract: String, message: String }, +} + +impl NyxdError { + pub fn extension_query_failure( + contract: impl Into, + message: impl Into, + ) -> Self { + NyxdError::ExtensionQueryFailure { + contract: contract.into(), + message: message.into(), + } + } } // The purpose of parsing the abci query result is that we want to generate the `pretty_log` if diff --git a/common/commands/src/validator/cosmwasm/generators/mixnet.rs b/common/commands/src/validator/cosmwasm/generators/mixnet.rs index afd21fde9a..7f97809e36 100644 --- a/common/commands/src/validator/cosmwasm/generators/mixnet.rs +++ b/common/commands/src/validator/cosmwasm/generators/mixnet.rs @@ -4,6 +4,7 @@ use clap::Parser; use cosmwasm_std::Decimal; use log::{debug, info}; +use nym_mixnet_contract_common::reward_params::RewardedSetParams; use nym_mixnet_contract_common::{ InitialRewardingParams, InstantiateMsg, OperatingCostRange, Percent, ProfitMarginRange, }; @@ -56,11 +57,17 @@ pub struct Args { #[clap(long, default_value_t = 2)] pub interval_pool_emission: u64, - #[clap(long, default_value_t = 240)] - pub rewarded_set_size: u32, + #[clap(long, default_value_t = 50)] + pub(crate) entry_gateways: u32, - #[clap(long, default_value_t = 240)] - pub active_set_size: u32, + #[clap(long, default_value_t = 70)] + pub(crate) exit_gateways: u32, + + #[clap(long, default_value_t = 120)] + pub(crate) mixnodes: u32, + + #[clap(long, default_value_t = 0)] + pub(crate) standby: u32, #[clap(long, default_value_t = Percent::zero())] pub minimum_profit_margin_percent: Percent, @@ -95,8 +102,13 @@ pub async fn generate(args: Args) { .expect("active_set_work_factor can't be converted to Decimal"), interval_pool_emission: Percent::from_percentage_value(args.interval_pool_emission) .expect("interval_pool_emission can't be converted to Percent"), - rewarded_set_size: args.rewarded_set_size, - active_set_size: args.active_set_size, + + rewarded_set_params: RewardedSetParams { + entry_gateways: args.entry_gateways, + exit_gateways: args.exit_gateways, + mixnodes: args.mixnodes, + standby: args.standby, + }, }; debug!("initial_rewarding_params: {:?}", initial_rewarding_params); diff --git a/common/commands/src/validator/mixnet/delegators/delegate_to_mixnode.rs b/common/commands/src/validator/mixnet/delegators/delegate_to_mixnode.rs index 54b2516d6e..dc8a2d591f 100644 --- a/common/commands/src/validator/mixnet/delegators/delegate_to_mixnode.rs +++ b/common/commands/src/validator/mixnet/delegators/delegate_to_mixnode.rs @@ -4,13 +4,13 @@ use crate::context::SigningClient; use clap::Parser; use log::info; -use nym_mixnet_contract_common::{Coin, MixId}; +use nym_mixnet_contract_common::{Coin, NodeId}; use nym_validator_client::nyxd::contract_traits::{MixnetQueryClient, MixnetSigningClient}; #[derive(Debug, Parser)] pub struct Args { #[clap(long)] - pub mix_id: Option, + pub mix_id: Option, #[clap(long)] pub identity_key: Option, @@ -43,7 +43,7 @@ pub async fn delegate_to_mixnode(args: Args, client: SigningClient) { let coin = Coin::new(args.amount, denom); let res = client - .delegate_to_mixnode(mix_id, coin.into(), None) + .delegate(mix_id, coin.into(), None) .await .expect("failed to delegate to mixnode!"); diff --git a/common/commands/src/validator/mixnet/delegators/delegate_to_multiple_mixnodes.rs b/common/commands/src/validator/mixnet/delegators/delegate_to_multiple_mixnodes.rs index 787d27a56e..77d59d6bb3 100644 --- a/common/commands/src/validator/mixnet/delegators/delegate_to_multiple_mixnodes.rs +++ b/common/commands/src/validator/mixnet/delegators/delegate_to_multiple_mixnodes.rs @@ -1,21 +1,18 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use std::cmp::Ordering; -use std::collections::{HashMap, HashSet}; -use std::fs; -use std::fs::OpenOptions; - use clap::Parser; use comfy_table::Table; use csv::WriterBuilder; use log::info; use nym_mixnet_contract_common::ExecuteMsg; -use nym_mixnet_contract_common::ExecuteMsg::{DelegateToMixnode, UndelegateFromMixnode}; - -use nym_mixnet_contract_common::PendingEpochEventKind::{Delegate, Undelegate}; +use nym_mixnet_contract_common::PendingEpochEventKind; use nym_validator_client::nyxd::contract_traits::{NymContractsProvider, PagedMixnetQueryClient}; use nym_validator_client::nyxd::Coin; +use std::cmp::Ordering; +use std::collections::{HashMap, HashSet}; +use std::fs; +use std::fs::OpenOptions; use crate::context::SigningClient; use crate::utils::pretty_coin; @@ -40,7 +37,7 @@ pub struct Args { #[derive(Debug)] pub struct InputFileRow { - pub mix_id: String, + pub node_id: String, pub amount: Coin, } #[derive(Debug)] @@ -76,7 +73,7 @@ impl InputFileReader { } rows.push(InputFileRow { - mix_id, + node_id: mix_id, amount: Coin { amount: micro_nym_amount, denom: "unym".to_string(), @@ -140,8 +137,10 @@ async fn fetch_delegation_data( let mut pending_delegation_map: HashMap = HashMap::new(); for delegation in delegations { - existing_delegation_map - .insert(delegation.mix_id.to_string(), Coin::from(delegation.amount)); + existing_delegation_map.insert( + delegation.node_id.to_string(), + Coin::from(delegation.amount), + ); } // Look for pending delegate / undelegate events which might be of interest to us @@ -155,27 +154,27 @@ async fn fetch_delegation_data( for event in pending_events { match event.event.kind { // If a pending undelegate tx is found, remove it from delegation map - Undelegate { owner, mix_id, .. } => { + PendingEpochEventKind::Undelegate { owner, node_id, .. } => { if owner == address.as_ref() - && existing_delegation_map.contains_key(&mix_id.to_string()) + && existing_delegation_map.contains_key(&node_id.to_string()) { - existing_delegation_map.remove(&mix_id.to_string()); + existing_delegation_map.remove(&node_id.to_string()); } } // If a pending delegation event is found, gather them to consolidate later - Delegate { + PendingEpochEventKind::Delegate { owner, - mix_id, + node_id, amount, .. } => { if owner == address.as_ref() { let mut amount = Coin::from(amount); - if let Some(pending_record) = pending_delegation_map.get(&mix_id.to_string()) { + if let Some(pending_record) = pending_delegation_map.get(&node_id.to_string()) { amount.amount += pending_record.amount; } - pending_delegation_map.insert(mix_id.to_string(), amount); + pending_delegation_map.insert(node_id.to_string(), amount); } } _ => {} @@ -217,7 +216,7 @@ pub async fn delegate_to_multiple_mixnodes(args: Args, client: SigningClient) { for row in &records.rows { let input_amount = row.amount.amount; let existing_delegation_amount = existing_delegation_map - .get(&row.mix_id) + .get(&row.node_id) .map_or(0, |coin| coin.amount); match existing_delegation_amount.cmp(&input_amount) { @@ -229,25 +228,26 @@ pub async fn delegate_to_multiple_mixnodes(args: Args, client: SigningClient) { amount: input_amount - existing_delegation_amount, denom: row.amount.denom.clone(), }; - let mix_id = row.mix_id.clone().parse::().unwrap(); - delegation_msgs.push((DelegateToMixnode { mix_id }, vec![difference.clone()])); + let node_id = row.node_id.clone().parse::().unwrap(); + delegation_msgs.push((ExecuteMsg::Delegate { node_id }, vec![difference.clone()])); delegation_table.add_row(&[ - row.mix_id.clone(), + row.node_id.clone(), pretty_coin(&row.amount), pretty_coin(&difference), ]); } Ordering::Greater => { - let mix_id = row.mix_id.clone().parse::().unwrap(); + let node_id = row.node_id.clone().parse::().unwrap(); let coins: Vec = vec![]; - undelegation_msgs.push((UndelegateFromMixnode { mix_id }, coins)); - undelegation_table.add_row(&[row.mix_id.clone()]); + undelegation_msgs.push((ExecuteMsg::Undelegate { node_id }, coins)); + undelegation_table.add_row(&[row.node_id.clone()]); if row.amount.amount > 0 { - delegation_msgs.push((DelegateToMixnode { mix_id }, vec![row.amount.clone()])); + delegation_msgs + .push((ExecuteMsg::Delegate { node_id }, vec![row.amount.clone()])); delegation_table.add_row(&[ - row.mix_id.clone(), + row.node_id.clone(), pretty_coin(&row.amount), pretty_coin(&row.amount), ]); diff --git a/common/commands/src/validator/mixnet/delegators/migrate_vested_delegation.rs b/common/commands/src/validator/mixnet/delegators/migrate_vested_delegation.rs index 8523e63639..8ac1ab9c07 100644 --- a/common/commands/src/validator/mixnet/delegators/migrate_vested_delegation.rs +++ b/common/commands/src/validator/mixnet/delegators/migrate_vested_delegation.rs @@ -4,13 +4,13 @@ use crate::context::SigningClient; use clap::Parser; use log::info; -use nym_mixnet_contract_common::MixId; +use nym_mixnet_contract_common::NodeId; use nym_validator_client::nyxd::contract_traits::{MixnetQueryClient, MixnetSigningClient}; #[derive(Debug, Parser)] pub struct Args { #[clap(long)] - pub mix_id: Option, + pub mix_id: Option, #[clap(long)] pub identity_key: Option, diff --git a/common/commands/src/validator/mixnet/delegators/query_for_delegations.rs b/common/commands/src/validator/mixnet/delegators/query_for_delegations.rs index b1a223b603..b05f44b6fb 100644 --- a/common/commands/src/validator/mixnet/delegators/query_for_delegations.rs +++ b/common/commands/src/validator/mixnet/delegators/query_for_delegations.rs @@ -66,7 +66,7 @@ async fn print_delegations(delegations: Vec, client: &SigningClientW for delegation in delegations { table.add_row(vec![ to_iso_timestamp(delegation.height as u32, client).await, - delegation.mix_id.to_string(), + delegation.node_id.to_string(), pretty_cosmwasm_coin(&delegation.amount), delegation .proxy @@ -93,7 +93,7 @@ async fn print_delegation_events(events: Vec, client: &Signin match event.event.kind { PendingEpochEventKind::Delegate { owner, - mix_id, + node_id: mix_id, amount, proxy, .. @@ -110,7 +110,7 @@ async fn print_delegation_events(events: Vec, client: &Signin } PendingEpochEventKind::Undelegate { owner, - mix_id, + node_id: mix_id, proxy, .. } => { diff --git a/common/commands/src/validator/mixnet/delegators/rewards/claim_delegator_reward.rs b/common/commands/src/validator/mixnet/delegators/rewards/claim_delegator_reward.rs index 327a3c0fd3..83426a316e 100644 --- a/common/commands/src/validator/mixnet/delegators/rewards/claim_delegator_reward.rs +++ b/common/commands/src/validator/mixnet/delegators/rewards/claim_delegator_reward.rs @@ -4,13 +4,13 @@ use crate::context::SigningClient; use clap::Parser; use log::info; -use nym_mixnet_contract_common::MixId; +use nym_mixnet_contract_common::NodeId; use nym_validator_client::nyxd::contract_traits::{MixnetQueryClient, MixnetSigningClient}; #[derive(Debug, Parser)] pub struct Args { #[clap(long)] - pub mix_id: Option, + pub mix_id: Option, #[clap(long)] pub identity_key: Option, diff --git a/common/commands/src/validator/mixnet/delegators/rewards/vesting_claim_delegator_reward.rs b/common/commands/src/validator/mixnet/delegators/rewards/vesting_claim_delegator_reward.rs index aa6b8dbe44..d36c4a7b17 100644 --- a/common/commands/src/validator/mixnet/delegators/rewards/vesting_claim_delegator_reward.rs +++ b/common/commands/src/validator/mixnet/delegators/rewards/vesting_claim_delegator_reward.rs @@ -4,13 +4,13 @@ use crate::context::SigningClient; use clap::Parser; use log::info; -use nym_mixnet_contract_common::MixId; +use nym_mixnet_contract_common::NodeId; use nym_validator_client::nyxd::contract_traits::{MixnetQueryClient, MixnetSigningClient}; #[derive(Debug, Parser)] pub struct Args { #[clap(long)] - pub mix_id: Option, + pub mix_id: Option, #[clap(long)] pub identity_key: Option, diff --git a/common/commands/src/validator/mixnet/delegators/undelegate_from_mixnode.rs b/common/commands/src/validator/mixnet/delegators/undelegate_from_mixnode.rs index cadb6c272f..19593ed5ce 100644 --- a/common/commands/src/validator/mixnet/delegators/undelegate_from_mixnode.rs +++ b/common/commands/src/validator/mixnet/delegators/undelegate_from_mixnode.rs @@ -4,13 +4,13 @@ use crate::context::SigningClient; use clap::Parser; use log::info; -use nym_mixnet_contract_common::MixId; +use nym_mixnet_contract_common::NodeId; use nym_validator_client::nyxd::contract_traits::{MixnetQueryClient, MixnetSigningClient}; #[derive(Debug, Parser)] pub struct Args { #[clap(long)] - pub mix_id: Option, + pub mix_id: Option, #[clap(long)] pub identity_key: Option, @@ -36,7 +36,7 @@ pub async fn undelegate_from_mixnode(args: Args, client: SigningClient) { }; let res = client - .undelegate_from_mixnode(mix_id, None) + .undelegate(mix_id, None) .await .expect("failed to remove stake from mixnode!"); diff --git a/common/commands/src/validator/mixnet/delegators/vesting_delegate_to_mixnode.rs b/common/commands/src/validator/mixnet/delegators/vesting_delegate_to_mixnode.rs index 45c4ffa5c3..3fa3fd7cee 100644 --- a/common/commands/src/validator/mixnet/delegators/vesting_delegate_to_mixnode.rs +++ b/common/commands/src/validator/mixnet/delegators/vesting_delegate_to_mixnode.rs @@ -4,7 +4,7 @@ use clap::Parser; use log::info; -use nym_mixnet_contract_common::{Coin, MixId}; +use nym_mixnet_contract_common::{Coin, NodeId}; use nym_validator_client::nyxd::contract_traits::MixnetQueryClient; use nym_validator_client::nyxd::contract_traits::VestingSigningClient; @@ -13,7 +13,7 @@ use crate::context::SigningClient; #[derive(Debug, Parser)] pub struct Args { #[clap(long)] - pub mix_id: Option, + pub mix_id: Option, #[clap(long)] pub identity_key: Option, diff --git a/common/commands/src/validator/mixnet/delegators/vesting_undelegate_from_mixnode.rs b/common/commands/src/validator/mixnet/delegators/vesting_undelegate_from_mixnode.rs index 13bae0b7a0..9daf8691d3 100644 --- a/common/commands/src/validator/mixnet/delegators/vesting_undelegate_from_mixnode.rs +++ b/common/commands/src/validator/mixnet/delegators/vesting_undelegate_from_mixnode.rs @@ -3,7 +3,7 @@ use clap::Parser; use log::info; -use nym_mixnet_contract_common::MixId; +use nym_mixnet_contract_common::NodeId; use nym_validator_client::nyxd::contract_traits::MixnetQueryClient; use nym_validator_client::nyxd::contract_traits::VestingSigningClient; @@ -12,7 +12,7 @@ use crate::context::SigningClient; #[derive(Debug, Parser)] pub struct Args { #[clap(long)] - pub mix_id: Option, + pub mix_id: Option, #[clap(long)] pub identity_key: Option, diff --git a/common/commands/src/validator/mixnet/operators/gateway/mod.rs b/common/commands/src/validator/mixnet/operators/gateway/mod.rs index 8c4b8753c3..dcc81efbcc 100644 --- a/common/commands/src/validator/mixnet/operators/gateway/mod.rs +++ b/common/commands/src/validator/mixnet/operators/gateway/mod.rs @@ -5,6 +5,7 @@ use clap::{Args, Subcommand}; pub mod bond_gateway; pub mod gateway_bonding_sign_payload; +pub mod nymnode_migration; pub mod settings; pub mod unbond_gateway; pub mod vesting_bond_gateway; @@ -31,4 +32,6 @@ pub enum MixnetOperatorsGatewayCommands { VestingUnbond(vesting_unbond_gateway::Args), /// Create base58-encoded payload required for producing valid bonding signature. CreateGatewayBondingSignPayload(gateway_bonding_sign_payload::Args), + /// Migrate the gateway into a Nym Node + MigrateToNymnode(nymnode_migration::Args), } diff --git a/common/commands/src/validator/mixnet/operators/gateway/nymnode_migration.rs b/common/commands/src/validator/mixnet/operators/gateway/nymnode_migration.rs new file mode 100644 index 0000000000..a6b22a2d4b --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/gateway/nymnode_migration.rs @@ -0,0 +1,56 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::context::SigningClient; +use clap::Parser; +use cosmwasm_std::Uint128; +use log::info; +use nym_contracts_common::Percent; +use nym_mixnet_contract_common::{ + NodeCostParams, DEFAULT_INTERVAL_OPERATING_COST_AMOUNT, DEFAULT_PROFIT_MARGIN_PERCENT, +}; +use nym_validator_client::nyxd::contract_traits::MixnetSigningClient; +use nym_validator_client::nyxd::CosmWasmCoin; + +#[derive(Debug, Parser)] +pub struct Args { + #[clap(long)] + pub profit_margin_percent: Option, + + #[clap( + long, + help = "operating cost in current DENOMINATION (so it would be 'unym', rather than 'nym')" + )] + pub interval_operating_cost: Option, +} + +pub async fn migrate_to_nymnode(args: Args, client: SigningClient) { + let denom = client.current_chain_details().mix_denom.base.as_str(); + + let cost_params = + if args.profit_margin_percent.is_some() || args.interval_operating_cost.is_some() { + Some(NodeCostParams { + profit_margin_percent: Percent::from_percentage_value( + args.profit_margin_percent + .unwrap_or(DEFAULT_PROFIT_MARGIN_PERCENT), + ) + .unwrap(), + interval_operating_cost: CosmWasmCoin { + denom: denom.into(), + amount: Uint128::new( + args.interval_operating_cost + .unwrap_or(DEFAULT_INTERVAL_OPERATING_COST_AMOUNT), + ), + }, + }) + } else { + None + }; + + let res = client + .migrate_legacy_gateway(cost_params, None) + .await + .expect("failed to migrate gateway!"); + + info!("migration result: {:?}", res) +} diff --git a/common/commands/src/validator/mixnet/operators/mixnode/bond_mixnode.rs b/common/commands/src/validator/mixnet/operators/mixnode/bond_mixnode.rs index 862a797566..410ce91d28 100644 --- a/common/commands/src/validator/mixnet/operators/mixnode/bond_mixnode.rs +++ b/common/commands/src/validator/mixnet/operators/mixnode/bond_mixnode.rs @@ -1,20 +1,21 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::context::SigningClient; use clap::Parser; use cosmwasm_std::Uint128; use log::{info, warn}; - use nym_contracts_common::signing::MessageSignature; -use nym_mixnet_contract_common::{Coin, MixNodeCostParams, Percent}; +use nym_mixnet_contract_common::{ + Coin, NodeCostParams, Percent, DEFAULT_INTERVAL_OPERATING_COST_AMOUNT, + DEFAULT_PROFIT_MARGIN_PERCENT, +}; use nym_network_defaults::{ DEFAULT_HTTP_API_LISTENING_PORT, DEFAULT_MIX_LISTENING_PORT, DEFAULT_VERLOC_LISTENING_PORT, }; use nym_validator_client::nyxd::contract_traits::MixnetSigningClient; use nym_validator_client::nyxd::CosmWasmCoin; -use crate::context::SigningClient; - #[derive(Debug, Parser)] pub struct Args { #[clap(long)] @@ -42,7 +43,7 @@ pub struct Args { pub version: String, #[clap(long)] - pub profit_margin_percent: Option, + pub profit_margin_percent: Option, #[clap( long, @@ -85,14 +86,18 @@ pub async fn bond_mixnode(args: Args, client: SigningClient) { let coin = Coin::new(args.amount, denom); - let cost_params = MixNodeCostParams { + let cost_params = NodeCostParams { profit_margin_percent: Percent::from_percentage_value( - args.profit_margin_percent.unwrap_or(10) as u64, + args.profit_margin_percent + .unwrap_or(DEFAULT_PROFIT_MARGIN_PERCENT), ) .unwrap(), interval_operating_cost: CosmWasmCoin { denom: denom.into(), - amount: Uint128::new(args.interval_operating_cost.unwrap_or(40_000_000)), + amount: Uint128::new( + args.interval_operating_cost + .unwrap_or(DEFAULT_INTERVAL_OPERATING_COST_AMOUNT), + ), }, }; diff --git a/common/commands/src/validator/mixnet/operators/mixnode/families/create_family.rs b/common/commands/src/validator/mixnet/operators/mixnode/families/create_family.rs deleted file mode 100644 index 6e2c664a35..0000000000 --- a/common/commands/src/validator/mixnet/operators/mixnode/families/create_family.rs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::context::SigningClient; -use clap::Parser; -use log::info; -use nym_validator_client::nyxd::contract_traits::MixnetSigningClient; - -#[derive(Debug, Parser)] -pub struct Args { - /// Label that is going to be used for creating the family - #[arg(long)] - pub family_label: String, -} - -pub async fn create_family(args: Args, client: SigningClient) { - info!("Create family"); - - let res = client - .create_family(args.family_label, None) - .await - .expect("failed to create family"); - - info!("Family creation result: {:?}", res); -} diff --git a/common/commands/src/validator/mixnet/operators/mixnode/families/create_family_join_permit_sign_payload.rs b/common/commands/src/validator/mixnet/operators/mixnode/families/create_family_join_permit_sign_payload.rs deleted file mode 100644 index ac397150ea..0000000000 --- a/common/commands/src/validator/mixnet/operators/mixnode/families/create_family_join_permit_sign_payload.rs +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright 2021 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::context::QueryClient; -use crate::utils::DataWrapper; -use clap::Parser; -use cosmrs::AccountId; -use log::info; -use nym_bin_common::output_format::OutputFormat; -use nym_crypto::asymmetric::identity; -use nym_mixnet_contract_common::construct_family_join_permit; -use nym_mixnet_contract_common::families::FamilyHead; -use nym_validator_client::nyxd::contract_traits::MixnetQueryClient; - -#[derive(Debug, Parser)] -pub struct Args { - /// Account address (i.e. owner of the family head) which will be used for issuing the permit - #[arg(long)] - pub address: AccountId, - - // might as well validate the value when parsing the arguments - /// Identity of the member for whom we're issuing the permit - #[arg(long)] - pub member: identity::PublicKey, - - #[clap(short, long, default_value_t = OutputFormat::default())] - output: OutputFormat, -} - -pub async fn create_family_join_permit_sign_payload(args: Args, client: QueryClient) { - info!("Create family join permit sign payload"); - - // get the address of our mixnode to recover the family head information - let Some(mixnode) = client - .get_owned_mixnode(&args.address) - .await - .unwrap() - .mixnode_details - else { - eprintln!("{} does not seem to even own a mixnode!", args.address); - return; - }; - - // make sure this mixnode is actually a family head - if client - .get_node_family_by_head(mixnode.bond_information.identity().to_string()) - .await - .unwrap() - .family - .is_none() - { - eprintln!("{} does not even seem to own a family!", args.address); - return; - } - - let nonce = match client.get_signing_nonce(&args.address).await { - Ok(nonce) => nonce, - Err(err) => { - eprint!( - "failed to query for the signing nonce of {}: {err}", - args.address - ); - return; - } - }; - - let head = FamilyHead::new(mixnode.bond_information.identity()); - - let payload = construct_family_join_permit(nonce, head, args.member.to_base58_string()); - let wrapper = DataWrapper::new(payload.to_base58_string().unwrap()); - println!("{}", args.output.format(&wrapper)) -} diff --git a/common/commands/src/validator/mixnet/operators/mixnode/families/join_family.rs b/common/commands/src/validator/mixnet/operators/mixnode/families/join_family.rs deleted file mode 100644 index 08b4c471c0..0000000000 --- a/common/commands/src/validator/mixnet/operators/mixnode/families/join_family.rs +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::context::SigningClient; -use clap::Parser; -use log::info; -use nym_contracts_common::signing::MessageSignature; -use nym_crypto::asymmetric::identity; -use nym_mixnet_contract_common::families::FamilyHead; -use nym_validator_client::nyxd::contract_traits::MixnetSigningClient; - -#[derive(Debug, Parser)] -pub struct Args { - /// The head of the family that we intend to join - #[arg(long)] - pub family_head: identity::PublicKey, - - /// Permission, as provided by the family head, for joining the family - #[arg(long)] - pub join_permit: MessageSignature, -} - -pub async fn join_family(args: Args, client: SigningClient) { - info!("Join family"); - - let family_head = FamilyHead::new(args.family_head.to_base58_string()); - - let res = client - .join_family(args.join_permit, family_head, None) - .await - .expect("failed to join family"); - - info!("Family join result: {:?}", res); -} diff --git a/common/commands/src/validator/mixnet/operators/mixnode/families/kick_family_member.rs b/common/commands/src/validator/mixnet/operators/mixnode/families/kick_family_member.rs deleted file mode 100644 index 27b5d71d7d..0000000000 --- a/common/commands/src/validator/mixnet/operators/mixnode/families/kick_family_member.rs +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::context::SigningClient; -use clap::Parser; -use log::info; -use nym_crypto::asymmetric::identity; -use nym_validator_client::nyxd::contract_traits::MixnetSigningClient; -use nym_validator_client::nyxd::contract_traits::VestingSigningClient; - -#[derive(Debug, Parser)] -pub struct Args { - /// The member of the family that we intend to kick - #[arg(long)] - pub member: identity::PublicKey, - - /// Indicates whether the family was created (and managed) via the vesting contract - #[arg(long)] - pub with_vesting_account: bool, -} - -pub async fn kick_family_member(args: Args, client: SigningClient) { - info!("Leave family"); - - let member = args.member.to_base58_string(); - - let res = if args.with_vesting_account { - client - .vesting_kick_family_member(member, None) - .await - .expect("failed to kick family member with vesting account") - } else { - client - .kick_family_member(member, None) - .await - .expect("failed to kick family member") - }; - - info!("Family leave result: {:?}", res); -} diff --git a/common/commands/src/validator/mixnet/operators/mixnode/families/leave_family.rs b/common/commands/src/validator/mixnet/operators/mixnode/families/leave_family.rs deleted file mode 100644 index d9c31e3933..0000000000 --- a/common/commands/src/validator/mixnet/operators/mixnode/families/leave_family.rs +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::context::SigningClient; -use clap::Parser; -use log::info; -use nym_crypto::asymmetric::identity; -use nym_mixnet_contract_common::families::FamilyHead; -use nym_validator_client::nyxd::contract_traits::MixnetSigningClient; - -#[derive(Debug, Parser)] -pub struct Args { - /// The head of the family that we intend to leave - #[arg(long)] - pub family_head: identity::PublicKey, -} - -pub async fn leave_family(args: Args, client: SigningClient) { - info!("Leave family"); - - let family_head = FamilyHead::new(args.family_head.to_base58_string()); - - let res = client - .leave_family(family_head, None) - .await - .expect("failed to leave family"); - - info!("Family leave result: {:?}", res); -} diff --git a/common/commands/src/validator/mixnet/operators/mixnode/families/mod.rs b/common/commands/src/validator/mixnet/operators/mixnode/families/mod.rs deleted file mode 100644 index c343ef977f..0000000000 --- a/common/commands/src/validator/mixnet/operators/mixnode/families/mod.rs +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use clap::{Args, Subcommand}; - -pub mod create_family; -pub mod create_family_join_permit_sign_payload; -pub mod join_family; -pub mod kick_family_member; -pub mod leave_family; - -#[derive(Debug, Args)] -#[clap(args_conflicts_with_subcommands = true, subcommand_required = true)] -pub struct MixnetOperatorsMixnodeFamilies { - #[clap(subcommand)] - pub command: MixnetOperatorsMixnodeFamiliesCommands, -} - -#[derive(Debug, Subcommand)] -pub enum MixnetOperatorsMixnodeFamiliesCommands { - /// Create family - CreateFamily(create_family::Args), - - /// Join family - JoinFamily(join_family::Args), - - /// Leave family, - LeaveFamily(leave_family::Args), - - /// Kick family member - KickFamilyMember(kick_family_member::Args), - - /// Create a message payload that is required to get signed in order to obtain a permit for joining family - CreateFamilyJoinPermitSignPayload(create_family_join_permit_sign_payload::Args), -} diff --git a/common/commands/src/validator/mixnet/operators/mixnode/mixnode_bonding_sign_payload.rs b/common/commands/src/validator/mixnet/operators/mixnode/mixnode_bonding_sign_payload.rs index 3eda67a2e9..7525657547 100644 --- a/common/commands/src/validator/mixnet/operators/mixnode/mixnode_bonding_sign_payload.rs +++ b/common/commands/src/validator/mixnet/operators/mixnode/mixnode_bonding_sign_payload.rs @@ -8,7 +8,8 @@ use cosmwasm_std::{Coin, Uint128}; use nym_bin_common::output_format::OutputFormat; use nym_contracts_common::Percent; use nym_mixnet_contract_common::{ - construct_legacy_mixnode_bonding_sign_payload, MixNodeCostParams, + construct_legacy_mixnode_bonding_sign_payload, NodeCostParams, + DEFAULT_INTERVAL_OPERATING_COST_AMOUNT, DEFAULT_PROFIT_MARGIN_PERCENT, }; use nym_network_defaults::{ DEFAULT_HTTP_API_LISTENING_PORT, DEFAULT_MIX_LISTENING_PORT, DEFAULT_VERLOC_LISTENING_PORT, @@ -40,7 +41,7 @@ pub struct Args { pub version: String, #[clap(long)] - pub profit_margin_percent: Option, + pub profit_margin_percent: Option, #[clap( long, @@ -75,14 +76,18 @@ pub async fn create_payload(args: Args, client: SigningClient) { let coin = Coin::new(args.amount, denom); - let cost_params = MixNodeCostParams { + let cost_params = NodeCostParams { profit_margin_percent: Percent::from_percentage_value( - args.profit_margin_percent.unwrap_or(10) as u64, + args.profit_margin_percent + .unwrap_or(DEFAULT_PROFIT_MARGIN_PERCENT), ) .unwrap(), interval_operating_cost: CosmWasmCoin { denom: denom.into(), - amount: Uint128::new(args.interval_operating_cost.unwrap_or(40_000_000)), + amount: Uint128::new( + args.interval_operating_cost + .unwrap_or(DEFAULT_INTERVAL_OPERATING_COST_AMOUNT), + ), }, }; diff --git a/common/commands/src/validator/mixnet/operators/mixnode/mod.rs b/common/commands/src/validator/mixnet/operators/mixnode/mod.rs index abb5060e9b..3244a189bc 100644 --- a/common/commands/src/validator/mixnet/operators/mixnode/mod.rs +++ b/common/commands/src/validator/mixnet/operators/mixnode/mod.rs @@ -5,10 +5,10 @@ use clap::{Args, Subcommand}; pub mod bond_mixnode; pub mod decrease_pledge; -pub mod families; pub mod keys; pub mod migrate_vested_mixnode; pub mod mixnode_bonding_sign_payload; +pub mod nymnode_migration; pub mod pledge_more; pub mod rewards; pub mod settings; @@ -33,8 +33,6 @@ pub enum MixnetOperatorsMixnodeCommands { Rewards(rewards::MixnetOperatorsMixnodeRewards), /// Manage your mixnode settings stored in the directory Settings(settings::MixnetOperatorsMixnodeSettings), - /// Operations for mixnode families - Families(families::MixnetOperatorsMixnodeFamilies), /// Bond to a mixnode Bond(bond_mixnode::Args), /// Unbond from a mixnode @@ -55,4 +53,6 @@ pub enum MixnetOperatorsMixnodeCommands { DecreasePledgeVesting(vesting_decrease_pledge::Args), /// Migrate the mixnode to use liquid tokens MigrateVestedNode(migrate_vested_mixnode::Args), + /// Migrate the mixnode into a Nym Node + MigrateToNymnode(nymnode_migration::Args), } diff --git a/common/commands/src/validator/mixnet/operators/mixnode/nymnode_migration.rs b/common/commands/src/validator/mixnet/operators/mixnode/nymnode_migration.rs new file mode 100644 index 0000000000..fe46e2c11a --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/mixnode/nymnode_migration.rs @@ -0,0 +1,19 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::context::SigningClient; +use clap::Parser; +use log::info; +use nym_validator_client::nyxd::contract_traits::MixnetSigningClient; + +#[derive(Debug, Parser)] +pub struct Args {} + +pub async fn migrate_to_nymnode(_args: Args, client: SigningClient) { + let res = client + .migrate_legacy_mixnode(None) + .await + .expect("failed to migrate mixnode!"); + + info!("migration result: {:?}", res) +} diff --git a/common/commands/src/validator/mixnet/operators/mixnode/settings/update_cost_params.rs b/common/commands/src/validator/mixnet/operators/mixnode/settings/update_cost_params.rs index 7c09ba2946..c246bc8fba 100644 --- a/common/commands/src/validator/mixnet/operators/mixnode/settings/update_cost_params.rs +++ b/common/commands/src/validator/mixnet/operators/mixnode/settings/update_cost_params.rs @@ -2,10 +2,13 @@ // SPDX-License-Identifier: Apache-2.0 use crate::context::SigningClient; +use anyhow::{anyhow, bail}; use clap::Parser; use cosmwasm_std::Uint128; use log::info; -use nym_mixnet_contract_common::{MixNodeCostParams, Percent}; +use nym_mixnet_contract_common::{ + NodeCostParams, Percent, DEFAULT_INTERVAL_OPERATING_COST_AMOUNT, DEFAULT_PROFIT_MARGIN_PERCENT, +}; use nym_validator_client::nyxd::contract_traits::{MixnetQueryClient, MixnetSigningClient}; use nym_validator_client::nyxd::CosmWasmCoin; @@ -24,62 +27,45 @@ pub struct Args { pub interval_operating_cost: Option, } -pub async fn update_cost_params(args: Args, client: SigningClient) { +pub async fn update_cost_params(args: Args, client: SigningClient) -> anyhow::Result<()> { let denom = client.current_chain_details().mix_denom.base.as_str(); - fn convert_to_percent(value: u64) -> Percent { - Percent::from_percentage_value(value).expect("Invalid value") - } + let default_profit_margin = + Percent::from_percentage_value(DEFAULT_PROFIT_MARGIN_PERCENT).unwrap(); - let default_profit_margin: Percent = convert_to_percent(20); + let mix_details = client + .get_owned_mixnode(&client.address()) + .await? + .mixnode_details + .ok_or_else(|| anyhow!("the client does not own any mixnodes"))?; + let current_parameters = mix_details.rewarding_details.cost_params; - let mixownership_response = match client.get_owned_mixnode(&client.address()).await { - Ok(response) => response, - Err(_) => { - eprintln!("Failed to obtain owned mixnode"); - return; - } - }; - - let mix_id = match mixownership_response.mixnode_details { - Some(details) => details.bond_information.mix_id, - None => { - eprintln!("Failed to obtain mixnode details"); - return; - } - }; - - let rewarding_response = match client.get_mixnode_rewarding_details(mix_id).await { - Ok(details) => details, - Err(_) => { - eprintln!("Failed to obtain rewarding details"); - return; - } - }; - - let profit_margin_percent = rewarding_response - .rewarding_details + let profit_margin_percent = current_parameters .map(|rd| rd.cost_params.profit_margin_percent) .unwrap_or(default_profit_margin); let profit_margin_value = args .profit_margin_percent - .map(|pm| convert_to_percent(pm as u64)) - .unwrap_or(profit_margin_percent); + .map(|pm| Percent::from_percentage_value(pm as u64)) + .unwrap_or(profit_margin_percent)?; - let cost_params = MixNodeCostParams { + let cost_params = NodeCostParams { profit_margin_percent: profit_margin_value, interval_operating_cost: CosmWasmCoin { denom: denom.into(), - amount: Uint128::new(args.interval_operating_cost.unwrap_or(40_000_000)), + amount: Uint128::new( + args.interval_operating_cost + .unwrap_or(DEFAULT_INTERVAL_OPERATING_COST_AMOUNT), + ), }, }; info!("Starting mixnode params updating!"); let res = client - .update_mixnode_cost_params(cost_params, None) + .update_cost_params(cost_params, None) .await .expect("failed to update cost params"); - info!("Cost params result: {:?}", res) + info!("Cost params result: {:?}", res); + Ok(()) } diff --git a/common/commands/src/validator/mixnet/operators/mixnode/vesting_bond_mixnode.rs b/common/commands/src/validator/mixnet/operators/mixnode/vesting_bond_mixnode.rs index 415f9fe2b3..d55d6463e7 100644 --- a/common/commands/src/validator/mixnet/operators/mixnode/vesting_bond_mixnode.rs +++ b/common/commands/src/validator/mixnet/operators/mixnode/vesting_bond_mixnode.rs @@ -6,7 +6,9 @@ use clap::Parser; use cosmwasm_std::Uint128; use log::{info, warn}; use nym_contracts_common::signing::MessageSignature; -use nym_mixnet_contract_common::{Coin, MixNodeCostParams}; +use nym_mixnet_contract_common::{ + Coin, NodeCostParams, DEFAULT_INTERVAL_OPERATING_COST_AMOUNT, DEFAULT_PROFIT_MARGIN_PERCENT, +}; use nym_mixnet_contract_common::{MixNode, Percent}; use nym_network_defaults::{ DEFAULT_HTTP_API_LISTENING_PORT, DEFAULT_MIX_LISTENING_PORT, DEFAULT_VERLOC_LISTENING_PORT, @@ -40,7 +42,7 @@ pub struct Args { pub version: String, #[clap(long)] - pub profit_margin_percent: Option, + pub profit_margin_percent: Option, #[clap( long, @@ -84,14 +86,18 @@ pub async fn vesting_bond_mixnode(client: SigningClient, args: Args, denom: &str let coin = Coin::new(args.amount, denom); - let cost_params = MixNodeCostParams { + let cost_params = NodeCostParams { profit_margin_percent: Percent::from_percentage_value( - args.profit_margin_percent.unwrap_or(10) as u64, + args.profit_margin_percent + .unwrap_or(DEFAULT_PROFIT_MARGIN_PERCENT), ) .unwrap(), interval_operating_cost: CosmWasmCoin { denom: denom.into(), - amount: Uint128::new(args.interval_operating_cost.unwrap_or(40_000_000)), + amount: Uint128::new( + args.interval_operating_cost + .unwrap_or(DEFAULT_INTERVAL_OPERATING_COST_AMOUNT), + ), }, }; diff --git a/common/commands/src/validator/mixnet/operators/mod.rs b/common/commands/src/validator/mixnet/operators/mod.rs index cde9dc3754..cf6e2d9d5f 100644 --- a/common/commands/src/validator/mixnet/operators/mod.rs +++ b/common/commands/src/validator/mixnet/operators/mod.rs @@ -6,6 +6,7 @@ use clap::{Args, Subcommand}; pub mod gateway; pub mod identity_key; pub mod mixnode; +pub mod nymnode; #[derive(Debug, Args)] #[clap(args_conflicts_with_subcommands = true, subcommand_required = true)] @@ -17,9 +18,11 @@ pub struct MixnetOperators { #[allow(clippy::large_enum_variant)] #[derive(Debug, Subcommand)] pub enum MixnetOperatorsCommands { - /// Manage your mixnode + /// Manage your Nym Node + Nymnode(nymnode::MixnetOperatorsNymNode), + /// Manage your legacy mixnode Mixnode(mixnode::MixnetOperatorsMixnode), - /// Manage your gateway + /// Manage your legacy gateway Gateway(gateway::MixnetOperatorsGateway), /// Sign messages using your private identity key IdentityKey(identity_key::MixnetOperatorsIdentityKey), diff --git a/common/commands/src/validator/mixnet/operators/nymnode/bond_nymnode.rs b/common/commands/src/validator/mixnet/operators/nymnode/bond_nymnode.rs new file mode 100644 index 0000000000..acde3d39e3 --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/nymnode/bond_nymnode.rs @@ -0,0 +1,89 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::context::SigningClient; +use clap::Parser; +use cosmwasm_std::Uint128; +use log::{info, warn}; +use nym_contracts_common::signing::MessageSignature; +use nym_mixnet_contract_common::{ + Coin, NodeCostParams, Percent, DEFAULT_INTERVAL_OPERATING_COST_AMOUNT, + DEFAULT_PROFIT_MARGIN_PERCENT, +}; +use nym_validator_client::nyxd::contract_traits::MixnetSigningClient; +use nym_validator_client::nyxd::CosmWasmCoin; + +#[derive(Debug, Parser)] +pub struct Args { + #[clap(long)] + pub host: String, + + #[clap(long)] + pub signature: MessageSignature, + + #[clap(long)] + pub http_api_port: Option, + + #[clap(long)] + pub identity_key: String, + + #[clap(long)] + pub profit_margin_percent: Option, + + #[clap( + long, + help = "operating cost in current DENOMINATION (so it would be 'unym', rather than 'nym')" + )] + pub interval_operating_cost: Option, + + #[clap( + long, + help = "bonding amount in current DENOMINATION (so it would be 'unym', rather than 'nym')" + )] + pub amount: u128, + + #[clap(short, long)] + pub force: bool, +} + +pub async fn bond_nymnode(args: Args, client: SigningClient) { + let denom = client.current_chain_details().mix_denom.base.as_str(); + + info!("Starting nym node bonding!"); + + // if we're trying to bond less than 1 token + if args.amount < 1_000_000 && !args.force { + warn!("You're trying to bond only {}{} which is less than 1 full token. Are you sure that's what you want? If so, run with `--force` or `-f` flag", args.amount, denom); + return; + } + + let nymnode = nym_mixnet_contract_common::NymNode { + host: args.host, + custom_http_port: args.http_api_port, + identity_key: args.identity_key, + }; + + let coin = Coin::new(args.amount, denom); + + let cost_params = NodeCostParams { + profit_margin_percent: Percent::from_percentage_value( + args.profit_margin_percent + .unwrap_or(DEFAULT_PROFIT_MARGIN_PERCENT), + ) + .unwrap(), + interval_operating_cost: CosmWasmCoin { + denom: denom.into(), + amount: Uint128::new( + args.interval_operating_cost + .unwrap_or(DEFAULT_INTERVAL_OPERATING_COST_AMOUNT), + ), + }, + }; + + let res = client + .bond_nymnode(nymnode, cost_params, args.signature, coin.into(), None) + .await + .expect("failed to bond nymnode!"); + + info!("Bonding result: {:?}", res) +} diff --git a/common/commands/src/validator/mixnet/operators/nymnode/keys/decode_node_key.rs b/common/commands/src/validator/mixnet/operators/nymnode/keys/decode_node_key.rs new file mode 100644 index 0000000000..04b6d7c9ea --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/nymnode/keys/decode_node_key.rs @@ -0,0 +1,20 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use base64::Engine; +use clap::Parser; + +#[derive(Debug, Parser)] +pub struct Args { + #[clap(short, long)] + pub key: String, +} + +pub fn decode_node_key(args: Args) { + let b64_decoded = base64::prelude::BASE64_STANDARD + .decode(args.key) + .expect("failed to decode base64 string"); + let b58_encoded = bs58::encode(&b64_decoded).into_string(); + + println!("{b58_encoded}") +} diff --git a/common/commands/src/validator/mixnet/operators/nymnode/keys/mod.rs b/common/commands/src/validator/mixnet/operators/nymnode/keys/mod.rs new file mode 100644 index 0000000000..1f0b5d4c21 --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/nymnode/keys/mod.rs @@ -0,0 +1,19 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use clap::{Args, Subcommand}; + +pub mod decode_node_key; + +#[derive(Debug, Args)] +#[clap(args_conflicts_with_subcommands = true, subcommand_required = true)] +pub struct MixnetOperatorsNymNodeKeys { + #[clap(subcommand)] + pub command: MixnetOperatorsNymNodeKeysCommands, +} + +#[derive(Debug, Subcommand)] +pub enum MixnetOperatorsNymNodeKeysCommands { + /// Decode a Nym Node key + DecodeNodeKey(decode_node_key::Args), +} diff --git a/common/commands/src/validator/mixnet/operators/nymnode/mod.rs b/common/commands/src/validator/mixnet/operators/nymnode/mod.rs new file mode 100644 index 0000000000..e2cd7890a7 --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/nymnode/mod.rs @@ -0,0 +1,43 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use clap::{Args, Subcommand}; + +pub mod bond_nymnode; +pub mod keys; +pub mod nymnode_bonding_sign_payload; +pub mod pledge; +pub mod rewards; +pub mod settings; +pub mod unbond_nymnode; + +#[derive(Debug, Args)] +#[clap(args_conflicts_with_subcommands = true, subcommand_required = true)] +pub struct MixnetOperatorsNymNode { + #[clap(subcommand)] + pub command: MixnetOperatorsNymNodeCommands, +} + +#[derive(Debug, Subcommand)] +pub enum MixnetOperatorsNymNodeCommands { + /// Operations for Nym Node keys + Keys(keys::MixnetOperatorsNymNodeKeys), + + /// Manage your Nym Node operator rewards + Rewards(rewards::MixnetOperatorsNymNodeRewards), + + /// Manage your Nym Node settings stored in the directory + Settings(settings::MixnetOperatorsNymNodeSettings), + + /// Manage your Nym Node pledge + Pledge(pledge::MixnetOperatorsNymNodePledge), + + /// Bond to a Nym Node + Bond(bond_nymnode::Args), + + /// Unbond from a Nym Node + Unbond(unbond_nymnode::Args), + + /// Create base58-encoded payload required for producing valid bonding signature. + CreateNodeBondingSignPayload(nymnode_bonding_sign_payload::Args), +} diff --git a/common/commands/src/validator/mixnet/operators/nymnode/nymnode_bonding_sign_payload.rs b/common/commands/src/validator/mixnet/operators/nymnode/nymnode_bonding_sign_payload.rs new file mode 100644 index 0000000000..e3b66e65be --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/nymnode/nymnode_bonding_sign_payload.rs @@ -0,0 +1,90 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::context::SigningClient; +use crate::utils::{account_id_to_cw_addr, DataWrapper}; +use clap::Parser; +use cosmwasm_std::{Coin, Uint128}; +use nym_bin_common::output_format::OutputFormat; +use nym_contracts_common::Percent; +use nym_mixnet_contract_common::{ + construct_nym_node_bonding_sign_payload, NodeCostParams, + DEFAULT_INTERVAL_OPERATING_COST_AMOUNT, DEFAULT_PROFIT_MARGIN_PERCENT, +}; +use nym_validator_client::nyxd::contract_traits::MixnetQueryClient; +use nym_validator_client::nyxd::CosmWasmCoin; + +#[derive(Debug, Parser)] +pub struct Args { + #[clap(long)] + pub host: String, + + #[clap(long)] + pub identity_key: String, + + #[clap(long)] + pub custom_http_api_port: Option, + + #[clap(long)] + pub profit_margin_percent: Option, + + #[clap( + long, + help = "operating cost in current DENOMINATION (so it would be 'unym', rather than 'nym')" + )] + pub interval_operating_cost: Option, + + #[clap( + long, + help = "bonding amount in current DENOMINATION (so it would be 'unym', rather than 'nym')" + )] + pub amount: u128, + + #[clap(short, long, default_value_t = OutputFormat::default())] + pub output: OutputFormat, +} + +pub async fn create_payload(args: Args, client: SigningClient) { + let denom = client.current_chain_details().mix_denom.base.as_str(); + + let mixnode = nym_mixnet_contract_common::NymNode { + host: args.host, + custom_http_port: args.custom_http_api_port, + identity_key: args.identity_key, + }; + + let coin = Coin::new(args.amount, denom); + + let cost_params = NodeCostParams { + profit_margin_percent: Percent::from_percentage_value( + args.profit_margin_percent + .unwrap_or(DEFAULT_PROFIT_MARGIN_PERCENT), + ) + .unwrap(), + interval_operating_cost: CosmWasmCoin { + denom: denom.into(), + amount: Uint128::new( + args.interval_operating_cost + .unwrap_or(DEFAULT_INTERVAL_OPERATING_COST_AMOUNT), + ), + }, + }; + + let nonce = match client.get_signing_nonce(&client.address()).await { + Ok(nonce) => nonce, + Err(err) => { + eprint!( + "failed to query for the signing nonce of {}: {err}", + client.address() + ); + return; + } + }; + + let address = account_id_to_cw_addr(&client.address()); + + let payload = + construct_nym_node_bonding_sign_payload(nonce, address, coin, mixnode, cost_params); + let wrapper = DataWrapper::new(payload.to_base58_string().unwrap()); + println!("{}", args.output.format(&wrapper)) +} diff --git a/common/commands/src/validator/mixnet/operators/nymnode/pledge/decrease_pledge.rs b/common/commands/src/validator/mixnet/operators/nymnode/pledge/decrease_pledge.rs new file mode 100644 index 0000000000..a299ddbe65 --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/nymnode/pledge/decrease_pledge.rs @@ -0,0 +1,29 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::context::SigningClient; +use clap::Parser; +use log::info; +use nym_mixnet_contract_common::Coin; +use nym_validator_client::nyxd::contract_traits::MixnetSigningClient; + +#[derive(Debug, Parser)] +pub struct Args { + #[clap(long)] + pub decrease_by: u128, +} + +pub async fn decrease_pledge(args: Args, client: SigningClient) { + let denom = client.current_chain_details().mix_denom.base.as_str(); + + info!("Starting to decrease pledge"); + + let coin = Coin::new(args.decrease_by, denom); + + let res = client + .pledge_more(coin.into(), None) + .await + .expect("failed to decrease pledge!"); + + info!("decreasing pledge: {:?}", res); +} diff --git a/common/commands/src/validator/mixnet/operators/nymnode/pledge/increase_pledge.rs b/common/commands/src/validator/mixnet/operators/nymnode/pledge/increase_pledge.rs new file mode 100644 index 0000000000..09b94bc5d5 --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/nymnode/pledge/increase_pledge.rs @@ -0,0 +1,29 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::context::SigningClient; +use clap::Parser; +use log::info; +use nym_mixnet_contract_common::Coin; +use nym_validator_client::nyxd::contract_traits::MixnetSigningClient; + +#[derive(Debug, Parser)] +pub struct Args { + #[clap(long)] + pub amount: u128, +} + +pub async fn increase_pledge(args: Args, client: SigningClient) { + let denom = client.current_chain_details().mix_denom.base.as_str(); + + info!("Starting to pledge more"); + + let coin = Coin::new(args.amount, denom); + + let res = client + .pledge_more(coin.into(), None) + .await + .expect("failed to pledge more!"); + + info!("pledging more: {:?}", res); +} diff --git a/common/commands/src/validator/mixnet/operators/nymnode/pledge/mod.rs b/common/commands/src/validator/mixnet/operators/nymnode/pledge/mod.rs new file mode 100644 index 0000000000..188e024857 --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/nymnode/pledge/mod.rs @@ -0,0 +1,22 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use clap::{Args, Subcommand}; + +pub mod decrease_pledge; +pub mod increase_pledge; + +#[derive(Debug, Args)] +#[clap(args_conflicts_with_subcommands = true, subcommand_required = true)] +pub struct MixnetOperatorsNymNodePledge { + #[clap(subcommand)] + pub command: MixnetOperatorsNymNodePledgeCommands, +} + +#[derive(Debug, Subcommand)] +pub enum MixnetOperatorsNymNodePledgeCommands { + /// Increase current pledge + Increase(increase_pledge::Args), + /// decrease current pledge + Decrease(decrease_pledge::Args), +} diff --git a/common/commands/src/validator/mixnet/operators/nymnode/rewards/claim_operator_reward.rs b/common/commands/src/validator/mixnet/operators/nymnode/rewards/claim_operator_reward.rs new file mode 100644 index 0000000000..a8f157f661 --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/nymnode/rewards/claim_operator_reward.rs @@ -0,0 +1,21 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::context::SigningClient; +use clap::Parser; +use log::info; +use nym_validator_client::nyxd::contract_traits::MixnetSigningClient; + +#[derive(Debug, Parser)] +pub struct Args {} + +pub async fn claim_operator_reward(_args: Args, client: SigningClient) { + info!("Claim operator reward"); + + let res = client + .withdraw_operator_reward(None) + .await + .expect("failed to claim operator reward"); + + info!("Claiming operator reward: {:?}", res) +} diff --git a/common/commands/src/validator/mixnet/operators/nymnode/rewards/mod.rs b/common/commands/src/validator/mixnet/operators/nymnode/rewards/mod.rs new file mode 100644 index 0000000000..166124751c --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/nymnode/rewards/mod.rs @@ -0,0 +1,19 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use clap::{Args, Subcommand}; + +pub mod claim_operator_reward; + +#[derive(Debug, Args)] +#[clap(args_conflicts_with_subcommands = true, subcommand_required = true)] +pub struct MixnetOperatorsNymNodeRewards { + #[clap(subcommand)] + pub command: MixnetOperatorsNymNodeRewardsCommands, +} + +#[derive(Debug, Subcommand)] +pub enum MixnetOperatorsNymNodeRewardsCommands { + /// Claim rewards + Claim(claim_operator_reward::Args), +} diff --git a/common/commands/src/validator/mixnet/operators/nymnode/settings/mod.rs b/common/commands/src/validator/mixnet/operators/nymnode/settings/mod.rs new file mode 100644 index 0000000000..8c5661188c --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/nymnode/settings/mod.rs @@ -0,0 +1,22 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use clap::{Args, Subcommand}; + +pub mod update_config; +pub mod update_cost_params; + +#[derive(Debug, Args)] +#[clap(args_conflicts_with_subcommands = true, subcommand_required = true)] +pub struct MixnetOperatorsNymNodeSettings { + #[clap(subcommand)] + pub command: MixnetOperatorsNymNodeSettingsCommands, +} + +#[derive(Debug, Subcommand)] +pub enum MixnetOperatorsNymNodeSettingsCommands { + /// Update Nym Node configuration + UpdateConfig(update_config::Args), + /// Update Nym Node cost parameters + UpdateCostParameters(update_cost_params::Args), +} diff --git a/common/commands/src/validator/mixnet/operators/nymnode/settings/update_config.rs b/common/commands/src/validator/mixnet/operators/nymnode/settings/update_config.rs new file mode 100644 index 0000000000..5ba5bf52fa --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/nymnode/settings/update_config.rs @@ -0,0 +1,50 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::context::SigningClient; +use clap::Parser; +use log::info; +use nym_mixnet_contract_common::nym_node::NodeConfigUpdate; +use nym_validator_client::nyxd::contract_traits::{MixnetQueryClient, MixnetSigningClient}; + +#[derive(Debug, Parser)] +pub struct Args { + #[clap(long)] + pub host: Option, + + // ideally this would have been `Option>`, but not sure if clap would have recognised it + #[clap(long)] + pub custom_http_port: Option, + + // equivalent to setting `custom_http_port` to `None` + #[clap(long)] + pub restore_default_http_port: bool, +} + +pub async fn update_config(args: Args, client: SigningClient) { + info!("Update nym node config!"); + + if client + .get_owned_nymnode(&client.address()) + .await + .expect("failed to query the chain for nym node details") + .details + .is_none() + { + log::warn!("this operator does not own a nym node to update"); + return; + } + + let update = NodeConfigUpdate { + host: args.host, + custom_http_port: args.custom_http_port, + restore_default_http_port: args.restore_default_http_port, + }; + + let res = client + .update_nymnode_config(update, None) + .await + .expect("updating nym node config"); + + info!("nym node config updated: {:?}", res) +} diff --git a/common/commands/src/validator/mixnet/operators/nymnode/settings/update_cost_params.rs b/common/commands/src/validator/mixnet/operators/nymnode/settings/update_cost_params.rs new file mode 100644 index 0000000000..b7168f243a --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/nymnode/settings/update_cost_params.rs @@ -0,0 +1,71 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::context::SigningClient; +use anyhow::anyhow; +use clap::Parser; +use cosmwasm_std::Uint128; +use log::info; +use nym_mixnet_contract_common::{ + NodeCostParams, Percent, DEFAULT_INTERVAL_OPERATING_COST_AMOUNT, DEFAULT_PROFIT_MARGIN_PERCENT, +}; +use nym_validator_client::nyxd::contract_traits::{MixnetQueryClient, MixnetSigningClient}; +use nym_validator_client::nyxd::CosmWasmCoin; + +#[derive(Debug, Parser)] +pub struct Args { + #[clap( + long, + help = "input your profit margin as follows; (so it would be 20, rather than 0.2)" + )] + pub profit_margin_percent: Option, + + #[clap( + long, + help = "operating cost in current DENOMINATION (so it would be 'unym', rather than 'nym')" + )] + pub interval_operating_cost: Option, +} + +pub async fn update_cost_params(args: Args, client: SigningClient) -> anyhow::Result<()> { + let denom = client.current_chain_details().mix_denom.base.as_str(); + + let default_profit_margin = + Percent::from_percentage_value(DEFAULT_PROFIT_MARGIN_PERCENT).unwrap(); + + let node_details = client + .get_owned_nymnode(&client.address()) + .await? + .details + .ok_or_else(|| anyhow!("the client does not own any nodes"))?; + let current_parameters = node_details.rewarding_details.cost_params; + + let profit_margin_percent = current_parameters + .map(|rd| rd.cost_params.profit_margin_percent) + .unwrap_or(default_profit_margin); + + let profit_margin_value = args + .profit_margin_percent + .map(|pm| Percent::from_percentage_value(pm as u64)) + .unwrap_or(profit_margin_percent)?; + + let cost_params = NodeCostParams { + profit_margin_percent: profit_margin_value, + interval_operating_cost: CosmWasmCoin { + denom: denom.into(), + amount: Uint128::new( + args.interval_operating_cost + .unwrap_or(DEFAULT_INTERVAL_OPERATING_COST_AMOUNT), + ), + }, + }; + + info!("Starting nym node params updating!"); + let res = client + .update_cost_params(cost_params, None) + .await + .expect("failed to update cost params"); + + info!("Cost params result: {:?}", res); + Ok(()) +} diff --git a/common/commands/src/validator/mixnet/operators/nymnode/unbond_nymnode.rs b/common/commands/src/validator/mixnet/operators/nymnode/unbond_nymnode.rs new file mode 100644 index 0000000000..fbcef6bfd7 --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/nymnode/unbond_nymnode.rs @@ -0,0 +1,22 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use clap::Parser; +use log::info; +use nym_validator_client::nyxd::contract_traits::MixnetSigningClient; + +use crate::context::SigningClient; + +#[derive(Debug, Parser)] +pub struct Args {} + +pub async fn unbond_nymnode(_args: Args, client: SigningClient) { + info!("Starting Nym Node unbonding!"); + + let res = client + .unbond_nymnode(None) + .await + .expect("failed to unbond Nym Node!"); + + info!("Unbonding result: {:?}", res) +} diff --git a/common/commands/src/validator/mixnet/query/query_all_gateways.rs b/common/commands/src/validator/mixnet/query/query_all_gateways.rs index 16f0e44285..cfe66ffd6c 100644 --- a/common/commands/src/validator/mixnet/query/query_all_gateways.rs +++ b/common/commands/src/validator/mixnet/query/query_all_gateways.rs @@ -39,7 +39,7 @@ pub async fn query(args: Args, client: &QueryClientWithNyxd) { node.owner.to_string(), node.gateway.host.to_string(), pretty_cosmwasm_coin(&node.pledge_amount), - node.gateway.version, + node.gateway.version.clone(), ]); } diff --git a/common/cosmwasm-smart-contracts/contracts-common/src/types.rs b/common/cosmwasm-smart-contracts/contracts-common/src/types.rs index e14cb3501a..30a01c17a9 100644 --- a/common/cosmwasm-smart-contracts/contracts-common/src/types.rs +++ b/common/cosmwasm-smart-contracts/contracts-common/src/types.rs @@ -8,7 +8,7 @@ use cosmwasm_std::Uint128; use serde::de::Error; use serde::{Deserialize, Deserializer}; use std::fmt::{self, Display, Formatter}; -use std::ops::Mul; +use std::ops::{Deref, Mul}; use std::str::FromStr; use thiserror::Error; @@ -23,7 +23,7 @@ pub fn truncate_decimal(amount: Decimal) -> Uint128 { #[derive(Error, Debug)] pub enum ContractsCommonError { #[error("Provided percent value ({0}) is greater than 100%")] - InvalidPercent(Decimal), + InvalidPercent(String), #[error("{source}")] StdErr { @@ -41,7 +41,7 @@ pub struct Percent(#[serde(deserialize_with = "de_decimal_percent")] Decimal); impl Percent { pub fn new(value: Decimal) -> Result { if value > Decimal::one() { - Err(ContractsCommonError::InvalidPercent(value)) + Err(ContractsCommonError::InvalidPercent(value.to_string())) } else { Ok(Percent(value)) } @@ -51,11 +51,15 @@ impl Percent { self.0 == Decimal::zero() } - pub fn zero() -> Self { + pub fn is_hundred(&self) -> bool { + self == &Self::hundred() + } + + pub const fn zero() -> Self { Self(Decimal::zero()) } - pub fn hundred() -> Self { + pub const fn hundred() -> Self { Self(Decimal::one()) } @@ -117,6 +121,70 @@ impl Mul for Percent { } } +impl Deref for Percent { + type Target = Decimal; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +// this is not implemented via From traits due to its naive nature and loss of precision +#[cfg(not(target_arch = "wasm32"))] +pub trait NaiveFloat { + fn naive_to_f64(&self) -> f64; + + fn naive_try_from_f64(val: f64) -> Result + where + Self: Sized; +} + +#[cfg(not(target_arch = "wasm32"))] +impl NaiveFloat for Percent { + fn naive_to_f64(&self) -> f64 { + use cosmwasm_std::Fraction; + + // note: this conversion loses precision with too many decimal places, + // but for the purposes of displaying basic performance, that's not an issue + self.numerator().u128() as f64 / self.denominator().u128() as f64 + } + + fn naive_try_from_f64(val: f64) -> Result + where + Self: Sized, + { + // we are only interested in positive values between 0 and 1 + if !(0. ..=1.).contains(&val) { + return Err(ContractsCommonError::InvalidPercent(val.to_string())); + } + + fn gcd(mut x: u64, mut y: u64) -> u64 { + while y > 0 { + let rem = x % y; + x = y; + y = rem; + } + + x + } + + fn to_rational(x: f64) -> (u64, u64) { + let log = x.log2().floor(); + if log >= 0.0 { + (x as u64, 1) + } else { + let num: u64 = (x / f64::EPSILON) as _; + let den: u64 = (1.0 / f64::EPSILON) as _; + let gcd = gcd(num, den); + (num / gcd, den / gcd) + } + } + + let (n, d) = to_rational(val); + Percent::new(Decimal::from_ratio(n, d)) + } +} + // implement custom Deserialize because we want to validate Percent has the correct range fn de_decimal_percent<'de, D>(deserializer: D) -> Result where @@ -204,6 +272,7 @@ impl ContractBuildInformation { #[cfg(test)] mod tests { use super::*; + use cosmwasm_std::Fraction; #[test] fn percent_serde() { @@ -243,4 +312,19 @@ mod tests { let p = serde_json::from_str::<'_, Percent>("\"1.00\"").unwrap(); assert_eq!(p.round_to_integer(), 100); } + + #[test] + fn naive_float_conversion() { + // around 15 decimal places is the maximum precision we can handle + // which is still way more than enough for what we use it for + let float: f64 = "0.546295475423853".parse().unwrap(); + let percent: Percent = "0.546295475423853".parse().unwrap(); + + assert_eq!(float, percent.naive_to_f64()); + + let epsilon = Decimal::from_ratio(1u64, 1000000000000000u64); + let converted = Percent::naive_try_from_f64(float).unwrap(); + + assert!(converted.0 - converted.0 < epsilon); + } } diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/Cargo.toml b/common/cosmwasm-smart-contracts/mixnet-contract/Cargo.toml index 58335f5c5e..ca87cb9b95 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/Cargo.toml +++ b/common/cosmwasm-smart-contracts/mixnet-contract/Cargo.toml @@ -12,6 +12,7 @@ repository = { workspace = true } bs58 = { workspace = true } cosmwasm-std = { workspace = true } cosmwasm-schema = { workspace = true } +cw-storage-plus.workspace = true cw-controllers = { workspace = true } cw2 = { workspace = true, optional = true } serde = { workspace = true, features = ["derive"] } diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/constants.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/constants.rs index 2269c20cd7..c68d1fc31f 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/constants.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/constants.rs @@ -5,6 +5,9 @@ use cosmwasm_std::{Decimal, Uint128}; pub const TOKEN_SUPPLY: Uint128 = Uint128::new(1_000_000_000_000_000); +pub const DEFAULT_INTERVAL_OPERATING_COST_AMOUNT: u128 = 40_000_000; +pub const DEFAULT_PROFIT_MARGIN_PERCENT: u64 = 20; + // I'm still not 100% sure how to feel about existence of this file // This is equivalent of representing our display coin with 6 decimal places. // I'm using this one as opposed to "Decimal::one()", as this provides us with higher accuracy diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/delegation.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/delegation.rs index f30dc63a73..5c7a733728 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/delegation.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/delegation.rs @@ -3,14 +3,14 @@ use crate::constants::TOKEN_SUPPLY; use crate::helpers::IntoBaseDecimal; -use crate::{Addr, MixId}; +use crate::{Addr, NodeId}; use cosmwasm_schema::cw_serde; use cosmwasm_std::{Coin, Decimal, StdResult}; // just use a string representation of those so that we wouldn't need to bother with decoding bytes // and trying to figure out whether they're valid, etc pub type OwnerProxySubKey = String; -pub type StorageKey = (MixId, OwnerProxySubKey); +pub type StorageKey = (NodeId, OwnerProxySubKey); // throughout the contract we ensure that our proxy can ONLY ever be the vesting contract // thus this method is equivalent to either using the existing address (for when there's no proxy) @@ -40,8 +40,9 @@ pub struct Delegation { /// Address of the owner of this delegation. pub owner: Addr, - /// Id of the MixNode that this delegation was performed against. - pub mix_id: MixId, + /// Id of the Node that this delegation was performed against. + #[serde(alias = "mix_id")] + pub node_id: NodeId, // Note to UI/UX devs: there's absolutely no point in displaying this value to the users, // it would serve them no purpose. It's only used for calculating rewards @@ -56,12 +57,13 @@ pub struct Delegation { /// Proxy address used to delegate the funds on behalf of another address pub proxy: Option, + // TODO: perhaps add a field to indicate if it was made against old mixnode with #[serde(default)]? } impl Delegation { pub fn new( owner: Addr, - mix_id: MixId, + node_id: NodeId, cumulative_reward_ratio: Decimal, amount: Coin, height: u64, @@ -73,7 +75,7 @@ impl Delegation { Delegation { owner, - mix_id, + node_id, cumulative_reward_ratio, amount, height, @@ -82,7 +84,7 @@ impl Delegation { } pub fn generate_storage_key( - mix_id: MixId, + mix_id: NodeId, owner_address: &Addr, proxy: Option<&Addr>, ) -> StorageKey { @@ -92,7 +94,7 @@ impl Delegation { // this function might seem a bit redundant, but I'd rather explicitly keep it around in case // some types change in the future pub fn generate_storage_key_with_subkey( - mix_id: MixId, + mix_id: NodeId, owner_proxy_subkey: OwnerProxySubKey, ) -> StorageKey { (mix_id, owner_proxy_subkey) @@ -107,13 +109,13 @@ impl Delegation { } pub fn storage_key(&self) -> StorageKey { - Self::generate_storage_key(self.mix_id, &self.owner, self.proxy.as_ref()) + Self::generate_storage_key(self.node_id, &self.owner, self.proxy.as_ref()) } } -/// Response containing paged list of all delegations made towards particular mixnode. +/// Response containing paged list of all delegations made towards particular node. #[cw_serde] -pub struct PagedMixNodeDelegationsResponse { +pub struct PagedNodeDelegationsResponse { /// Each individual delegation made. pub delegations: Vec, @@ -121,9 +123,9 @@ pub struct PagedMixNodeDelegationsResponse { pub start_next_after: Option, } -impl PagedMixNodeDelegationsResponse { +impl PagedNodeDelegationsResponse { pub fn new(delegations: Vec, start_next_after: Option) -> Self { - PagedMixNodeDelegationsResponse { + PagedNodeDelegationsResponse { delegations, start_next_after, } @@ -137,13 +139,13 @@ pub struct PagedDelegatorDelegationsResponse { pub delegations: Vec, /// Field indicating paging information for the following queries if the caller wishes to get further entries. - pub start_next_after: Option<(MixId, OwnerProxySubKey)>, + pub start_next_after: Option<(NodeId, OwnerProxySubKey)>, } impl PagedDelegatorDelegationsResponse { pub fn new( delegations: Vec, - start_next_after: Option<(MixId, OwnerProxySubKey)>, + start_next_after: Option<(NodeId, OwnerProxySubKey)>, ) -> Self { PagedDelegatorDelegationsResponse { delegations, @@ -154,19 +156,24 @@ impl PagedDelegatorDelegationsResponse { /// Response containing delegation details. #[cw_serde] -pub struct MixNodeDelegationResponse { +pub struct NodeDelegationResponse { /// If the delegation exists, this field contains its detailed information. pub delegation: Option, /// Flag indicating whether the node towards which the delegation was made is still bonded in the network. + #[deprecated(note = "this field will be removed. use .node_still_bonded instead")] pub mixnode_still_bonded: bool, + + pub node_still_bonded: bool, } -impl MixNodeDelegationResponse { - pub fn new(delegation: Option, mixnode_still_bonded: bool) -> Self { - MixNodeDelegationResponse { +impl NodeDelegationResponse { + pub fn new(delegation: Option, node_still_bonded: bool) -> Self { + #[allow(deprecated)] + NodeDelegationResponse { delegation, - mixnode_still_bonded, + mixnode_still_bonded: node_still_bonded, + node_still_bonded, } } } diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs index 12f2030acc..9a67023224 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs @@ -1,7 +1,10 @@ // Copyright 2022-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::{EpochEventId, EpochState, IdentityKey, MixId, OperatingCostRange, ProfitMarginRange}; +use crate::nym_node::Role; +use crate::{ + EpochEventId, EpochState, IntervalEventId, NodeId, OperatingCostRange, ProfitMarginRange, +}; use contracts_common::signing::verifier::ApiVerifierError; use contracts_common::Percent; use cosmwasm_std::{Addr, Coin, Decimal, Uint128}; @@ -34,6 +37,16 @@ pub enum MixnetContractError { #[error("Not enough funds sent for node pledge. (received {received}, minimum {minimum})")] InsufficientPledge { received: Coin, minimum: Coin }, + #[error( + "the provided value for node host is too long. it must not be longer than 255 characters" + )] + HostTooLong, + + #[error( + "the provided node identity public key is not a correctly encoded base58 slice of 32 bytes" + )] + InvalidPubKey, + #[error("Attempted to reduce node pledge ({current}{denom} - {decrease_by}{denom}) below the minimum amount: {minimum}{denom}")] InvalidPledgeReduction { current: Uint128, @@ -45,11 +58,19 @@ pub enum MixnetContractError { #[error("A pledge change is already pending in this epoch. The event id: {pending_event_id}")] PendingPledgeChange { pending_event_id: EpochEventId }, + #[error( + "A cost params change is already pending in this epoch. The event id: {pending_event_id}" + )] + PendingParamsChange { pending_event_id: IntervalEventId }, + #[error("Not enough funds sent for node delegation. (received {received}, minimum {minimum})")] InsufficientDelegation { received: Coin, minimum: Coin }, + #[error("Node ({node_id}) does not exist")] + NymNodeBondNotFound { node_id: NodeId }, + #[error("Mixnode ({mix_id}) does not exist")] - MixNodeBondNotFound { mix_id: MixId }, + MixNodeBondNotFound { mix_id: NodeId }, #[error("{owner} does not seem to own any mixnodes")] NoAssociatedMixNodeBond { owner: Addr }, @@ -57,12 +78,18 @@ pub enum MixnetContractError { #[error("{owner} does not seem to own any gateways")] NoAssociatedGatewayBond { owner: Addr }, + #[error("{owner} does not seem to own any nodes")] + NoAssociatedNodeBond { owner: Addr }, + #[error("This address has already bonded a mixnode")] AlreadyOwnsMixnode, #[error("This address has already bonded a gateway")] AlreadyOwnsGateway, + #[error("This address has already bonded a nym-node")] + AlreadyOwnsNymNode, + #[error("Gateway with this identity already exists. Its owner is {owner}")] DuplicateGateway { owner: Addr }, @@ -103,32 +130,41 @@ pub enum MixnetContractError { epoch_end: i64, }, - #[error("Mixnode {mix_id} has already been rewarded during the current rewarding epoch ({absolute_epoch_id})")] - MixnodeAlreadyRewarded { - mix_id: MixId, + #[error("attempted to reward a gateway node - this has not been fully integrated yet")] + GatewayRewarding, + + #[error("node {node_id} has already been rewarded during the current rewarding epoch ({absolute_epoch_id})")] + NodeAlreadyRewarded { + node_id: NodeId, absolute_epoch_id: u32, }, + #[error("node {node_id} hasn't been assigned the role of {role} for this epoch")] + IncorrectEpochRole { node_id: NodeId, role: Role }, + #[error("Mixnode {mix_id} hasn't been selected to the rewarding set in this epoch ({absolute_epoch_id})")] MixnodeNotInRewardedSet { - mix_id: MixId, + mix_id: NodeId, absolute_epoch_id: u32, }, #[error("Mixnode {mix_id} is currently in the process of unbonding")] - MixnodeIsUnbonding { mix_id: MixId }, + MixnodeIsUnbonding { mix_id: NodeId }, + + #[error("Node {node_id} is currently in the process of unbonding")] + NodeIsUnbonding { node_id: NodeId }, #[error("Mixnode {mix_id} has already unbonded")] - MixnodeHasUnbonded { mix_id: MixId }, + MixnodeHasUnbonded { mix_id: NodeId }, #[error("The contract has ended up in a state that was deemed impossible: {comment}")] InconsistentState { comment: String }, #[error( - "Could not find any delegation information associated with mixnode {mix_id} for {address} (proxy: {proxy:?})" + "Could not find any delegation information associated with node {node_id} for {address} (proxy: {proxy:?})" )] - NoMixnodeDelegationFound { - mix_id: MixId, + NodeDelegationNotFound { + node_id: NodeId, address: String, proxy: Option, }, @@ -136,63 +172,18 @@ pub enum MixnetContractError { #[error("Provided message to update rewarding params did not contain any updates")] EmptyParamsChangeMsg, - #[error("Provided active set size is bigger than the rewarded set")] + #[error("one of the roles in the new active set is empty")] + EmptyRoleAssignment, + + #[error("the number of mixnodes in the rewarded set is not divisible by the number of mix-layers (3)")] + UnevenLayerAssignment, + + #[error("provided active set is bigger than the rewarded set")] InvalidActiveSetSize, - #[error("Provided rewarded set size is smaller than the active set")] - InvalidRewardedSetSize, - - #[error("Provided active set size is zero")] - ZeroActiveSet, - - #[error("Provided rewarded set size is zero")] - ZeroRewardedSet, - - #[error("Received unexpected value for the active set. Got: {received}, expected: {expected}")] - UnexpectedActiveSetSize { received: u32, expected: u32 }, - - #[error("Received unexpected value for the rewarded set. Got: {received}, expected at most: {expected}")] - UnexpectedRewardedSetSize { received: u32, expected: u32 }, - - #[error("Mixnode {mix_id} appears multiple times in the provided rewarded set update!")] - DuplicateRewardedSetNode { mix_id: MixId }, - - #[error("Family with head {head} does not exist!")] - FamilyDoesNotExist { head: String }, - - #[error("Family with label {label} does not exist!")] - FamilyLabelDoesNotExist { label: String }, - - #[error("Family with label '{0}' already exists")] - FamilyWithLabelExists(String), - #[error("Invalid layer expected 1, 2 or 3, got {0}")] InvalidLayer(u8), - #[error("Head already has a family")] - FamilyCanHaveOnlyOne, - - #[error("Already member of family {0}")] - AlreadyMemberOfFamily(String), - - #[error("Can't join own family, family head {head}, member {member}")] - CantJoinOwnFamily { - head: IdentityKey, - member: IdentityKey, - }, - - #[error("Can't leave own family, family head {head}, member {member}")] - CantLeaveOwnFamily { - head: IdentityKey, - member: IdentityKey, - }, - - #[error("{member} is not a member of family {head}")] - NotAMember { - head: IdentityKey, - member: IdentityKey, - }, - #[error("Feature is not yet implemented")] NotImplemented, @@ -219,13 +210,28 @@ pub enum MixnetContractError { #[error("attempted to reward mixnode out of order. Attempted to reward {attempted_to_reward} while last rewarded was {last_rewarded}.")] RewardingOutOfOrder { - last_rewarded: MixId, - attempted_to_reward: MixId, + last_rewarded: NodeId, + attempted_to_reward: NodeId, }, #[error("the epoch is currently not in the 'event reconciliation' state. (the state is {current_state})")] EpochNotInEventReconciliationState { current_state: EpochState }, + #[error( + "the epoch is currently not in the 'role assignment' state. (the state is {current_state})" + )] + EpochNotInRoleAssignmentState { current_state: EpochState }, + + #[error("unexpected role assignment. got: {got} while expected: {expected}")] + UnexpectedRoleAssignment { expected: Role, got: Role }, + + #[error("attempted to assign an invalid number of nodes for a role of {role}. got {assigned}, but the maximum allowed is {allowed}")] + IllegalRoleCount { + role: Role, + assigned: u32, + allowed: u32, + }, + #[error("the epoch is currently not in the 'epoch advancement' state. (the state is {current_state})")] EpochNotInAdvancementState { current_state: EpochState }, @@ -258,6 +264,17 @@ pub enum MixnetContractError { provided: Uint128, range: OperatingCostRange, }, + + #[error( + "currently it's not possible to migrate nodes bonded with vesting tokens into a nym-node. please perform vesting->liquid migration first." + )] + VestingNodeMigration, + + #[error("value {got} does not correspond to any known node role")] + UnknownRoleRepresentation { got: u8 }, + + #[error("the total work for this epoch seems to be bigger than 1.0!")] + TotalWorkAboveOne, } impl MixnetContractError { diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/events.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/events.rs index 7ebb604225..e0b7f5f506 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/events.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/events.rs @@ -1,11 +1,13 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::error::MixnetContractError; use crate::gateway::GatewayConfigUpdate; -use crate::mixnode::{MixNodeConfigUpdate, MixNodeCostParams}; -use crate::reward_params::{IntervalRewardParams, IntervalRewardingParamsUpdate}; +use crate::mixnode::{MixNodeConfigUpdate, NodeCostParams}; +use crate::nym_node::Role; +use crate::reward_params::{ActiveSetUpdate, IntervalRewardParams, IntervalRewardingParamsUpdate}; use crate::rewarding::RewardDistribution; -use crate::{BlockHeight, ContractStateParams, IdentityKeyRef, Interval, Layer, MixId}; +use crate::{BlockHeight, ContractStateParams, EpochId, IdentityKeyRef, Interval, NodeId}; pub use contracts_common::events::*; use cosmwasm_std::{Addr, Coin, Decimal, Event}; use std::fmt::Display; @@ -14,6 +16,11 @@ pub const EVENT_VERSION_PREFIX: &str = "v2_"; pub enum MixnetEventType { MixnodeBonding, + NymNodeBonding, + NymNodeUnbonding, + PendingNymNodeUnbonding, + GatewayMigration, + MixnodeMigration, PendingPledgeIncrease, PledgeIncrease, PendingPledgeDecrease, @@ -23,9 +30,9 @@ pub enum MixnetEventType { PendingMixnodeUnbonding, MixnodeUnbonding, MixnodeConfigUpdate, - PendingMixnodeCostParamsUpdate, - MixnodeCostParamsUpdate, - MixnodeRewarding, + PendingCostParamsUpdate, + CostParamsUpdate, + NodeRewarding, WithdrawDelegatorReward, WithdrawOperatorReward, PendingActiveSetUpdate, @@ -41,6 +48,7 @@ pub enum MixnetEventType { RewardingValidatorUpdate, BeginEpochTransition, AdvanceEpoch, + RoleAssignment, ExecutePendingEpochEvents, ExecutePendingIntervalEvents, ReconcilePendingEvents, @@ -59,6 +67,11 @@ impl Display for MixnetEventType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let event_name = match self { MixnetEventType::MixnodeBonding => "mixnode_bonding", + MixnetEventType::NymNodeBonding => "nymnode_bonding", + MixnetEventType::NymNodeUnbonding => "nymnode_unbonding", + MixnetEventType::PendingNymNodeUnbonding => "pending_nymnode_unbonding", + MixnetEventType::GatewayMigration => "gateway_migration", + MixnetEventType::MixnodeMigration => "mixnode_migration", MixnetEventType::PendingPledgeIncrease => "pending_pledge_increase", MixnetEventType::PledgeIncrease => "pledge_increase", MixnetEventType::PendingPledgeDecrease => "pending_pledge_decrease", @@ -68,9 +81,9 @@ impl Display for MixnetEventType { MixnetEventType::PendingMixnodeUnbonding => "pending_mixnode_unbonding", MixnetEventType::MixnodeConfigUpdate => "mixnode_config_update", MixnetEventType::MixnodeUnbonding => "mixnode_unbonding", - MixnetEventType::PendingMixnodeCostParamsUpdate => "pending_mixnode_cost_params_update", - MixnetEventType::MixnodeCostParamsUpdate => "mixnode_cost_params_update", - MixnetEventType::MixnodeRewarding => "mix_rewarding", + MixnetEventType::PendingCostParamsUpdate => "pending_cost_params_update", + MixnetEventType::CostParamsUpdate => "cost_params_update", + MixnetEventType::NodeRewarding => "node_rewarding", MixnetEventType::WithdrawDelegatorReward => "withdraw_delegator_reward", MixnetEventType::WithdrawOperatorReward => "withdraw_operator_reward", MixnetEventType::PendingActiveSetUpdate => "pending_active_set_update", @@ -87,6 +100,7 @@ impl Display for MixnetEventType { MixnetEventType::RewardingValidatorUpdate => "rewarding_validator_address_update", MixnetEventType::BeginEpochTransition => "beginning_epoch_transition", MixnetEventType::AdvanceEpoch => "advance_epoch", + MixnetEventType::RoleAssignment => "role_assignment", MixnetEventType::ExecutePendingEpochEvents => "execute_pending_epoch_events", MixnetEventType::ExecutePendingIntervalEvents => "execute_pending_interval_events", MixnetEventType::ReconcilePendingEvents => "reconcile_pending_events", @@ -103,6 +117,7 @@ impl Display for MixnetEventType { // attributes that are used in multiple places pub const OWNER_KEY: &str = "owner"; pub const AMOUNT_KEY: &str = "amount"; +pub const ERROR_MESSAGE_KEY: &str = "error_message"; // event-specific attributes @@ -113,16 +128,14 @@ pub const UNIT_REWARD_KEY: &str = "unit_reward"; // bonding/unbonding pub const MIX_ID_KEY: &str = "mix_id"; +pub const NODE_ID_KEY: &str = "node_id"; pub const NODE_IDENTITY_KEY: &str = "identity"; -pub const ASSIGNED_LAYER_KEY: &str = "assigned_layer"; // settings change -pub const OLD_MINIMUM_MIXNODE_PLEDGE_KEY: &str = "old_minimum_mixnode_pledge"; -pub const OLD_MINIMUM_GATEWAY_PLEDGE_KEY: &str = "old_minimum_gateway_pledge"; +pub const OLD_MINIMUM_PLEDGE_KEY: &str = "old_minimum_pledge"; pub const OLD_MINIMUM_DELEGATION_KEY: &str = "old_minimum_delegation"; -pub const NEW_MINIMUM_MIXNODE_PLEDGE_KEY: &str = "new_minimum_mixnode_pledge"; -pub const NEW_MINIMUM_GATEWAY_PLEDGE_KEY: &str = "new_minimum_gateway_pledge"; +pub const NEW_MINIMUM_PLEDGE_KEY: &str = "new_minimum_pledge"; pub const NEW_MINIMUM_DELEGATION_KEY: &str = "new_minimum_delegation"; pub const OLD_REWARDING_VALIDATOR_ADDRESS_KEY: &str = "old_rewarding_validator_address"; @@ -147,11 +160,16 @@ pub const BOND_NOT_FOUND_VALUE: &str = "bond_not_found"; pub const ZERO_PERFORMANCE_VALUE: &str = "zero_performance"; // rewarded set update -pub const ACTIVE_SET_SIZE_KEY: &str = "active_set_size"; +pub const NUM_MIXNODES_KEY: &str = "num_mixnodes"; +pub const NUM_ENTRIES_KEY: &str = "num_entry_gateways"; +pub const NUM_EXITS_KEY: &str = "num_exit_gateways"; pub const CURRENT_EPOCH_KEY: &str = "current_epoch"; pub const NEW_CURRENT_EPOCH_KEY: &str = "new_current_epoch"; +pub const ROLE_KEY: &str = "role"; +pub const NODE_COUNT_KEY: &str = "node_count"; + // interval pub const EVENTS_EXECUTED_KEY: &str = "number_of_events_executed"; pub const EVENT_CREATION_HEIGHT_KEY: &str = "created_at"; @@ -163,7 +181,7 @@ pub fn new_delegation_event( created_at: BlockHeight, delegator: &Addr, amount: &Coin, - mix_id: MixId, + mix_id: NodeId, unit_reward: Decimal, ) -> Event { Event::new(MixnetEventType::Delegation) @@ -174,45 +192,57 @@ pub fn new_delegation_event( .add_attribute(UNIT_REWARD_KEY, unit_reward.to_string()) } -pub fn new_delegation_on_unbonded_node_event(delegator: &Addr, mix_id: MixId) -> Event { +pub fn new_delegation_on_unbonded_node_event(delegator: &Addr, mix_id: NodeId) -> Event { Event::new(MixnetEventType::Delegation) .add_attribute(DELEGATOR_KEY, delegator) .add_attribute(DELEGATION_TARGET_KEY, mix_id.to_string()) } -pub fn new_pending_delegation_event(delegator: &Addr, amount: &Coin, mix_id: MixId) -> Event { +pub fn new_pending_delegation_event(delegator: &Addr, amount: &Coin, mix_id: NodeId) -> Event { Event::new(MixnetEventType::PendingDelegation) .add_attribute(DELEGATOR_KEY, delegator) .add_attribute(AMOUNT_KEY, amount.to_string()) .add_attribute(DELEGATION_TARGET_KEY, mix_id.to_string()) } -pub fn new_withdraw_operator_reward_event(owner: &Addr, amount: Coin, mix_id: MixId) -> Event { +pub fn new_withdraw_operator_reward_event(owner: &Addr, amount: Coin, mix_id: NodeId) -> Event { Event::new(MixnetEventType::WithdrawOperatorReward) .add_attribute(OWNER_KEY, owner.as_str()) .add_attribute(AMOUNT_KEY, amount.to_string()) .add_attribute(MIX_ID_KEY, mix_id.to_string()) } -pub fn new_withdraw_delegator_reward_event(delegator: &Addr, amount: Coin, mix_id: MixId) -> Event { +pub fn new_withdraw_delegator_reward_event( + delegator: &Addr, + amount: Coin, + mix_id: NodeId, +) -> Event { Event::new(MixnetEventType::WithdrawDelegatorReward) .add_attribute(DELEGATOR_KEY, delegator) .add_attribute(AMOUNT_KEY, amount.to_string()) .add_attribute(DELEGATION_TARGET_KEY, mix_id.to_string()) } -pub fn new_active_set_update_event(created_at: BlockHeight, new_size: u32) -> Event { +pub fn new_active_set_update_failure(err: MixnetContractError) -> Event { + Event::new(MixnetEventType::ActiveSetUpdate).add_attribute(ERROR_MESSAGE_KEY, err.to_string()) +} + +pub fn new_active_set_update_event(created_at: BlockHeight, update: ActiveSetUpdate) -> Event { Event::new(MixnetEventType::ActiveSetUpdate) .add_attribute(EVENT_CREATION_HEIGHT_KEY, created_at.to_string()) - .add_attribute(ACTIVE_SET_SIZE_KEY, new_size.to_string()) + .add_attribute(NUM_MIXNODES_KEY, update.mixnodes.to_string()) + .add_attribute(NUM_ENTRIES_KEY, update.entry_gateways.to_string()) + .add_attribute(NUM_EXITS_KEY, update.exit_gateways.to_string()) } pub fn new_pending_active_set_update_event( - new_size: u32, + update: ActiveSetUpdate, approximate_time_remaining_secs: i64, ) -> Event { Event::new(MixnetEventType::PendingActiveSetUpdate) - .add_attribute(ACTIVE_SET_SIZE_KEY, new_size.to_string()) + .add_attribute(NUM_MIXNODES_KEY, update.mixnodes.to_string()) + .add_attribute(NUM_ENTRIES_KEY, update.entry_gateways.to_string()) + .add_attribute(NUM_EXITS_KEY, update.exit_gateways.to_string()) .add_attribute( APPROXIMATE_TIME_LEFT_SECS_KEY, approximate_time_remaining_secs.to_string(), @@ -221,7 +251,6 @@ pub fn new_pending_active_set_update_event( pub fn new_rewarding_params_update_event( created_at: BlockHeight, - update: IntervalRewardingParamsUpdate, updated: IntervalRewardParams, ) -> Event { @@ -252,30 +281,19 @@ pub fn new_pending_rewarding_params_update_event( ) } -pub fn new_undelegation_event(created_at: BlockHeight, delegator: &Addr, mix_id: MixId) -> Event { +pub fn new_undelegation_event(created_at: BlockHeight, delegator: &Addr, mix_id: NodeId) -> Event { Event::new(MixnetEventType::Undelegation) .add_attribute(EVENT_CREATION_HEIGHT_KEY, created_at.to_string()) .add_attribute(DELEGATOR_KEY, delegator) .add_attribute(MIX_ID_KEY, mix_id.to_string()) } -pub fn new_pending_undelegation_event(delegator: &Addr, mix_id: MixId) -> Event { +pub fn new_pending_undelegation_event(delegator: &Addr, mix_id: NodeId) -> Event { Event::new(MixnetEventType::PendingUndelegation) .add_attribute(DELEGATOR_KEY, delegator) .add_attribute(MIX_ID_KEY, mix_id.to_string()) } -pub fn new_gateway_bonding_event( - owner: &Addr, - amount: &Coin, - identity: IdentityKeyRef<'_>, -) -> Event { - Event::new(MixnetEventType::GatewayBonding) - .add_attribute(OWNER_KEY, owner) - .add_attribute(NODE_IDENTITY_KEY, identity) - .add_attribute(AMOUNT_KEY, amount.to_string()) -} - pub fn new_gateway_unbonding_event( owner: &Addr, amount: &Coin, @@ -287,49 +305,85 @@ pub fn new_gateway_unbonding_event( .add_attribute(AMOUNT_KEY, amount.to_string()) } -pub fn new_mixnode_bonding_event( +pub fn new_nym_node_bonding_event( owner: &Addr, amount: &Coin, identity: IdentityKeyRef<'_>, - mix_id: MixId, - assigned_layer: Layer, + node_id: NodeId, ) -> Event { - // coin implements Display trait and we use that implementation here - Event::new(MixnetEventType::MixnodeBonding) - .add_attribute(MIX_ID_KEY, mix_id.to_string()) + Event::new(MixnetEventType::NymNodeBonding) + .add_attribute(NODE_ID_KEY, node_id.to_string()) .add_attribute(NODE_IDENTITY_KEY, identity) .add_attribute(OWNER_KEY, owner) - .add_attribute(ASSIGNED_LAYER_KEY, assigned_layer) .add_attribute(AMOUNT_KEY, amount.to_string()) } -pub fn new_pending_pledge_increase_event(mix_id: MixId, amount: &Coin) -> Event { +pub fn new_nym_node_unbonding_event(created_at: BlockHeight, node_id: NodeId) -> Event { + Event::new(MixnetEventType::NymNodeUnbonding) + .add_attribute(EVENT_CREATION_HEIGHT_KEY, created_at.to_string()) + .add_attribute(NODE_ID_KEY, node_id.to_string()) +} + +pub fn new_pending_nym_node_unbonding_event( + owner: &Addr, + identity: IdentityKeyRef<'_>, + node_id: NodeId, +) -> Event { + Event::new(MixnetEventType::PendingNymNodeUnbonding) + .add_attribute(NODE_ID_KEY, node_id.to_string()) + .add_attribute(NODE_IDENTITY_KEY, identity) + .add_attribute(OWNER_KEY, owner) +} + +pub fn new_migrated_gateway_event( + owner: &Addr, + identity: IdentityKeyRef<'_>, + node_id: NodeId, +) -> Event { + Event::new(MixnetEventType::GatewayMigration) + .add_attribute(NODE_ID_KEY, node_id.to_string()) + .add_attribute(NODE_IDENTITY_KEY, identity) + .add_attribute(OWNER_KEY, owner) +} + +pub fn new_migrated_mixnode_event( + owner: &Addr, + identity: IdentityKeyRef<'_>, + node_id: NodeId, +) -> Event { + Event::new(MixnetEventType::MixnodeMigration) + .add_attribute(NODE_ID_KEY, node_id.to_string()) + .add_attribute(NODE_IDENTITY_KEY, identity) + .add_attribute(OWNER_KEY, owner) +} + +pub fn new_pending_pledge_increase_event(node_id: NodeId, amount: &Coin) -> Event { Event::new(MixnetEventType::PendingPledgeIncrease) - .add_attribute(MIX_ID_KEY, mix_id.to_string()) + .add_attribute(NODE_ID_KEY, node_id.to_string()) .add_attribute(AMOUNT_KEY, amount.to_string()) } -pub fn new_pledge_increase_event(created_at: BlockHeight, mix_id: MixId, amount: &Coin) -> Event { +pub fn new_pledge_increase_event(created_at: BlockHeight, node_id: NodeId, amount: &Coin) -> Event { Event::new(MixnetEventType::PledgeIncrease) .add_attribute(EVENT_CREATION_HEIGHT_KEY, created_at.to_string()) - .add_attribute(MIX_ID_KEY, mix_id.to_string()) + .add_attribute(NODE_ID_KEY, node_id.to_string()) .add_attribute(AMOUNT_KEY, amount.to_string()) } -pub fn new_pending_pledge_decrease_event(mix_id: MixId, amount: &Coin) -> Event { +pub fn new_pending_pledge_decrease_event(node_id: NodeId, amount: &Coin) -> Event { Event::new(MixnetEventType::PendingPledgeDecrease) - .add_attribute(MIX_ID_KEY, mix_id.to_string()) + .add_attribute(NODE_ID_KEY, node_id.to_string()) .add_attribute(AMOUNT_KEY, amount.to_string()) } -pub fn new_pledge_decrease_event(created_at: BlockHeight, mix_id: MixId, amount: &Coin) -> Event { +pub fn new_pledge_decrease_event(created_at: BlockHeight, node_id: NodeId, amount: &Coin) -> Event { Event::new(MixnetEventType::PledgeDecrease) .add_attribute(EVENT_CREATION_HEIGHT_KEY, created_at.to_string()) - .add_attribute(MIX_ID_KEY, mix_id.to_string()) + .add_attribute(NODE_ID_KEY, node_id.to_string()) .add_attribute(AMOUNT_KEY, amount.to_string()) } -pub fn new_mixnode_unbonding_event(created_at: BlockHeight, mix_id: MixId) -> Event { +pub fn new_mixnode_unbonding_event(created_at: BlockHeight, mix_id: NodeId) -> Event { Event::new(MixnetEventType::MixnodeUnbonding) .add_attribute(EVENT_CREATION_HEIGHT_KEY, created_at.to_string()) .add_attribute(MIX_ID_KEY, mix_id.to_string()) @@ -338,7 +392,7 @@ pub fn new_mixnode_unbonding_event(created_at: BlockHeight, mix_id: MixId) -> Ev pub fn new_pending_mixnode_unbonding_event( owner: &Addr, identity: IdentityKeyRef<'_>, - mix_id: MixId, + mix_id: NodeId, ) -> Event { Event::new(MixnetEventType::PendingMixnodeUnbonding) .add_attribute(MIX_ID_KEY, mix_id.to_string()) @@ -347,7 +401,7 @@ pub fn new_pending_mixnode_unbonding_event( } pub fn new_mixnode_config_update_event( - mix_id: MixId, + mix_id: NodeId, owner: &Addr, update: &MixNodeConfigUpdate, ) -> Event { @@ -363,23 +417,18 @@ pub fn new_gateway_config_update_event(owner: &Addr, update: &GatewayConfigUpdat .add_attribute(UPDATED_GATEWAY_CONFIG_KEY, update.to_inline_json()) } -pub fn new_mixnode_pending_cost_params_update_event( - mix_id: MixId, - owner: &Addr, - new_costs: &MixNodeCostParams, -) -> Event { - Event::new(MixnetEventType::PendingMixnodeCostParamsUpdate) - .add_attribute(MIX_ID_KEY, mix_id.to_string()) - .add_attribute(OWNER_KEY, owner) +pub fn new_pending_cost_params_update_event(mix_id: NodeId, new_costs: &NodeCostParams) -> Event { + Event::new(MixnetEventType::PendingCostParamsUpdate) + .add_attribute(NODE_ID_KEY, mix_id.to_string()) .add_attribute(UPDATED_MIXNODE_COST_PARAMS_KEY, new_costs.to_inline_json()) } -pub fn new_mixnode_cost_params_update_event( +pub fn new_cost_params_update_event( created_at: BlockHeight, - mix_id: MixId, - new_costs: &MixNodeCostParams, + mix_id: NodeId, + new_costs: &NodeCostParams, ) -> Event { - Event::new(MixnetEventType::MixnodeCostParamsUpdate) + Event::new(MixnetEventType::CostParamsUpdate) .add_attribute(EVENT_CREATION_HEIGHT_KEY, created_at.to_string()) .add_attribute(MIX_ID_KEY, mix_id.to_string()) .add_attribute(UPDATED_MIXNODE_COST_PARAMS_KEY, new_costs.to_inline_json()) @@ -397,37 +446,25 @@ pub fn new_settings_update_event( ) -> Event { let mut event = Event::new(MixnetEventType::ContractSettingsUpdate); - if old_params.minimum_mixnode_pledge != new_params.minimum_mixnode_pledge { + if old_params.minimum_pledge != new_params.minimum_pledge { event = event .add_attribute( - OLD_MINIMUM_MIXNODE_PLEDGE_KEY, - old_params.minimum_mixnode_pledge.to_string(), + OLD_MINIMUM_PLEDGE_KEY, + old_params.minimum_pledge.to_string(), ) .add_attribute( - NEW_MINIMUM_MIXNODE_PLEDGE_KEY, - new_params.minimum_mixnode_pledge.to_string(), + NEW_MINIMUM_PLEDGE_KEY, + new_params.minimum_pledge.to_string(), ) } - if old_params.minimum_gateway_pledge != new_params.minimum_gateway_pledge { - event = event - .add_attribute( - OLD_MINIMUM_GATEWAY_PLEDGE_KEY, - old_params.minimum_gateway_pledge.to_string(), - ) - .add_attribute( - NEW_MINIMUM_GATEWAY_PLEDGE_KEY, - new_params.minimum_gateway_pledge.to_string(), - ) - } - - if old_params.minimum_mixnode_delegation != new_params.minimum_mixnode_delegation { - if let Some(ref old) = old_params.minimum_mixnode_delegation { + if old_params.minimum_delegation != new_params.minimum_delegation { + if let Some(ref old) = old_params.minimum_delegation { event = event.add_attribute(OLD_MINIMUM_DELEGATION_KEY, old.to_string()) } else { event = event.add_attribute(OLD_MINIMUM_DELEGATION_KEY, "None") } - if let Some(ref new) = new_params.minimum_mixnode_delegation { + if let Some(ref new) = new_params.minimum_delegation { event = event.add_attribute(NEW_MINIMUM_DELEGATION_KEY, new.to_string()) } else { event = event.add_attribute(NEW_MINIMUM_DELEGATION_KEY, "None") @@ -437,41 +474,41 @@ pub fn new_settings_update_event( event } -pub fn new_not_found_mix_operator_rewarding_event(interval: Interval, mix_id: MixId) -> Event { - Event::new(MixnetEventType::MixnodeRewarding) +pub fn new_not_found_node_operator_rewarding_event(interval: Interval, node_id: NodeId) -> Event { + Event::new(MixnetEventType::NodeRewarding) .add_attribute( INTERVAL_KEY, interval.current_epoch_absolute_id().to_string(), ) - .add_attribute(MIX_ID_KEY, mix_id.to_string()) + .add_attribute(NODE_ID_KEY, node_id.to_string()) .add_attribute(NO_REWARD_REASON_KEY, BOND_NOT_FOUND_VALUE) } -pub fn new_zero_uptime_mix_operator_rewarding_event(interval: Interval, mix_id: MixId) -> Event { - Event::new(MixnetEventType::MixnodeRewarding) +pub fn new_zero_uptime_mix_operator_rewarding_event(interval: Interval, node_id: NodeId) -> Event { + Event::new(MixnetEventType::NodeRewarding) .add_attribute( INTERVAL_KEY, interval.current_epoch_absolute_id().to_string(), ) - .add_attribute(MIX_ID_KEY, mix_id.to_string()) + .add_attribute(NODE_ID_KEY, node_id.to_string()) .add_attribute(NO_REWARD_REASON_KEY, ZERO_PERFORMANCE_VALUE) } pub fn new_mix_rewarding_event( interval: Interval, - mix_id: MixId, + node_id: NodeId, reward_distribution: RewardDistribution, prior_delegates: Decimal, prior_unit_reward: Decimal, ) -> Event { - Event::new(MixnetEventType::MixnodeRewarding) + Event::new(MixnetEventType::NodeRewarding) .add_attribute( INTERVAL_KEY, interval.current_epoch_absolute_id().to_string(), ) .add_attribute(PRIOR_DELEGATES_KEY, prior_delegates.to_string()) .add_attribute(PRIOR_UNIT_REWARD_KEY, prior_unit_reward.to_string()) - .add_attribute(MIX_ID_KEY, mix_id.to_string()) + .add_attribute(NODE_ID_KEY, node_id.to_string()) .add_attribute( OPERATOR_REWARD_KEY, reward_distribution.operator.to_string(), @@ -489,13 +526,15 @@ pub fn new_epoch_transition_start_event(current_interval: Interval) -> Event { ) } -pub fn new_advance_epoch_event(interval: Interval, rewarded_nodes: u32) -> Event { +pub fn new_assigned_role_event(role: Role, nodes: u32) -> Event { + Event::new(MixnetEventType::RoleAssignment) + .add_attribute(ROLE_KEY, role.to_string()) + .add_attribute(NODE_COUNT_KEY, nodes.to_string()) +} + +pub fn new_advance_epoch_event(epoch_id: EpochId) -> Event { Event::new(MixnetEventType::AdvanceEpoch) - .add_attribute( - NEW_CURRENT_EPOCH_KEY, - interval.current_epoch_absolute_id().to_string(), - ) - .add_attribute(REWARDED_SET_NODES_KEY, rewarded_nodes.to_string()) + .add_attribute(NEW_CURRENT_EPOCH_KEY, epoch_id.to_string()) } pub fn new_pending_epoch_events_execution_event(executed: u32) -> Event { diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/families.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/families.rs deleted file mode 100644 index 58f74f4e08..0000000000 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/families.rs +++ /dev/null @@ -1,189 +0,0 @@ -// Copyright 2022-2023 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::{IdentityKey, IdentityKeyRef}; -use cosmwasm_schema::cw_serde; -use schemars::JsonSchema; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use std::fmt::{Display, Formatter}; -use std::str::FromStr; - -/// A group of mixnodes associated with particular staking entity. -/// When defined all nodes belonging to the same family will be prioritised to be put onto the same layer. -#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] -#[cfg_attr( - feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/NodeFamily.ts") -)] -#[cw_serde] -pub struct Family { - /// Owner of this family. - head: FamilyHead, - - /// Optional proxy (i.e. vesting contract address) used when creating the family. - proxy: Option, - - /// Human readable label for this family. - label: String, -} - -/// Head of particular family as identified by its identity key (i.e. public component of its ed25519 keypair stringified into base58). -#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] -#[cfg_attr( - feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/NodeFamilyHead.ts") -)] -#[derive(Debug, Clone, Eq, PartialEq, JsonSchema)] -pub struct FamilyHead(IdentityKey); - -impl Serialize for FamilyHead { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - self.0.serialize(serializer) - } -} - -impl<'de> Deserialize<'de> for FamilyHead { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let inner = IdentityKey::deserialize(deserializer)?; - Ok(FamilyHead(inner)) - } -} - -impl FromStr for FamilyHead { - type Err = ::Err; - - fn from_str(s: &str) -> Result { - // theoretically we should be verifying whether it's a valid base58 value - // (or even better, whether it's a valid ed25519 public key), but definition of - // `FamilyHead` might change later - Ok(FamilyHead(IdentityKey::from_str(s)?)) - } -} - -impl Display for FamilyHead { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - self.0.fmt(f) - } -} - -impl FamilyHead { - pub fn new>(identity: S) -> Self { - FamilyHead(identity.into()) - } - - pub fn identity(&self) -> IdentityKeyRef<'_> { - &self.0 - } -} - -impl Family { - pub fn new(head: FamilyHead, label: String) -> Self { - Family { - head, - proxy: None, - label, - } - } - - #[allow(dead_code)] - pub fn head(&self) -> &FamilyHead { - &self.head - } - - pub fn head_identity(&self) -> IdentityKeyRef<'_> { - self.head.identity() - } - - #[allow(dead_code)] - pub fn proxy(&self) -> Option<&String> { - self.proxy.as_ref() - } - - pub fn label(&self) -> &str { - &self.label - } -} - -/// Response containing paged list of all families registered in the contract. -#[cw_serde] -pub struct PagedFamiliesResponse { - /// The families registered in the contract. - pub families: Vec, - - /// Field indicating paging information for the following queries if the caller wishes to get further entries. - pub start_next_after: Option, -} - -/// Response containing paged list of all family members (of ALL families) registered in the contract. -#[cw_serde] -pub struct PagedMembersResponse { - /// The members alongside their family heads. - pub members: Vec<(IdentityKey, FamilyHead)>, - - /// Field indicating paging information for the following queries if the caller wishes to get further entries. - pub start_next_after: Option, -} - -/// Response containing family information. -#[cw_serde] -pub struct FamilyByHeadResponse { - /// The family head used for the query. - pub head: FamilyHead, - - /// If applicable, the family associated with the provided head. - pub family: Option, -} - -/// Response containing family information. -#[cw_serde] -pub struct FamilyByLabelResponse { - /// The family label used for the query. - pub label: String, - - /// If applicable, the family associated with the provided label. - pub family: Option, -} - -/// Response containing family members information. -#[cw_serde] -pub struct FamilyMembersByHeadResponse { - /// The family head used for the query. - pub head: FamilyHead, - - /// All members belonging to the specified family. - pub members: Vec, -} - -/// Response containing family members information. -#[cw_serde] -pub struct FamilyMembersByLabelResponse { - /// The family label used for the query. - pub label: String, - - /// All members belonging to the specified family. - pub members: Vec, -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn family_head_serde() { - let dummy = FamilyHead::new("foomp"); - - let ser_str = serde_json_wasm::to_string(&dummy).unwrap(); - let de_str: FamilyHead = serde_json_wasm::from_str(&ser_str).unwrap(); - assert_eq!(dummy, de_str); - - let ser_bytes = serde_json_wasm::to_vec(&dummy).unwrap(); - let de_bytes: FamilyHead = serde_json_wasm::from_slice(&ser_bytes).unwrap(); - assert_eq!(dummy, de_bytes); - } -} diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/gateway.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/gateway.rs index 9d75b66c22..f819bedb50 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/gateway.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/gateway.rs @@ -1,7 +1,7 @@ // Copyright 2021-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::{IdentityKey, SphinxKey}; +use crate::{IdentityKey, NodeId, SphinxKey}; use cosmwasm_schema::cw_serde; use cosmwasm_std::{Addr, Coin}; use std::cmp::Ordering; @@ -200,6 +200,23 @@ pub struct GatewayBondResponse { pub gateway: Option, } +#[cw_serde] +pub struct PreassignedId { + /// The identity key (base58-encoded ed25519 public key) of the gateway. + pub identity: IdentityKey, + + /// The id pre-assigned to this gateway + pub node_id: NodeId, +} + +#[cw_serde] +pub struct PreassignedGatewayIdsResponse { + pub ids: Vec, + + /// Field indicating paging information for the following queries if the caller wishes to get further entries. + pub start_next_after: Option, +} + #[cfg(test)] mod tests { use super::*; diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/helpers.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/helpers.rs index fc0a05892b..31e8e820d4 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/helpers.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/helpers.rs @@ -1,7 +1,14 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use cosmwasm_std::{Decimal, StdError, StdResult, Uint128}; +use crate::error::MixnetContractError; +use crate::mixnode::PendingMixNodeChanges; +use crate::{ + EpochEventId, IntervalEventId, MixNodeBond, MixNodeDetails, NodeId, NodeRewarding, NymNodeBond, + NymNodeDetails, PendingNodeChanges, +}; +use contracts_common::IdentityKeyRef; +use cosmwasm_std::{Coin, Decimal, StdError, StdResult, Uint128}; pub fn compare_decimals(a: Decimal, b: Decimal, epsilon: Option) { let epsilon = epsilon.unwrap_or_else(|| Decimal::from_ratio(1u128, 100_000_000u128)); @@ -31,3 +38,158 @@ where }) } } + +pub trait NodeDetails { + type Bond: NodeBond; + type PendingChanges: PendingChanges; + + fn split(self) -> (Self::Bond, NodeRewarding, Self::PendingChanges); + fn rewarding_info(&self) -> &NodeRewarding; + fn bond_info(&self) -> &Self::Bond; + fn pending_changes(&self) -> &Self::PendingChanges; +} + +pub trait NodeBond { + fn node_id(&self) -> NodeId; + + fn is_unbonding(&self) -> bool; + + fn identity(&self) -> IdentityKeyRef; + + fn original_pledge(&self) -> &Coin; + + fn ensure_bonded(&self) -> Result<(), MixnetContractError> { + if self.is_unbonding() { + return Err(MixnetContractError::NodeIsUnbonding { + node_id: self.node_id(), + }); + } + Ok(()) + } +} + +pub trait PendingChanges { + fn pending_pledge_changes(&self) -> Option; + + fn pending_cost_params_changes(&self) -> Option; + + fn ensure_no_pending_pledge_changes(&self) -> Result<(), MixnetContractError> { + if let Some(pending_event_id) = self.pending_pledge_changes() { + return Err(MixnetContractError::PendingPledgeChange { pending_event_id }); + } + Ok(()) + } + + fn ensure_no_pending_params_changes(&self) -> Result<(), MixnetContractError> { + if let Some(pending_event_id) = self.pending_cost_params_changes() { + return Err(MixnetContractError::PendingParamsChange { pending_event_id }); + } + Ok(()) + } +} + +impl NodeDetails for MixNodeDetails { + type Bond = MixNodeBond; + type PendingChanges = PendingMixNodeChanges; + + fn split(self) -> (Self::Bond, NodeRewarding, Self::PendingChanges) { + ( + self.bond_information, + self.rewarding_details, + self.pending_changes, + ) + } + + fn rewarding_info(&self) -> &NodeRewarding { + &self.rewarding_details + } + + fn bond_info(&self) -> &Self::Bond { + &self.bond_information + } + + fn pending_changes(&self) -> &Self::PendingChanges { + &self.pending_changes + } +} + +impl NodeBond for MixNodeBond { + fn node_id(&self) -> NodeId { + self.mix_id + } + + fn is_unbonding(&self) -> bool { + self.is_unbonding + } + + fn identity(&self) -> IdentityKeyRef { + self.identity() + } + + fn original_pledge(&self) -> &Coin { + self.original_pledge() + } +} + +impl PendingChanges for PendingMixNodeChanges { + fn pending_pledge_changes(&self) -> Option { + self.pledge_change + } + + fn pending_cost_params_changes(&self) -> Option { + self.cost_params_change + } +} + +impl NodeDetails for NymNodeDetails { + type Bond = NymNodeBond; + type PendingChanges = PendingNodeChanges; + + fn split(self) -> (Self::Bond, NodeRewarding, Self::PendingChanges) { + ( + self.bond_information, + self.rewarding_details, + self.pending_changes, + ) + } + + fn rewarding_info(&self) -> &NodeRewarding { + &self.rewarding_details + } + + fn bond_info(&self) -> &Self::Bond { + &self.bond_information + } + + fn pending_changes(&self) -> &Self::PendingChanges { + &self.pending_changes + } +} + +impl NodeBond for NymNodeBond { + fn node_id(&self) -> NodeId { + self.node_id + } + + fn is_unbonding(&self) -> bool { + self.is_unbonding + } + + fn identity(&self) -> IdentityKeyRef { + self.identity() + } + + fn original_pledge(&self) -> &Coin { + &self.original_pledge + } +} + +impl PendingChanges for PendingNodeChanges { + fn pending_pledge_changes(&self) -> Option { + self.pledge_change + } + + fn pending_cost_params_changes(&self) -> Option { + self.cost_params_change + } +} diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/interval.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/interval.rs index d5382a29e6..067a3aa50e 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/interval.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/interval.rs @@ -2,7 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 use crate::error::MixnetContractError; -use crate::MixId; +use crate::nym_node::Role; +use crate::NodeId; use cosmwasm_schema::cw_serde; use cosmwasm_schema::schemars::gen::SchemaGenerator; use cosmwasm_schema::schemars::schema::{InstanceType, Schema, SchemaObject}; @@ -86,7 +87,7 @@ impl EpochStatus { pub fn update_last_rewarded( &mut self, - new_last_rewarded: MixId, + new_last_rewarded: NodeId, ) -> Result { match &mut self.state { EpochState::Rewarding { @@ -109,7 +110,7 @@ impl EpochStatus { } } - pub fn last_rewarded(&self) -> Result { + pub fn last_rewarded(&self) -> Result { match self.state { EpochState::Rewarding { last_rewarded, .. } => Ok(last_rewarded), state => Err(MixnetContractError::UnexpectedNonRewardingEpochState { @@ -127,12 +128,23 @@ impl EpochStatus { Ok(()) } - pub fn ensure_is_in_advancement_state(&self) -> Result<(), MixnetContractError> { - if !matches!(self.state, EpochState::AdvancingEpoch) { - return Err(MixnetContractError::EpochNotInAdvancementState { + pub fn ensure_is_in_expected_role_assignment_state( + &self, + caller: Role, + ) -> Result<(), MixnetContractError> { + let EpochState::RoleAssignment { next } = self.state else { + return Err(MixnetContractError::EpochNotInRoleAssignmentState { current_state: self.state, }); + }; + + if caller != next { + return Err(MixnetContractError::UnexpectedRoleAssignment { + expected: next, + got: caller, + }); } + Ok(()) } @@ -147,10 +159,6 @@ impl EpochStatus { pub fn is_reconciling(&self) -> bool { matches!(self.state, EpochState::ReconcilingEvents) } - - pub fn is_advancing(&self) -> bool { - matches!(self.state, EpochState::AdvancingEpoch) - } } /// The state of the current rewarding epoch. @@ -167,10 +175,10 @@ pub enum EpochState { #[serde(alias = "Rewarding")] Rewarding { /// The id of the last node that has already received its rewards. - last_rewarded: MixId, + last_rewarded: NodeId, /// The id of the last node that's going to be rewarded before progressing into the next state. - final_node_id: MixId, + final_node_id: NodeId, // total_rewarded: u32, }, @@ -179,10 +187,9 @@ pub enum EpochState { #[serde(alias = "ReconcilingEvents")] ReconcilingEvents, - /// Represents the state of an epoch when all mixnodes have already been rewarded for their work in this epoch, - /// all issued actions got resolved and the epoch should now be advanced whilst assigning new rewarded set. - #[serde(alias = "AdvancingEpoch")] - AdvancingEpoch, + /// Represents the state of an epoch when all nodes have already been rewarded for their work in this epoch, + /// all issued actions got resolved and node roles should now be assigned before advancing into the next epoch. + RoleAssignment { next: Role }, } impl Display for EpochState { @@ -197,7 +204,9 @@ impl Display for EpochState { "mix rewarding (last rewarded: {last_rewarded}, final node: {final_node_id})" ), EpochState::ReconcilingEvents => write!(f, "event reconciliation"), - EpochState::AdvancingEpoch => write!(f, "advancing epoch"), + EpochState::RoleAssignment { next } => { + write!(f, "role assignment with next assignment for: {next}") + } } } } @@ -359,6 +368,17 @@ impl Interval { self.current_epoch_end_unix_timestamp() <= env.block.time.seconds() as i64 } + pub fn ensure_current_epoch_is_over(&self, env: &Env) -> Result<(), MixnetContractError> { + if !self.is_current_epoch_over(&env) { + return Err(MixnetContractError::EpochInProgress { + current_block_time: env.block.time.seconds(), + epoch_start: self.current_epoch_start_unix_timestamp(), + epoch_end: self.current_epoch_end_unix_timestamp(), + }); + } + Ok(()) + } + pub fn secs_until_current_epoch_end(&self, env: &Env) -> i64 { if self.is_current_epoch_over(env) { 0 diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/lib.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/lib.rs index 50a5e90aec..fca9f6bfcf 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/lib.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/lib.rs @@ -3,32 +3,30 @@ #![warn(clippy::expect_used)] #![warn(clippy::unwrap_used)] +#![warn(clippy::todo)] -mod constants; +pub mod constants; pub mod delegation; pub mod error; pub mod events; -pub mod families; pub mod gateway; pub mod helpers; pub mod interval; pub mod mixnode; pub mod msg; +pub mod nym_node; pub mod pending_events; pub mod reward_params; pub mod rewarding; pub mod signing_types; pub mod types; +pub use constants::*; pub use contracts_common::types::*; pub use cosmwasm_std::{Addr, Coin, Decimal, Fraction}; pub use delegation::{ Delegation, PagedAllDelegationsResponse, PagedDelegatorDelegationsResponse, - PagedMixNodeDelegationsResponse, -}; -pub use families::{ - Family, FamilyByHeadResponse, FamilyByLabelResponse, FamilyHead, FamilyMembersByHeadResponse, - FamilyMembersByLabelResponse, PagedFamiliesResponse, PagedMembersResponse, + PagedNodeDelegationsResponse, }; pub use gateway::{ Gateway, GatewayBond, GatewayBondResponse, GatewayConfigUpdate, GatewayOwnershipResponse, @@ -38,11 +36,12 @@ pub use interval::{ CurrentIntervalResponse, EpochId, EpochState, EpochStatus, Interval, IntervalId, }; pub use mixnode::{ - Layer, MixNode, MixNodeBond, MixNodeConfigUpdate, MixNodeCostParams, MixNodeDetails, - MixNodeRewarding, MixOwnershipResponse, MixnodeDetailsByIdentityResponse, - MixnodeDetailsResponse, PagedMixnodeBondsResponse, RewardedSetNodeStatus, UnbondedMixnode, + LegacyMixLayer, MixNode, MixNodeBond, MixNodeConfigUpdate, MixNodeDetails, + MixOwnershipResponse, MixnodeDetailsByIdentityResponse, MixnodeDetailsResponse, NodeCostParams, + NodeRewarding, PagedMixnodeBondsResponse, UnbondedMixnode, }; pub use msg::*; +pub use nym_node::{NymNode, NymNodeBond, NymNodeDetails, PendingNodeChanges}; pub use pending_events::{ EpochEventId, IntervalEventId, NumberOfPendingEventsResponse, PendingEpochEvent, PendingEpochEventData, PendingEpochEventKind, PendingEpochEventResponse, @@ -50,8 +49,6 @@ pub use pending_events::{ PendingIntervalEventKind, PendingIntervalEventResponse, PendingIntervalEventsResponse, }; pub use reward_params::{IntervalRewardParams, IntervalRewardingParamsUpdate, RewardingParams}; -pub use rewarding::{ - EstimatedCurrentEpochRewardResponse, PagedRewardedSetResponse, PendingRewardResponse, -}; +pub use rewarding::{EstimatedCurrentEpochRewardResponse, PendingRewardResponse}; pub use signing_types::*; pub use types::*; diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs index 8772763150..779df2e125 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs @@ -7,42 +7,18 @@ use crate::constants::{TOKEN_SUPPLY, UNIT_DELEGATION_BASE}; use crate::error::MixnetContractError; use crate::helpers::IntoBaseDecimal; -use crate::reward_params::{NodeRewardParams, RewardingParams}; +use crate::reward_params::{NodeRewardingParameters, RewardingParams}; use crate::rewarding::helpers::truncate_reward; use crate::rewarding::RewardDistribution; use crate::{ - Delegation, EpochEventId, EpochId, IdentityKey, MixId, OperatingCostRange, Percent, - ProfitMarginRange, SphinxKey, + Delegation, EpochEventId, EpochId, IdentityKey, IntervalEventId, NodeId, OperatingCostRange, + Percent, ProfitMarginRange, SphinxKey, }; use cosmwasm_schema::cw_serde; use cosmwasm_std::{Addr, Coin, Decimal, StdResult, Uint128}; use schemars::JsonSchema; use serde_repr::{Deserialize_repr, Serialize_repr}; -/// Current state of given node in the rewarded set. -#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] -#[cfg_attr( - feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/RewardedSetNodeStatus.ts") -)] -#[cw_serde] -#[derive(Copy)] -pub enum RewardedSetNodeStatus { - /// Node that is currently active, i.e. is expected to be used by clients for mixing packets. - #[serde(alias = "Active")] - Active, - - /// Node that is currently in standby, i.e. it's present in the rewarded set but is not active. - #[serde(alias = "Standby")] - Standby, -} - -impl RewardedSetNodeStatus { - pub fn is_active(&self) -> bool { - matches!(self, RewardedSetNodeStatus::Active) - } -} - /// Full details associated with given mixnode. #[cw_serde] pub struct MixNodeDetails { @@ -50,7 +26,7 @@ pub struct MixNodeDetails { pub bond_information: MixNodeBond, /// Details used for computation of rewarding related data. - pub rewarding_details: MixNodeRewarding, + pub rewarding_details: NodeRewarding, /// Adjustments to the mixnode that are ought to happen during future epoch transitions. #[serde(default)] @@ -60,7 +36,7 @@ pub struct MixNodeDetails { impl MixNodeDetails { pub fn new( bond_information: MixNodeBond, - rewarding_details: MixNodeRewarding, + rewarding_details: NodeRewarding, pending_changes: PendingMixNodeChanges, ) -> Self { MixNodeDetails { @@ -70,14 +46,10 @@ impl MixNodeDetails { } } - pub fn mix_id(&self) -> MixId { + pub fn mix_id(&self) -> NodeId { self.bond_information.mix_id } - pub fn layer(&self) -> Layer { - self.bond_information.layer - } - pub fn is_unbonding(&self) -> bool { self.bond_information.is_unbonding } @@ -106,10 +78,11 @@ impl MixNodeDetails { } } +// currently this struct is shared between mixnodes and nymnodes #[cw_serde] -pub struct MixNodeRewarding { +pub struct NodeRewarding { /// Information provided by the operator that influence the cost function. - pub cost_params: MixNodeCostParams, + pub cost_params: NodeCostParams, /// Total pledge and compounded reward earned by the node operator. pub operator: Decimal, @@ -120,7 +93,7 @@ pub struct MixNodeRewarding { /// Cumulative reward earned by the "unit delegation" since the block 0. pub total_unit_reward: Decimal, - /// Value of the theoretical "unit delegation" that has delegated to this mixnode at block 0. + /// Value of the theoretical "unit delegation" that has delegated to this node at block 0. pub unit_delegation: Decimal, /// Marks the epoch when this node was last rewarded so that we wouldn't accidentally attempt @@ -133,9 +106,9 @@ pub struct MixNodeRewarding { pub unique_delegations: u32, } -impl MixNodeRewarding { +impl NodeRewarding { pub fn initialise_new( - cost_params: MixNodeCostParams, + cost_params: NodeCostParams, initial_pledge: &Coin, current_epoch: EpochId, ) -> Result { @@ -144,7 +117,7 @@ impl MixNodeRewarding { "pledge cannot be larger than the token supply" ); - Ok(MixNodeRewarding { + Ok(NodeRewarding { cost_params, operator: initial_pledge.amount.into_base_decimal()?, delegates: Decimal::zero(), @@ -155,6 +128,15 @@ impl MixNodeRewarding { }) } + pub fn normalise_cost_function( + &mut self, + allowed_profit_margin: ProfitMarginRange, + allowed_operating_cost: OperatingCostRange, + ) { + self.normalise_profit_margin(allowed_profit_margin); + self.normalise_operating_cost(allowed_operating_cost) + } + pub fn normalise_profit_margin(&mut self, allowed_range: ProfitMarginRange) { self.cost_params.profit_margin_percent = allowed_range.normalise(self.cost_params.profit_margin_percent) @@ -257,23 +239,18 @@ impl MixNodeRewarding { pub fn node_reward( &self, - reward_params: &RewardingParams, - node_params: NodeRewardParams, + global_params: &RewardingParams, + node_params: NodeRewardingParameters, ) -> Decimal { - let work = if node_params.in_active_set { - reward_params.active_node_work() - } else { - reward_params.standby_node_work() - }; + let work = node_params.work_factor; + let alpha = global_params.interval.sybil_resistance; - let alpha = reward_params.interval.sybil_resistance; - - reward_params.interval.epoch_reward_budget - * node_params.performance.value() - * self.bond_saturation(reward_params) + global_params.interval.epoch_reward_budget + * node_params.performance + * self.bond_saturation(global_params) * (work - + alpha.value() * self.pledge_saturation(reward_params) - / reward_params.dec_rewarded_set_size()) + + alpha.value() * self.pledge_saturation(global_params) + / global_params.dec_rewarded_set_size()) / (Decimal::one() + alpha.value()) } @@ -285,7 +262,7 @@ impl MixNodeRewarding { epochs_in_interval: u32, ) -> RewardDistribution { let node_cost = - self.cost_params.epoch_operating_cost(epochs_in_interval) * node_performance.value(); + self.cost_params.epoch_operating_cost(epochs_in_interval) * node_performance; // check if profit is positive if node_reward > node_cost { @@ -315,7 +292,7 @@ impl MixNodeRewarding { pub fn calculate_epoch_reward( &self, reward_params: &RewardingParams, - node_params: NodeRewardParams, + node_params: NodeRewardingParameters, epochs_in_interval: u32, ) -> RewardDistribution { let node_reward = self.node_reward(reward_params, node_params); @@ -341,7 +318,7 @@ impl MixNodeRewarding { pub fn epoch_rewarding( &mut self, reward_params: &RewardingParams, - node_params: NodeRewardParams, + node_params: NodeRewardingParameters, epochs_in_interval: u32, absolute_epoch_id: EpochId, ) { @@ -492,13 +469,30 @@ impl MixNodeRewarding { amount / self.delegates } } + + /// Returns a copy of `Self` with zeroed operator value + pub fn clear_operator(&self) -> NodeRewarding { + let mut zeroed = self.clone(); + zeroed.operator = Decimal::zero(); + zeroed + } } /// Basic mixnode information provided by the node operator. -#[cw_serde] +// note: we had to remove `#[cw_serde]` as it enforces `#[serde(deny_unknown_fields)]` which we do not want +// with the removal of explicit .layer field +#[derive( + ::cosmwasm_schema::serde::Serialize, + ::cosmwasm_schema::serde::Deserialize, + ::std::clone::Clone, + ::std::fmt::Debug, + ::std::cmp::PartialEq, + ::cosmwasm_schema::schemars::JsonSchema, +)] +#[schemars(crate = "::cosmwasm_schema::schemars")] pub struct MixNodeBond { /// Unique id assigned to the bonded mixnode. - pub mix_id: MixId, + pub mix_id: NodeId, /// Address of the owner of this mixnode. pub owner: Addr, @@ -506,9 +500,9 @@ pub struct MixNodeBond { /// Original amount pledged by the operator of this node. pub original_pledge: Coin, - /// Layer assigned to this mixnode. - pub layer: Layer, - + // REMOVED (but might be needed due to legacy things, idk yet) + // /// Layer assigned to this mixnode. + // pub layer: Layer, /// Information provided by the operator for the purposes of bonding. pub mix_node: MixNode, @@ -525,26 +519,6 @@ pub struct MixNodeBond { } impl MixNodeBond { - pub fn new( - mix_id: MixId, - owner: Addr, - original_pledge: Coin, - layer: Layer, - mix_node: MixNode, - bonding_height: u64, - ) -> Self { - MixNodeBond { - mix_id, - owner, - original_pledge, - layer, - mix_node, - proxy: None, - bonding_height, - is_unbonding: false, - } - } - pub fn identity(&self) -> &str { &self.mix_node.identity_key } @@ -595,21 +569,21 @@ pub struct MixNode { /// The cost parameters, or the cost function, defined for the particular mixnode that influences /// how the rewards should be split between the node operator and its delegators. #[cw_serde] -pub struct MixNodeCostParams { - /// The profit margin of the associated mixnode, i.e. the desired percent of the reward to be distributed to the operator. +pub struct NodeCostParams { + /// The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator. pub profit_margin_percent: Percent, - /// Operating cost of the associated mixnode per the entire interval. + /// Operating cost of the associated node per the entire interval. pub interval_operating_cost: Coin, } -impl MixNodeCostParams { +impl NodeCostParams { pub fn to_inline_json(&self) -> String { serde_json_wasm::to_string(self).unwrap_or_else(|_| "serialisation failure".into()) } } -impl MixNodeCostParams { +impl NodeCostParams { pub fn epoch_operating_cost(&self, epochs_in_interval: u32) -> Decimal { Decimal::from_ratio(self.interval_operating_cost.amount, epochs_in_interval) } @@ -628,38 +602,39 @@ impl MixNodeCostParams { Deserialize_repr, JsonSchema, )] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[repr(u8)] -pub enum Layer { +pub enum LegacyMixLayer { One = 1, Two = 2, Three = 3, } -impl From for String { - fn from(layer: Layer) -> Self { +impl From for String { + fn from(layer: LegacyMixLayer) -> Self { (layer as u8).to_string() } } -impl TryFrom for Layer { +impl TryFrom for LegacyMixLayer { type Error = MixnetContractError; - fn try_from(i: u8) -> Result { + fn try_from(i: u8) -> Result { match i { - 1 => Ok(Layer::One), - 2 => Ok(Layer::Two), - 3 => Ok(Layer::Three), + 1 => Ok(LegacyMixLayer::One), + 2 => Ok(LegacyMixLayer::Two), + 3 => Ok(LegacyMixLayer::Three), _ => Err(MixnetContractError::InvalidLayer(i)), } } } -impl From for u8 { - fn from(layer: Layer) -> u8 { +impl From for u8 { + fn from(layer: LegacyMixLayer) -> u8 { match layer { - Layer::One => 1, - Layer::Two => 2, - Layer::Three => 3, + LegacyMixLayer::One => 1, + LegacyMixLayer::Two => 2, + LegacyMixLayer::Three => 3, } } } @@ -673,13 +648,15 @@ impl From for u8 { #[derive(Default, Copy)] pub struct PendingMixNodeChanges { pub pledge_change: Option, - // pub cost_params_change: Option, + #[serde(default)] + pub cost_params_change: Option, } impl PendingMixNodeChanges { pub fn new_empty() -> PendingMixNodeChanges { PendingMixNodeChanges { pledge_change: None, + cost_params_change: None, } } } @@ -740,11 +717,11 @@ pub struct PagedMixnodeBondsResponse { pub per_page: usize, /// Field indicating paging information for the following queries if the caller wishes to get further entries. - pub start_next_after: Option, + pub start_next_after: Option, } impl PagedMixnodeBondsResponse { - pub fn new(nodes: Vec, per_page: usize, start_next_after: Option) -> Self { + pub fn new(nodes: Vec, per_page: usize, start_next_after: Option) -> Self { PagedMixnodeBondsResponse { nodes, per_page, @@ -766,14 +743,14 @@ pub struct PagedMixnodesDetailsResponse { pub per_page: usize, /// Field indicating paging information for the following queries if the caller wishes to get further entries. - pub start_next_after: Option, + pub start_next_after: Option, } impl PagedMixnodesDetailsResponse { pub fn new( nodes: Vec, per_page: usize, - start_next_after: Option, + start_next_after: Option, ) -> Self { PagedMixnodesDetailsResponse { nodes, @@ -787,21 +764,21 @@ impl PagedMixnodesDetailsResponse { #[cw_serde] pub struct PagedUnbondedMixnodesResponse { /// The past ids of unbonded mixnodes alongside their basic information such as the owner or the identity key. - pub nodes: Vec<(MixId, UnbondedMixnode)>, + pub nodes: Vec<(NodeId, UnbondedMixnode)>, /// Maximum number of entries that could be included in a response. `per_page <= nodes.len()` // this field is rather redundant and should be deprecated. pub per_page: usize, /// Field indicating paging information for the following queries if the caller wishes to get further entries. - pub start_next_after: Option, + pub start_next_after: Option, } impl PagedUnbondedMixnodesResponse { pub fn new( - nodes: Vec<(MixId, UnbondedMixnode)>, + nodes: Vec<(NodeId, UnbondedMixnode)>, per_page: usize, - start_next_after: Option, + start_next_after: Option, ) -> Self { PagedUnbondedMixnodesResponse { nodes, @@ -825,7 +802,7 @@ pub struct MixOwnershipResponse { #[cw_serde] pub struct MixnodeDetailsResponse { /// Id of the requested mixnode. - pub mix_id: MixId, + pub mix_id: NodeId, /// If there exists a mixnode with the provided id, this field contains its detailed information. pub mixnode_details: Option, @@ -845,17 +822,17 @@ pub struct MixnodeDetailsByIdentityResponse { #[cw_serde] pub struct MixnodeRewardingDetailsResponse { /// Id of the requested mixnode. - pub mix_id: MixId, + pub mix_id: NodeId, /// If there exists a mixnode with the provided id, this field contains its rewarding information. - pub rewarding_details: Option, + pub rewarding_details: Option, } /// Response containing basic information of an unbonded mixnode with the provided id. #[cw_serde] pub struct UnbondedMixnodeResponse { /// Id of the requested mixnode. - pub mix_id: MixId, + pub mix_id: NodeId, /// If there existed a mixnode with the provided id, this field contains its basic information. pub unbonded_info: Option, @@ -863,9 +840,9 @@ pub struct UnbondedMixnodeResponse { /// Response containing the current state of the stake saturation of a mixnode with the provided id. #[cw_serde] -pub struct StakeSaturationResponse { +pub struct MixStakeSaturationResponse { /// Id of the requested mixnode. - pub mix_id: MixId, + pub mix_id: NodeId, /// The current stake saturation of this node that is indirectly used in reward calculation formulas. /// Note that it can't be larger than 1. diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs index 863fdc0327..39a81b93c5 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs @@ -3,15 +3,17 @@ use crate::delegation::{self, OwnerProxySubKey}; use crate::error::MixnetContractError; -use crate::families::FamilyHead; use crate::gateway::{Gateway, GatewayConfigUpdate}; use crate::helpers::IntoBaseDecimal; -use crate::mixnode::{Layer, MixNode, MixNodeConfigUpdate, MixNodeCostParams}; +use crate::mixnode::{MixNode, MixNodeConfigUpdate, NodeCostParams}; +use crate::nym_node::{NodeConfigUpdate, Role}; use crate::pending_events::{EpochEventId, IntervalEventId}; use crate::reward_params::{ - IntervalRewardParams, IntervalRewardingParamsUpdate, Performance, RewardingParams, + ActiveSetUpdate, IntervalRewardParams, IntervalRewardingParamsUpdate, NodeRewardingParameters, + Performance, RewardedSetParams, RewardingParams, WorkFactor, }; -use crate::types::{ContractStateParams, LayerAssignment, MixId}; +use crate::types::{ContractStateParams, NodeId}; +use crate::{NymNode, RoleAssignment}; use crate::{OperatingCostRange, ProfitMarginRange}; use contracts_common::{signing::MessageSignature, IdentityKey, Percent}; use cosmwasm_schema::cw_serde; @@ -21,28 +23,31 @@ use std::time::Duration; #[cfg(feature = "schema")] use crate::{ delegation::{ - MixNodeDelegationResponse, PagedAllDelegationsResponse, PagedDelegatorDelegationsResponse, - PagedMixNodeDelegationsResponse, + NodeDelegationResponse, PagedAllDelegationsResponse, PagedDelegatorDelegationsResponse, + PagedNodeDelegationsResponse, }, - families::{ - FamilyByHeadResponse, FamilyByLabelResponse, FamilyMembersByHeadResponse, - FamilyMembersByLabelResponse, PagedFamiliesResponse, PagedMembersResponse, + gateway::{ + GatewayBondResponse, GatewayOwnershipResponse, PagedGatewayResponse, + PreassignedGatewayIdsResponse, }, - gateway::{GatewayBondResponse, GatewayOwnershipResponse, PagedGatewayResponse}, interval::{CurrentIntervalResponse, EpochStatus}, mixnode::{ - MixOwnershipResponse, MixnodeDetailsByIdentityResponse, MixnodeDetailsResponse, - MixnodeRewardingDetailsResponse, PagedMixnodeBondsResponse, PagedMixnodesDetailsResponse, - PagedUnbondedMixnodesResponse, StakeSaturationResponse, UnbondedMixnodeResponse, + MixOwnershipResponse, MixStakeSaturationResponse, MixnodeDetailsByIdentityResponse, + MixnodeDetailsResponse, MixnodeRewardingDetailsResponse, PagedMixnodeBondsResponse, + PagedMixnodesDetailsResponse, PagedUnbondedMixnodesResponse, UnbondedMixnodeResponse, + }, + nym_node::{ + EpochAssignmentResponse, NodeDetailsByIdentityResponse, NodeDetailsResponse, + NodeOwnershipResponse, NodeRewardingDetailsResponse, PagedNymNodeBondsResponse, + PagedNymNodeDetailsResponse, PagedUnbondedNymNodesResponse, RolesMetadataResponse, + StakeSaturationResponse, UnbondedNodeResponse, }, pending_events::{ NumberOfPendingEventsResponse, PendingEpochEventResponse, PendingEpochEventsResponse, PendingIntervalEventResponse, PendingIntervalEventsResponse, }, - rewarding::{ - EstimatedCurrentEpochRewardResponse, PagedRewardedSetResponse, PendingRewardResponse, - }, - types::{ContractState, LayerDistribution}, + rewarding::{EstimatedCurrentEpochRewardResponse, PendingRewardResponse}, + types::ContractState, }; #[cfg(feature = "schema")] use contracts_common::{signing::Nonce, ContractBuildInformation}; @@ -76,8 +81,7 @@ pub struct InitialRewardingParams { pub active_set_work_factor: Decimal, pub interval_pool_emission: Percent, - pub rewarded_set_size: u32, - pub active_set_size: u32, + pub rewarded_set_params: RewardedSetParams, } impl InitialRewardingParams { @@ -88,8 +92,11 @@ impl InitialRewardingParams { let epoch_reward_budget = self.initial_reward_pool / epochs_in_interval.into_base_decimal()? * self.interval_pool_emission; - let stake_saturation_point = - self.initial_staking_supply / self.rewarded_set_size.into_base_decimal()?; + let stake_saturation_point = self.initial_staking_supply + / self + .rewarded_set_params + .rewarded_set_size() + .into_base_decimal()?; Ok(RewardingParams { interval: IntervalRewardParams { @@ -102,8 +109,7 @@ impl InitialRewardingParams { active_set_work_factor: self.active_set_work_factor, interval_pool_emission: self.interval_pool_emission, }, - rewarded_set_size: self.rewarded_set_size, - active_set_size: self.active_set_size, + rewarded_set: self.rewarded_set_params, }) } } @@ -115,45 +121,6 @@ pub enum ExecuteMsg { admin: String, }, - AssignNodeLayer { - mix_id: MixId, - layer: Layer, - }, - // Families - /// Only owner of the node can crate the family with node as head - CreateFamily { - label: String, - }, - /// Family head needs to sign the joining node IdentityKey - JoinFamily { - join_permit: MessageSignature, - family_head: FamilyHead, - }, - LeaveFamily { - family_head: FamilyHead, - }, - KickFamilyMember { - member: IdentityKey, - }, - CreateFamilyOnBehalf { - owner_address: String, - label: String, - }, - /// Family head needs to sign the joining node IdentityKey, MixNode needs to provide its signature proving that it wants to join the family - JoinFamilyOnBehalf { - member_address: String, - join_permit: MessageSignature, - family_head: FamilyHead, - }, - LeaveFamilyOnBehalf { - member_address: String, - family_head: FamilyHead, - }, - KickFamilyMemberOnBehalf { - head_address: String, - member: IdentityKey, - }, - // state/sys-params-related UpdateRewardingValidatorAddress { address: String, @@ -161,8 +128,8 @@ pub enum ExecuteMsg { UpdateContractStateParams { updated_parameters: ContractStateParams, }, - UpdateActiveSetSize { - active_set_size: u32, + UpdateActiveSetDistribution { + update: ActiveSetUpdate, force_immediately: bool, }, UpdateRewardingParams { @@ -174,25 +141,24 @@ pub enum ExecuteMsg { epoch_duration_secs: u64, force_immediately: bool, }, + BeginEpochTransition {}, - AdvanceCurrentEpoch { - new_rewarded_set: Vec, - // families_in_layer: HashMap, - expected_active_set_size: u32, - }, ReconcileEpochEvents { limit: Option, }, + AssignRoles { + assignment: RoleAssignment, + }, // mixnode-related: BondMixnode { mix_node: MixNode, - cost_params: MixNodeCostParams, + cost_params: NodeCostParams, owner_signature: MessageSignature, }, BondMixnodeOnBehalf { mix_node: MixNode, - cost_params: MixNodeCostParams, + cost_params: NodeCostParams, owner_signature: MessageSignature, owner: String, }, @@ -211,11 +177,15 @@ pub enum ExecuteMsg { UnbondMixnodeOnBehalf { owner: String, }, - UpdateMixnodeCostParams { - new_costs: MixNodeCostParams, + #[serde( + alias = "UpdateMixnodeCostParams", + alias = "update_mixnode_cost_params" + )] + UpdateCostParams { + new_costs: NodeCostParams, }, UpdateMixnodeCostParamsOnBehalf { - new_costs: MixNodeCostParams, + new_costs: NodeCostParams, owner: String, }, UpdateMixnodeConfig { @@ -225,6 +195,7 @@ pub enum ExecuteMsg { new_config: MixNodeConfigUpdate, owner: String, }, + MigrateMixnode {}, // gateway-related: BondGateway { @@ -247,44 +218,64 @@ pub enum ExecuteMsg { new_config: GatewayConfigUpdate, owner: String, }, + MigrateGateway { + cost_params: Option, + }, + + // nym-node related: + BondNymNode { + node: NymNode, + cost_params: NodeCostParams, + owner_signature: MessageSignature, + }, + UnbondNymNode {}, + UpdateNodeConfig { + update: NodeConfigUpdate, + }, // delegation-related: - DelegateToMixnode { - mix_id: MixId, + #[serde(alias = "DelegateToMixnode", alias = "delegate_to_mixnode")] + Delegate { + #[serde(alias = "mix_id")] + node_id: NodeId, }, DelegateToMixnodeOnBehalf { - mix_id: MixId, + mix_id: NodeId, delegate: String, }, - UndelegateFromMixnode { - mix_id: MixId, + #[serde(alias = "UndelegateFromMixnode", alias = "undelegate_from_mixnode")] + Undelegate { + #[serde(alias = "mix_id")] + node_id: NodeId, }, UndelegateFromMixnodeOnBehalf { - mix_id: MixId, + mix_id: NodeId, delegate: String, }, // reward-related - RewardMixnode { - mix_id: MixId, - performance: Performance, + RewardNode { + #[serde(alias = "mix_id")] + node_id: NodeId, + params: NodeRewardingParameters, }, WithdrawOperatorReward {}, WithdrawOperatorRewardOnBehalf { owner: String, }, WithdrawDelegatorReward { - mix_id: MixId, + #[serde(alias = "mix_id")] + node_id: NodeId, }, WithdrawDelegatorRewardOnBehalf { - mix_id: MixId, + mix_id: NodeId, owner: String, }, // vesting migration: MigrateVestedMixNode {}, MigrateVestedDelegation { - mix_id: MixId, + mix_id: NodeId, }, // testing-only @@ -292,47 +283,31 @@ pub enum ExecuteMsg { TestingResolveAllPendingEvents { limit: Option, }, + + // TO BE REMOVED BEFORE MERGING + TestingUncheckedBondLegacyMixnode { + node: MixNode, + }, + TestingUncheckedBondLegacyGateway { + node: Gateway, + }, } +const bad_const: &str = "clippy will yell so that i'd remember to remove those legacy bonding ops"; + impl ExecuteMsg { pub fn default_memo(&self) -> String { match self { ExecuteMsg::UpdateAdmin { admin } => format!("updating contract admin to {admin}"), - ExecuteMsg::AssignNodeLayer { mix_id, layer } => { - format!("assigning mix {mix_id} for layer {layer:?}") - } - ExecuteMsg::CreateFamily { .. } => "crating node family with".to_string(), - ExecuteMsg::JoinFamily { family_head, .. } => { - format!("joining family {family_head}") - } - ExecuteMsg::LeaveFamily { family_head, .. } => { - format!("leaving family {family_head}") - } - ExecuteMsg::KickFamilyMember { member, .. } => { - format!("kicking {member} from family") - } - ExecuteMsg::CreateFamilyOnBehalf { .. } => "crating node family with".to_string(), - ExecuteMsg::JoinFamilyOnBehalf { family_head, .. } => { - format!("joining family {family_head}") - } - ExecuteMsg::LeaveFamilyOnBehalf { family_head, .. } => { - format!("leaving family {family_head}") - } - ExecuteMsg::KickFamilyMemberOnBehalf { member, .. } => { - format!("kicking {member} from family") - } ExecuteMsg::UpdateRewardingValidatorAddress { address } => { format!("updating rewarding validator to {address}") } ExecuteMsg::UpdateContractStateParams { .. } => { "updating mixnet state parameters".into() } - ExecuteMsg::UpdateActiveSetSize { - active_set_size, - force_immediately, - } => format!( - "updating active set size to {active_set_size}. forced: {force_immediately}" - ), + ExecuteMsg::UpdateActiveSetDistribution { + force_immediately, .. + } => format!("updating active set distribution. forced: {force_immediately}"), ExecuteMsg::UpdateRewardingParams { force_immediately, .. } => format!("updating mixnet rewarding parameters. forced: {force_immediately}"), @@ -340,7 +315,6 @@ impl ExecuteMsg { force_immediately, .. } => format!("updating mixnet interval configuration. forced: {force_immediately}"), ExecuteMsg::BeginEpochTransition {} => "beginning epoch transition".into(), - ExecuteMsg::AdvanceCurrentEpoch { .. } => "advancing current epoch".into(), ExecuteMsg::ReconcileEpochEvents { .. } => "reconciling epoch events".into(), ExecuteMsg::BondMixnode { mix_node, .. } => { format!("bonding mixnode {}", mix_node.identity_key) @@ -356,7 +330,7 @@ impl ExecuteMsg { } ExecuteMsg::UnbondMixnode { .. } => "unbonding mixnode".into(), ExecuteMsg::UnbondMixnodeOnBehalf { .. } => "unbonding mixnode on behalf".into(), - ExecuteMsg::UpdateMixnodeCostParams { .. } => "updating mixnode cost parameters".into(), + ExecuteMsg::UpdateCostParams { .. } => "updating mixnode cost parameters".into(), ExecuteMsg::UpdateMixnodeCostParamsOnBehalf { .. } => { "updating mixnode cost parameters on behalf".into() } @@ -376,25 +350,22 @@ impl ExecuteMsg { ExecuteMsg::UpdateGatewayConfigOnBehalf { .. } => { "updating gateway configuration on behalf".into() } - ExecuteMsg::DelegateToMixnode { mix_id } => format!("delegating to mixnode {mix_id}"), + ExecuteMsg::Delegate { node_id: mix_id } => format!("delegating to mixnode {mix_id}"), ExecuteMsg::DelegateToMixnodeOnBehalf { mix_id, .. } => { format!("delegating to mixnode {mix_id} on behalf") } - ExecuteMsg::UndelegateFromMixnode { mix_id } => { + ExecuteMsg::Undelegate { node_id: mix_id } => { format!("removing delegation from mixnode {mix_id}") } ExecuteMsg::UndelegateFromMixnodeOnBehalf { mix_id, .. } => { format!("removing delegation from mixnode {mix_id} on behalf") } - ExecuteMsg::RewardMixnode { - mix_id, - performance, - } => format!("rewarding mixnode {mix_id} for performance {performance}"), + ExecuteMsg::RewardNode { node_id, .. } => format!("rewarding node {node_id}"), ExecuteMsg::WithdrawOperatorReward { .. } => "withdrawing operator reward".into(), ExecuteMsg::WithdrawOperatorRewardOnBehalf { .. } => { "withdrawing operator reward on behalf".into() } - ExecuteMsg::WithdrawDelegatorReward { mix_id } => { + ExecuteMsg::WithdrawDelegatorReward { node_id: mix_id } => { format!("withdrawing delegator reward from mixnode {mix_id}") } ExecuteMsg::WithdrawDelegatorRewardOnBehalf { mix_id, .. } => { @@ -402,11 +373,19 @@ impl ExecuteMsg { } ExecuteMsg::MigrateVestedMixNode { .. } => "migrate vested mixnode".into(), ExecuteMsg::MigrateVestedDelegation { .. } => "migrate vested delegation".to_string(), + ExecuteMsg::AssignRoles { .. } => "assigning epoch roles".into(), + ExecuteMsg::MigrateMixnode { .. } => "migrating legacy mixnode".into(), + ExecuteMsg::MigrateGateway { .. } => "migrating legacy gateway".into(), + ExecuteMsg::BondNymNode { .. } => "bonding nym-node".into(), + ExecuteMsg::UnbondNymNode { .. } => "unbonding nym-node".into(), + ExecuteMsg::UpdateNodeConfig { .. } => "updating node config".into(), #[cfg(feature = "contract-testing")] ExecuteMsg::TestingResolveAllPendingEvents { .. } => { "resolving all pending events".into() } + ExecuteMsg::TestingUncheckedBondLegacyMixnode { .. } => "todo!".into(), + ExecuteMsg::TestingUncheckedBondLegacyGateway { .. } => "todo!".into(), } } } @@ -417,43 +396,6 @@ pub enum QueryMsg { #[cfg_attr(feature = "schema", returns(cw_controllers::AdminResponse))] Admin {}, - // families - /// Gets the list of families registered in this contract. - #[cfg_attr(feature = "schema", returns(PagedFamiliesResponse))] - GetAllFamiliesPaged { - /// Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default. - limit: Option, - - /// Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response. - start_after: Option, - }, - - /// Gets the list of all family members registered in this contract. - #[cfg_attr(feature = "schema", returns(PagedMembersResponse))] - GetAllMembersPaged { - /// Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default. - limit: Option, - - /// Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response. - start_after: Option, - }, - - /// Attempts to lookup family information given the family head. - #[cfg_attr(feature = "schema", returns(FamilyByHeadResponse))] - GetFamilyByHead { head: String }, - - /// Attempts to lookup family information given the family label. - #[cfg_attr(feature = "schema", returns(FamilyByLabelResponse))] - GetFamilyByLabel { label: String }, - - /// Attempts to retrieve family members given the family head. - #[cfg_attr(feature = "schema", returns(FamilyMembersByHeadResponse))] - GetFamilyMembersByHead { head: String }, - - /// Attempts to retrieve family members given the family label. - #[cfg_attr(feature = "schema", returns(FamilyMembersByLabelResponse))] - GetFamilyMembersByLabel { label: String }, - // state/sys-params-related /// Gets build information of this contract, such as the commit hash used for the build or rustc version. #[cfg_attr(feature = "schema", returns(ContractBuildInformation))] @@ -488,16 +430,6 @@ pub enum QueryMsg { #[cfg_attr(feature = "schema", returns(CurrentIntervalResponse))] GetCurrentIntervalDetails {}, - /// Gets the current list of mixnodes in the rewarded set. - #[cfg_attr(feature = "schema", returns(PagedRewardedSetResponse))] - GetRewardedSet { - /// Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default. - limit: Option, - - /// Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response. - start_after: Option, - }, - // mixnode-related: /// Gets the basic list of all currently bonded mixnodes. #[cfg_attr(feature = "schema", returns(PagedMixnodeBondsResponse))] @@ -506,7 +438,7 @@ pub enum QueryMsg { limit: Option, /// Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response. - start_after: Option, + start_after: Option, }, /// Gets the detailed list of all currently bonded mixnodes. @@ -516,7 +448,7 @@ pub enum QueryMsg { limit: Option, /// Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response. - start_after: Option, + start_after: Option, }, /// Gets the basic list of all unbonded mixnodes. @@ -526,20 +458,20 @@ pub enum QueryMsg { limit: Option, /// Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response. - start_after: Option, + start_after: Option, }, /// Gets the basic list of all unbonded mixnodes that belonged to a particular owner. #[cfg_attr(feature = "schema", returns(PagedUnbondedMixnodesResponse))] GetUnbondedMixNodesByOwner { - /// The address of the owner of the the mixnodes used for the query. + /// The address of the owner of the mixnodes used for the query. owner: String, /// Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default. limit: Option, /// Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response. - start_after: Option, + start_after: Option, }, /// Gets the basic list of all unbonded mixnodes that used the particular identity key. @@ -552,7 +484,7 @@ pub enum QueryMsg { limit: Option, /// Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response. - start_after: Option, + start_after: Option, }, /// Gets the detailed mixnode information belonging to the particular owner. @@ -566,28 +498,28 @@ pub enum QueryMsg { #[cfg_attr(feature = "schema", returns(MixnodeDetailsResponse))] GetMixnodeDetails { /// Id of the node to query. - mix_id: MixId, + mix_id: NodeId, }, /// Gets the rewarding information of a mixnode with the provided id. #[cfg_attr(feature = "schema", returns(MixnodeRewardingDetailsResponse))] GetMixnodeRewardingDetails { /// Id of the node to query. - mix_id: MixId, + mix_id: NodeId, }, /// Gets the stake saturation of a mixnode with the provided id. - #[cfg_attr(feature = "schema", returns(StakeSaturationResponse))] + #[cfg_attr(feature = "schema", returns(MixStakeSaturationResponse))] GetStakeSaturation { /// Id of the node to query. - mix_id: MixId, + mix_id: NodeId, }, /// Gets the basic information of an unbonded mixnode with the provided id. #[cfg_attr(feature = "schema", returns(UnbondedMixnodeResponse))] GetUnbondedMixNodeInformation { /// Id of the node to query. - mix_id: MixId, + mix_id: NodeId, }, /// Gets the detailed mixnode information of a node given its current identity key. @@ -597,10 +529,6 @@ pub enum QueryMsg { mix_identity: IdentityKey, }, - /// Gets the current layer configuration of the mix network. - #[cfg_attr(feature = "schema", returns(LayerDistribution))] - GetLayerDistribution {}, - // gateway-related: /// Gets the basic list of all currently bonded gateways. #[cfg_attr(feature = "schema", returns(PagedGatewayResponse))] @@ -626,12 +554,128 @@ pub enum QueryMsg { address: String, }, - // delegation-related: - /// Gets all delegations associated with particular mixnode - #[cfg_attr(feature = "schema", returns(PagedMixNodeDelegationsResponse))] - GetMixnodeDelegations { + /// Get the `NodeId`s of all the legacy gateways that they will get assigned once migrated into NymNodes + #[cfg_attr(feature = "schema", returns(PreassignedGatewayIdsResponse))] + GetPreassignedGatewayIds { + /// Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response. + start_after: Option, + + /// Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default. + limit: Option, + }, + + // nym-node-related: + /// Gets the basic list of all currently bonded nymnodes. + #[cfg_attr(feature = "schema", returns(PagedNymNodeBondsResponse))] + GetNymNodeBondsPaged { + /// Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default. + limit: Option, + + /// Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response. + start_after: Option, + }, + + /// Gets the detailed list of all currently bonded nymnodes. + #[cfg_attr(feature = "schema", returns(PagedNymNodeDetailsResponse))] + GetNymNodesDetailedPaged { + /// Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default. + limit: Option, + + /// Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response. + start_after: Option, + }, + + /// Gets the basic information of an unbonded nym-node with the provided id. + #[cfg_attr(feature = "schema", returns(UnbondedNodeResponse))] + GetUnbondedNymNode { /// Id of the node to query. - mix_id: MixId, + node_id: NodeId, + }, + + /// Gets the basic list of all unbonded nymnodes. + #[cfg_attr(feature = "schema", returns(PagedUnbondedNymNodesResponse))] + GetUnbondedNymNodesPaged { + /// Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default. + limit: Option, + + /// Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response. + start_after: Option, + }, + + /// Gets the basic list of all unbonded nymnodes that belonged to a particular owner. + #[cfg_attr(feature = "schema", returns(PagedUnbondedNymNodesResponse))] + GetUnbondedNymNodesByOwnerPaged { + /// The address of the owner of the nym-node used for the query + owner: String, + + /// Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default. + limit: Option, + + /// Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response. + start_after: Option, + }, + + /// Gets the basic list of all unbonded nymnodes that used the particular identity key. + #[cfg_attr(feature = "schema", returns(PagedUnbondedNymNodesResponse))] + GetUnbondedNymNodesByIdentityKeyPaged { + /// The identity key (base58-encoded ed25519 public key) of the node used for the query. + identity_key: IdentityKey, + + /// Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default. + limit: Option, + + /// Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response. + start_after: Option, + }, + + /// Gets the detailed nymnode information belonging to the particular owner. + #[cfg_attr(feature = "schema", returns(NodeOwnershipResponse))] + GetOwnedNymNode { + /// Address of the node owner to use for the query. + address: String, + }, + + /// Gets the detailed nymnode information of a node with the provided id. + #[cfg_attr(feature = "schema", returns(NodeDetailsResponse))] + GetNymNodeDetails { + /// Id of the node to query. + node_id: NodeId, + }, + + /// Gets the detailed nym-node information given its current identity key. + #[cfg_attr(feature = "schema", returns(NodeDetailsByIdentityResponse))] + GetNymNodeDetailsByIdentityKey { + /// The identity key (base58-encoded ed25519 public key) of the nym-node used for the query. + node_identity: IdentityKey, + }, + + /// Gets the rewarding information of a nym-node with the provided id. + #[cfg_attr(feature = "schema", returns(NodeRewardingDetailsResponse))] + GetNodeRewardingDetails { + /// Id of the node to query. + node_id: NodeId, + }, + + /// Gets the stake saturation of a nym-node with the provided id. + #[cfg_attr(feature = "schema", returns(StakeSaturationResponse))] + GetNodeStakeSaturation { + /// Id of the node to query. + node_id: NodeId, + }, + + #[cfg_attr(feature = "schema", returns(EpochAssignmentResponse))] + GetRoleAssignment { role: Role }, + + #[cfg_attr(feature = "schema", returns(RolesMetadataResponse))] + GetRewardedSetMetadata {}, + + // delegation-related: + /// Gets all delegations associated with particular node + #[cfg_attr(feature = "schema", returns(PagedNodeDelegationsResponse))] + GetNodeDelegations { + /// Id of the node to query. + #[serde(alias = "mix_id")] + node_id: NodeId, /// Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response. start_after: Option, @@ -649,17 +693,18 @@ pub enum QueryMsg { delegator: String, /// Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response. - start_after: Option<(MixId, OwnerProxySubKey)>, + start_after: Option<(NodeId, OwnerProxySubKey)>, /// Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default. limit: Option, }, /// Gets delegation information associated with particular mixnode - delegator pair - #[cfg_attr(feature = "schema", returns(MixNodeDelegationResponse))] + #[cfg_attr(feature = "schema", returns(NodeDelegationResponse))] GetDelegationDetails { /// Id of the node to query. - mix_id: MixId, + #[serde(alias = "mix_id")] + node_id: NodeId, /// The address of the owner of the delegation. delegator: String, @@ -689,9 +734,14 @@ pub enum QueryMsg { /// Gets the reward amount accrued by the particular mixnode that has not yet been claimed. #[cfg_attr(feature = "schema", returns(PendingRewardResponse))] - GetPendingMixNodeOperatorReward { + #[serde( + alias = "GetPendingMixNodeOperatorReward", + alias = "get_pending_mix_node_operator_reward" + )] + GetPendingNodeOperatorReward { /// Id of the node to query. - mix_id: MixId, + #[serde(alias = "mix_id")] + node_id: NodeId, }, /// Gets the reward amount accrued by the particular delegator that has not yet been claimed. @@ -701,7 +751,8 @@ pub enum QueryMsg { address: String, /// Id of the node to query. - mix_id: MixId, + #[serde(alias = "mix_id")] + node_id: NodeId, /// Entity who made the delegation on behalf of the owner. /// If present, it's most likely the address of the vesting contract. @@ -712,10 +763,14 @@ pub enum QueryMsg { #[cfg_attr(feature = "schema", returns(EstimatedCurrentEpochRewardResponse))] GetEstimatedCurrentEpochOperatorReward { /// Id of the node to query. - mix_id: MixId, + #[serde(alias = "mix_id")] + node_id: NodeId, /// The estimated performance for the current epoch of the given node. estimated_performance: Performance, + + /// The estimated work for the current epoch of the given node. + estimated_work: Option, }, /// Given the provided node performance, attempt to estimate the delegator reward for the current epoch. @@ -725,14 +780,14 @@ pub enum QueryMsg { address: String, /// Id of the node to query. - mix_id: MixId, - - /// Entity who made the delegation on behalf of the owner. - /// If present, it's most likely the address of the vesting contract. - proxy: Option, + #[serde(alias = "mix_id")] + node_id: NodeId, /// The estimated performance for the current epoch of the given node. estimated_performance: Performance, + + /// The estimated work for the current epoch of the given node. + estimated_work: Option, }, // interval-related diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/nym_node.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/nym_node.rs new file mode 100644 index 0000000000..fa5db3b39c --- /dev/null +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/nym_node.rs @@ -0,0 +1,575 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::MixnetContractError; +use crate::{EpochEventId, EpochId, Gateway, IntervalEventId, MixNode, NodeId, NodeRewarding}; +use contracts_common::IdentityKey; +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Addr, Coin, Decimal, StdError, StdResult}; +use cw_storage_plus::{IntKey, Key, KeyDeserialize, PrimaryKey}; +use std::fmt::{Display, Formatter}; + +#[cw_serde] +#[derive(PartialOrd, Copy, Hash, Eq)] +#[repr(u8)] +pub enum Role { + #[serde(rename = "eg", alias = "entry", alias = "entry_gateway")] + EntryGateway = 0, + + #[serde(rename = "l1", alias = "layer1")] + Layer1 = 1, + + #[serde(rename = "l2", alias = "layer2")] + Layer2 = 2, + + #[serde(rename = "l3", alias = "layer3")] + Layer3 = 3, + + #[serde(rename = "xg", alias = "exit", alias = "exit_gateway")] + ExitGateway = 4, + + #[serde(rename = "stb", alias = "standby")] + Standby = 128, +} + +impl TryFrom for Role { + type Error = MixnetContractError; + fn try_from(value: u8) -> Result { + match value { + n if n == Role::EntryGateway as u8 => Ok(Role::EntryGateway), + n if n == Role::Layer1 as u8 => Ok(Role::Layer1), + n if n == Role::Layer2 as u8 => Ok(Role::Layer2), + n if n == Role::Layer3 as u8 => Ok(Role::Layer3), + n if n == Role::ExitGateway as u8 => Ok(Role::ExitGateway), + n if n == Role::Standby as u8 => Ok(Role::Standby), + n => Err(MixnetContractError::UnknownRoleRepresentation { got: n }), + } + } +} + +impl<'a> PrimaryKey<'a> for Role { + type Prefix = >::Prefix; + type SubPrefix = >::SubPrefix; + type Suffix = >::Suffix; + type SuperSuffix = >::SuperSuffix; + + fn key(&self) -> Vec { + // I'm not sure why it wasn't possible to delegate the call to + // `(*self as u8).key()` directly... + // I guess because of the `Key::Ref(&'a [u8])` variant? + vec![Key::Val8((*self as u8).to_cw_bytes())] + } + + fn joined_key(&self) -> Vec { + (*self as u8).joined_key() + } + + fn joined_extra_key(&self, key: &[u8]) -> Vec { + (*self as u8).joined_extra_key(key) + } +} + +impl KeyDeserialize for Role { + type Output = Role; + + fn from_vec(value: Vec) -> StdResult { + let u8_key: ::Output = ::from_vec(value)?; + Role::try_from(u8_key).map_err(|err| StdError::generic_err(err.to_string())) + } + + fn from_slice(value: &[u8]) -> StdResult { + let u8_key: ::Output = ::from_slice(value)?; + Role::try_from(u8_key).map_err(|err| StdError::generic_err(err.to_string())) + } +} + +impl Role { + pub fn first() -> Role { + Role::ExitGateway + } + + pub fn next(&self) -> Option { + // roles have to be assigned in the following order: + // exit -> entry -> l1 -> l2 -> l3 -> standby + match self { + Role::ExitGateway => Some(Role::EntryGateway), + Role::EntryGateway => Some(Role::Layer1), + Role::Layer1 => Some(Role::Layer2), + Role::Layer2 => Some(Role::Layer3), + Role::Layer3 => Some(Role::Standby), + Role::Standby => None, + } + } + + pub fn is_first(&self) -> bool { + self == &Role::first() + } + + pub fn is_standby(&self) -> bool { + matches!(self, Role::Standby) + } +} + +impl Display for Role { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Role::Layer1 => write!(f, "mix layer 1"), + Role::Layer2 => write!(f, "mix layer 2"), + Role::Layer3 => write!(f, "mix layer 3"), + Role::EntryGateway => write!(f, "entry gateway"), + Role::ExitGateway => write!(f, "exit gateway"), + Role::Standby => write!(f, "standby"), + } + } +} + +/// Metadata associated with the rewarded set. +#[cw_serde] +#[derive(Default, Copy)] +pub struct RewardedSetMetadata { + /// Epoch that this data corresponds to. + pub epoch_id: EpochId, + + /// Indicates whether all roles got assigned to the set for this epoch. + pub fully_assigned: bool, + + /// Metadata for the 'EntryGateway' role + pub entry_gateway_metadata: RoleMetadata, + + /// Metadata for the 'ExitGateway' role + pub exit_gateway_metadata: RoleMetadata, + + /// Metadata for the 'Layer1' role + pub layer1_metadata: RoleMetadata, + + /// Metadata for the 'Layer2' role + pub layer2_metadata: RoleMetadata, + + /// Metadata for the 'Layer3' role + pub layer3_metadata: RoleMetadata, + + /// Metadata for the 'Standby' role + pub standby_metadata: RoleMetadata, +} + +impl RewardedSetMetadata { + pub fn new(epoch_id: EpochId) -> Self { + RewardedSetMetadata { + epoch_id, + fully_assigned: false, + entry_gateway_metadata: Default::default(), + exit_gateway_metadata: Default::default(), + layer1_metadata: Default::default(), + layer2_metadata: Default::default(), + layer3_metadata: Default::default(), + standby_metadata: Default::default(), + } + } + + pub fn set_role_count(&mut self, role: Role, num_nodes: u32) { + match role { + Role::EntryGateway => self.entry_gateway_metadata.num_nodes = num_nodes, + Role::Layer1 => self.layer1_metadata.num_nodes = num_nodes, + Role::Layer2 => self.layer2_metadata.num_nodes = num_nodes, + Role::Layer3 => self.layer3_metadata.num_nodes = num_nodes, + Role::ExitGateway => self.exit_gateway_metadata.num_nodes = num_nodes, + Role::Standby => self.standby_metadata.num_nodes = num_nodes, + } + } + + pub fn set_highest_id(&mut self, highest_id: NodeId, role: Role) { + match role { + Role::EntryGateway => self.entry_gateway_metadata.highest_id = highest_id, + Role::Layer1 => self.layer1_metadata.highest_id = highest_id, + Role::Layer2 => self.layer2_metadata.highest_id = highest_id, + Role::Layer3 => self.layer3_metadata.highest_id = highest_id, + Role::ExitGateway => self.exit_gateway_metadata.highest_id = highest_id, + Role::Standby => self.standby_metadata.highest_id = highest_id, + } + } + + // important note: this currently does **NOT** include gateway role as they're not being rewarded + // and the metadata is primarily used for data lookup during epoch transition + pub fn highest_rewarded_id(&self) -> NodeId { + let mut highest = 0; + if self.layer1_metadata.highest_id > highest { + highest = self.layer1_metadata.highest_id; + } + if self.layer2_metadata.highest_id > highest { + highest = self.layer2_metadata.highest_id; + } + if self.layer3_metadata.highest_id > highest { + highest = self.layer3_metadata.highest_id; + } + if self.standby_metadata.highest_id > highest { + highest = self.standby_metadata.highest_id; + } + + highest + } +} + +/// Metadata associated with particular node role. +#[cw_serde] +#[derive(Default, Copy)] +pub struct RoleMetadata { + /// Highest, also latest, node-id of a node assigned this role. + pub highest_id: NodeId, + + /// Number of nodes assigned this particular role. + pub num_nodes: u32, +} + +/// Full details associated with given node. +#[cw_serde] +pub struct NymNodeDetails { + /// Basic bond information of this node, such as owner address, original pledge, etc. + pub bond_information: NymNodeBond, + + /// Details used for computation of rewarding related data. + pub rewarding_details: NodeRewarding, + + /// Adjustments to the node that are scheduled to happen during future epoch/interval transitions. + pub pending_changes: PendingNodeChanges, +} + +impl NymNodeDetails { + pub fn new( + bond_information: NymNodeBond, + rewarding_details: NodeRewarding, + pending_changes: PendingNodeChanges, + ) -> Self { + NymNodeDetails { + bond_information, + rewarding_details, + pending_changes, + } + } + + pub fn node_id(&self) -> NodeId { + self.bond_information.node_id + } + + pub fn is_unbonding(&self) -> bool { + self.bond_information.is_unbonding + } + + pub fn original_pledge(&self) -> &Coin { + &self.bond_information.original_pledge + } + + pub fn pending_operator_reward(&self) -> Coin { + let pledge = self.original_pledge(); + self.rewarding_details.pending_operator_reward(pledge) + } + + pub fn pending_detailed_operator_reward(&self) -> StdResult { + let pledge = self.original_pledge(); + self.rewarding_details + .pending_detailed_operator_reward(pledge) + } + + pub fn total_stake(&self) -> Decimal { + self.rewarding_details.node_bond() + } + + pub fn pending_pledge_change(&self) -> Option { + self.pending_changes.pledge_change + } +} + +/// +#[cw_serde] +pub struct NymNodeBond { + /// Unique id assigned to the bonded node. + pub node_id: NodeId, + + /// Address of the owner of this nym-node. + pub owner: Addr, + + /// Original amount pledged by the operator of this node. + pub original_pledge: Coin, + + /// Block height at which this nym-node has been bonded. + pub bonding_height: u64, + + /// Flag to indicate whether this node is in the process of unbonding, + /// that will conclude upon the epoch finishing. + pub is_unbonding: bool, + + /// Information provided by the operator for the purposes of bonding. + pub node: NymNode, +} + +impl NymNodeBond { + pub fn new( + node_id: NodeId, + owner: Addr, + original_pledge: Coin, + node: impl Into, + bonding_height: u64, + ) -> NymNodeBond { + Self { + node_id, + owner, + original_pledge, + bonding_height, + is_unbonding: false, + node: node.into(), + } + } + + pub fn identity(&self) -> &str { + &self.node.identity_key + } + + pub fn ensure_bonded(&self) -> Result<(), MixnetContractError> { + if self.is_unbonding { + return Err(MixnetContractError::NodeIsUnbonding { + node_id: self.node_id, + }); + } + Ok(()) + } +} + +/// Information provided by the node operator during bonding that are used to allow other entities to use the services of this node. +#[cw_serde] +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/NymNode.ts") +)] +pub struct NymNode { + /// Network address of this nym-node, for example 1.1.1.1 or foo.mixnode.com + /// that is used to discover other capabilities of this node. + pub host: String, + + /// Allow specifying custom port for accessing the http, and thus self-described, api + /// of this node for the capabilities discovery. + pub custom_http_port: Option, + + /// Base58-encoded ed25519 EdDSA public key. + pub identity_key: IdentityKey, + // TODO: I don't think we want to include sphinx keys here, + // given we want to rotate them and keeping that in sync with contract will be a PITA +} + +impl NymNode { + /// Perform naive validation of the attached identity key - makes sure it's correctly encoded + /// and has 32 bytes (as expected from ed25519). we're not, however, checking if it's a valid curve point + pub fn naive_ensure_valid_pubkey(&self) -> Result<(), MixnetContractError> { + let decoded = bs58::decode(&self.identity_key) + .into_vec() + .map_err(|_| MixnetContractError::InvalidPubKey)?; + if decoded.len() != 32 { + return Err(MixnetContractError::InvalidPubKey); + } + Ok(()) + } + + /// Makes sure the provided host's length is at most 255 characters to prevent abuse. + pub fn ensure_host_in_range(&self) -> Result<(), MixnetContractError> { + if self.host.len() > 255 { + return Err(MixnetContractError::HostTooLong); + } + Ok(()) + } +} + +impl From for NymNode { + fn from(value: MixNode) -> Self { + NymNode { + host: value.host, + custom_http_port: Some(value.http_api_port), + identity_key: value.identity_key, + } + } +} + +impl From for NymNode { + fn from(value: Gateway) -> Self { + NymNode { + host: value.host, + custom_http_port: None, + identity_key: value.identity_key, + } + } +} + +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/NodeConfigUpdate.ts") +)] +#[cw_serde] +#[derive(Default)] +pub struct NodeConfigUpdate { + pub host: Option, + // ideally this would have been `Option>`, but not sure if json would have recognised it + pub custom_http_port: Option, + + // equivalent to setting `custom_http_port` to `None` + #[serde(default)] + pub restore_default_http_port: bool, +} + +#[cw_serde] +#[derive(Default, Copy)] +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/PendingNodeChanges.ts") +)] +pub struct PendingNodeChanges { + pub pledge_change: Option, + pub cost_params_change: Option, +} + +impl PendingNodeChanges { + pub fn new_empty() -> PendingNodeChanges { + PendingNodeChanges { + pledge_change: None, + cost_params_change: None, + } + } + + pub fn ensure_no_pending_pledge_changes(&self) -> Result<(), MixnetContractError> { + if let Some(pending_event_id) = self.pledge_change { + return Err(MixnetContractError::PendingPledgeChange { pending_event_id }); + } + Ok(()) + } + + pub fn ensure_no_pending_params_changes(&self) -> Result<(), MixnetContractError> { + if let Some(pending_event_id) = self.cost_params_change { + return Err(MixnetContractError::PendingParamsChange { pending_event_id }); + } + Ok(()) + } +} + +/// Basic information of a node that used to be part of the nym network but has already unbonded. +#[cw_serde] +pub struct UnbondedNymNode { + /// Base58-encoded ed25519 EdDSA public key. + pub identity_key: IdentityKey, + + /// NodeId assigned to this node. + pub node_id: NodeId, + + /// Address of the owner of this nym node. + pub owner: Addr, + + /// Block height at which this nym node has unbonded. + pub unbonding_height: u64, +} + +/// Response containing rewarding information of a node with the provided id. +#[cw_serde] +pub struct NodeRewardingDetailsResponse { + /// Id of the requested node. + pub node_id: NodeId, + + /// If there exists a node with the provided id, this field contains its rewarding information. + pub rewarding_details: Option, +} + +/// Response containing details of a node belonging to the particular owner. +#[cw_serde] +pub struct NodeOwnershipResponse { + /// Validated address of the node owner. + pub address: Addr, + + /// If the provided address owns a nym-node, this field contains its detailed information. + pub details: Option, +} + +/// Response containing details of a node with the provided id. +#[cw_serde] +pub struct NodeDetailsResponse { + /// Id of the requested node. + pub node_id: NodeId, + + /// If there exists a node with the provided id, this field contains its detailed information. + pub details: Option, +} + +/// Response containing details of a bonded node with the provided identity key. +#[cw_serde] +pub struct NodeDetailsByIdentityResponse { + /// The identity key (base58-encoded ed25519 public key) of the node. + pub identity_key: IdentityKey, + + /// If there exists a bonded node with the provided identity key, this field contains its detailed information. + pub details: Option, +} + +/// Response containing the current state of the stake saturation of a node with the provided id. +#[cw_serde] +pub struct StakeSaturationResponse { + /// Id of the requested node. + pub node_id: NodeId, + + /// The current stake saturation of this node that is indirectly used in reward calculation formulas. + /// Note that it can't be larger than 1. + pub current_saturation: Option, + + /// The current, absolute, stake saturation of this node. + /// Note that as the name suggests it can be larger than 1. + /// However, anything beyond that value has no effect on the total node reward. + pub uncapped_saturation: Option, +} + +/// Response containing paged list of all nym-nodes that have ever unbonded. +#[cw_serde] +pub struct PagedUnbondedNymNodesResponse { + /// Basic information of the node such as the owner or the identity key. + pub nodes: Vec, + + /// Field indicating paging information for the following queries if the caller wishes to get further entries. + pub start_next_after: Option, +} + +/// Response containing basic information of an unbonded nym-node with the provided id. +#[cw_serde] +pub struct UnbondedNodeResponse { + /// Id of the requested nym-node. + pub node_id: NodeId, + + /// If there existed a nym-node with the provided id, this field contains its basic information. + pub details: Option, +} + +#[cw_serde] +pub struct PagedNymNodeBondsResponse { + /// The nym node bond information present in the contract. + pub nodes: Vec, + + /// Field indicating paging information for the following queries if the caller wishes to get further entries. + pub start_next_after: Option, +} + +#[cw_serde] +pub struct PagedNymNodeDetailsResponse { + /// All nym-node details stored in the contract. + /// Apart from the basic bond information it also contains details required for all future reward calculation + /// as well as any pending changes requested by the operator. + pub nodes: Vec, + + /// Field indicating paging information for the following queries if the caller wishes to get further entries. + pub start_next_after: Option, +} + +#[cw_serde] +pub struct EpochAssignmentResponse { + /// Epoch that this data corresponds to. + pub epoch_id: EpochId, + + pub nodes: Vec, +} + +#[cw_serde] +pub struct RolesMetadataResponse { + pub metadata: RewardedSetMetadata, +} diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/pending_events.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/pending_events.rs index 8d97e20494..72266d01d8 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/pending_events.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/pending_events.rs @@ -1,9 +1,9 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::mixnode::MixNodeCostParams; -use crate::reward_params::IntervalRewardingParamsUpdate; -use crate::{BlockHeight, MixId}; +use crate::mixnode::NodeCostParams; +use crate::reward_params::{ActiveSetUpdate, IntervalRewardingParamsUpdate}; +use crate::{BlockHeight, NodeId}; use cosmwasm_schema::cw_serde; use cosmwasm_std::{Addr, Coin}; @@ -35,7 +35,7 @@ pub struct PendingEpochEventData { pub enum PendingEpochEventKind { // can't just pass the `Delegation` struct here as it's impossible to determine // `cumulative_reward_ratio` ahead of time - /// Request to create a delegation towards particular mixnode. + /// Request to create a delegation towards particular node. /// Note that if such delegation already exists, it will get updated with the provided token amount. #[serde(alias = "Delegate")] #[non_exhaustive] @@ -43,8 +43,9 @@ pub enum PendingEpochEventKind { /// The address of the owner of the delegation. owner: Addr, - /// The id of the mixnode used for the delegation. - mix_id: MixId, + /// The id of the node used for the delegation. + #[serde(alias = "mix_id")] + node_id: NodeId, /// The amount of tokens to use for the delegation. amount: Coin, @@ -54,15 +55,16 @@ pub enum PendingEpochEventKind { proxy: Option, }, - /// Request to remove delegation from particular mixnode. + /// Request to remove delegation from particular node. #[serde(alias = "Undelegate")] #[non_exhaustive] Undelegate { /// The address of the owner of the delegation. owner: Addr, - /// The id of the mixnode used for the delegation. - mix_id: MixId, + /// The id of the node used for the delegation. + #[serde(alias = "mix_id")] + node_id: NodeId, /// Entity who made the delegation on behalf of the owner. /// If present, it's most likely the address of the vesting contract. @@ -70,20 +72,38 @@ pub enum PendingEpochEventKind { }, /// Request to pledge more tokens (by the node operator) towards its node. - #[serde(alias = "PledgeMore")] - PledgeMore { - /// The id of the mixnode that will have its pledge updated. - mix_id: MixId, + NymNodePledgeMore { + /// The id of the nym node that will have its pledge updated. + node_id: NodeId, - /// The amount of additional tokens to use by the pledge. + /// The amount of additional tokens to use in the pledge. + amount: Coin, + }, + + /// Request to pledge more tokens (by the node operator) towards its node. + #[serde(alias = "PledgeMore")] + MixnodePledgeMore { + /// The id of the mixnode that will have its pledge updated. + mix_id: NodeId, + + /// The amount of additional tokens to use in the pledge. amount: Coin, }, + /// Request to decrease amount of pledged tokens (by the node operator) from its node. + NymNodeDecreasePledge { + /// The id of the nym node that will have its pledge updated. + node_id: NodeId, + + /// The amount of tokens that should be removed from the pledge. + decrease_by: Coin, + }, + /// Request to decrease amount of pledged tokens (by the node operator) from its node. #[serde(alias = "DecreasePledge")] - DecreasePledge { + MixnodeDecreasePledge { /// The id of the mixnode that will have its pledge updated. - mix_id: MixId, + mix_id: NodeId, /// The amount of tokens that should be removed from the pledge. decrease_by: Coin, @@ -93,15 +113,17 @@ pub enum PendingEpochEventKind { #[serde(alias = "UnbondMixnode")] UnbondMixnode { /// The id of the mixnode that will get unbonded. - mix_id: MixId, + mix_id: NodeId, }, - /// Request to update the current size of the active set. - #[serde(alias = "UpdateActiveSetSize")] - UpdateActiveSetSize { - /// The new desired size of the active set. - new_size: u32, + /// Request to unbond a nym node and completely remove it from the network. + UnbondNymNode { + /// The id of the node that will get unbonded. + node_id: NodeId, }, + + /// Request to update the current active set. + UpdateActiveSet { update: ActiveSetUpdate }, } impl PendingEpochEventKind { @@ -112,19 +134,19 @@ impl PendingEpochEventKind { } } - pub fn new_delegate(owner: Addr, mix_id: MixId, amount: Coin) -> Self { + pub fn new_delegate(owner: Addr, node_id: NodeId, amount: Coin) -> Self { PendingEpochEventKind::Delegate { owner, - mix_id, + node_id, amount, proxy: None, } } - pub fn new_undelegate(owner: Addr, mix_id: MixId) -> Self { + pub fn new_undelegate(owner: Addr, node_id: NodeId) -> Self { PendingEpochEventKind::Undelegate { owner, - mix_id, + node_id, proxy: None, } } @@ -166,10 +188,19 @@ pub enum PendingIntervalEventKind { #[serde(alias = "ChangeMixCostParams")] ChangeMixCostParams { /// The id of the mixnode that will have its cost parameters updated. - mix_id: MixId, + mix_id: NodeId, /// The new updated cost function of this mixnode. - new_costs: MixNodeCostParams, + new_costs: NodeCostParams, + }, + + /// Request to update cost parameters of given nym node. + ChangeNymNodeCostParams { + /// The id of the nym node that will have its cost parameters updated. + node_id: NodeId, + + /// The new updated cost function of this nym node. + new_costs: NodeCostParams, }, /// Request to update the underlying rewarding parameters used by the system diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/reward_params.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/reward_params.rs index 4928bd6a1e..da6e2d3dae 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/reward_params.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/reward_params.rs @@ -2,11 +2,13 @@ // SPDX-License-Identifier: Apache-2.0 use crate::helpers::IntoBaseDecimal; +use crate::nym_node::Role; use crate::{error::MixnetContractError, Percent}; use cosmwasm_schema::cw_serde; use cosmwasm_std::Decimal; pub type Performance = Percent; +pub type WorkFactor = Decimal; /// Parameters required by the mix-mining reward distribution that do not change during an interval. #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] @@ -86,23 +88,15 @@ pub struct RewardingParams { /// Parameters that should remain unchanged throughout an interval. pub interval: IntervalRewardParams, - // while the rewarded set size can change between epochs to accommodate for bandwidth demands, - // the active set size should be unchanged between epochs and should only be adjusted between - // intervals. However, it makes more sense to keep both of those values together as they're - // very strongly related to each other. - /// The expected number of mixnodes in the rewarded set (i.e. active + standby). - pub rewarded_set_size: u32, - - /// The expected number of mixnodes in the active set. - pub active_set_size: u32, + pub rewarded_set: RewardedSetParams, } impl RewardingParams { - pub fn active_node_work(&self) -> Decimal { + pub fn active_node_work(&self) -> WorkFactor { self.interval.active_set_work_factor * self.standby_node_work() } - pub fn standby_node_work(&self) -> Decimal { + pub fn standby_node_work(&self) -> WorkFactor { let f = self.interval.active_set_work_factor; let k = self.dec_rewarded_set_size(); let one = Decimal::one(); @@ -113,27 +107,33 @@ impl RewardingParams { one / (f * k - (f - one) * k_r) } + pub fn rewarded_set_size(&self) -> u32 { + self.rewarded_set.rewarded_set_size() + } + + pub fn active_set_size(&self) -> u32 { + self.rewarded_set.active_set_size() + } + pub fn dec_rewarded_set_size(&self) -> Decimal { // the unwrap here is fine as we're guaranteed an `u32` is going to fit in a Decimal // with 0 decimal places #[allow(clippy::unwrap_used)] - self.rewarded_set_size.into_base_decimal().unwrap() + self.rewarded_set_size().into_base_decimal().unwrap() } pub fn dec_active_set_size(&self) -> Decimal { // the unwrap here is fine as we're guaranteed an `u32` is going to fit in a Decimal // with 0 decimal places #[allow(clippy::unwrap_used)] - self.active_set_size.into_base_decimal().unwrap() + self.active_set_size().into_base_decimal().unwrap() } fn dec_standby_set_size(&self) -> Decimal { // the unwrap here is fine as we're guaranteed an `u32` is going to fit in a Decimal // with 0 decimal places #[allow(clippy::unwrap_used)] - (self.rewarded_set_size - self.active_set_size) - .into_base_decimal() - .unwrap() + self.rewarded_set.standby.into_base_decimal().unwrap() } pub fn apply_epochs_in_interval_change(&mut self, new_epochs_in_interval: u32) { @@ -146,19 +146,35 @@ impl RewardingParams { * self.interval.interval_pool_emission; } - pub fn try_change_active_set_size( - &mut self, - new_active_set_size: u32, + pub fn validate_active_set_update( + &self, + update: ActiveSetUpdate, ) -> Result<(), MixnetContractError> { - if new_active_set_size == 0 { - return Err(MixnetContractError::ZeroActiveSet); - } + update.ensure_non_empty()?; + let active_set_size = update.active_set_size(); - if new_active_set_size > self.rewarded_set_size { + if active_set_size > self.rewarded_set_size() { return Err(MixnetContractError::InvalidActiveSetSize); } - self.active_set_size = new_active_set_size; + Ok(()) + } + + pub fn try_change_active_set( + &mut self, + update: ActiveSetUpdate, + ) -> Result<(), MixnetContractError> { + self.validate_active_set_update(update)?; + let active_set_size = update.active_set_size(); + let rewarded_set_size = self.rewarded_set_size(); + + // safety: due to validation we know that the active_set_size <= rewarded_set_size + let new_standby = rewarded_set_size - active_set_size; + + self.rewarded_set.exit_gateways = update.exit_gateways; + self.rewarded_set.entry_gateways = update.entry_gateways; + self.rewarded_set.mixnodes = update.mixnodes; + self.rewarded_set.standby = new_standby; Ok(()) } @@ -201,16 +217,10 @@ impl RewardingParams { self.interval.interval_pool_emission = interval_pool_emission; } - if let Some(rewarded_set_size) = updates.rewarded_set_size { - if rewarded_set_size == 0 { - return Err(MixnetContractError::ZeroRewardedSet); - } - if rewarded_set_size < self.active_set_size { - return Err(MixnetContractError::InvalidRewardedSetSize); - } - + if let Some(rewarded_set_update) = updates.rewarded_set_params { + rewarded_set_update.ensure_valid()?; recompute_saturation_point = true; - self.rewarded_set_size = rewarded_set_size; + self.rewarded_set = rewarded_set_update; } if recompute_epoch_budget { @@ -221,31 +231,99 @@ impl RewardingParams { if recompute_saturation_point { self.interval.stake_saturation_point = - self.interval.staking_supply / self.rewarded_set_size.into_base_decimal()? + self.interval.staking_supply / self.rewarded_set_size().into_base_decimal()? } Ok(()) } } -// TODO: possibly refactor this -/// Parameters used for rewarding particular mixnode. +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/RewardedSetParams.ts") +)] #[cw_serde] #[derive(Copy)] -pub struct NodeRewardParams { - /// Performance of the particular node in the current epoch. - pub performance: Percent, +pub struct RewardedSetParams { + /// The expected number of nodes assigned entry gateway role (i.e. [`Role::EntryGateway`]) + pub entry_gateways: u32, - /// Flag indicating whether the node has been in the active set during the epoch. - pub in_active_set: bool, + /// The expected number of nodes assigned exit gateway role (i.e. [`Role::ExitGateway`]) + pub exit_gateways: u32, + + /// The expected number of nodes assigned the 'mixnode' role, i.e. total of [`Role::Layer1`], [`Role::Layer2`] and [`Role::Layer3`]. + pub mixnodes: u32, + + /// Number of nodes in the 'standby' set. (i.e. [`Role::Standby`]) + pub standby: u32, } -impl NodeRewardParams { - pub fn new(performance: Percent, in_active_set: bool) -> Self { - NodeRewardParams { - performance, - in_active_set, +impl RewardedSetParams { + pub fn active_set_size(&self) -> u32 { + self.entry_gateways + self.exit_gateways + self.mixnodes + } + + pub fn rewarded_set_size(&self) -> u32 { + self.active_set_size() + self.standby + } + + pub fn ensure_valid(&self) -> Result<(), MixnetContractError> { + if self.entry_gateways == 0 || self.exit_gateways == 0 || self.mixnodes == 0 { + return Err(MixnetContractError::EmptyRoleAssignment); } + if self.mixnodes % 3 != 0 { + return Err(MixnetContractError::UnevenLayerAssignment); + } + Ok(()) + } + + pub fn maximum_role_count(&self, role: Role) -> u32 { + match role { + Role::EntryGateway => self.entry_gateways, + Role::Layer1 | Role::Layer2 | Role::Layer3 => self.mixnodes / 3, + Role::ExitGateway => self.exit_gateways, + Role::Standby => self.standby, + } + } + + pub fn ensure_role_count(&self, role: Role, assigned: u32) -> Result<(), MixnetContractError> { + let allowed = self.maximum_role_count(role); + + if assigned > allowed { + return Err(MixnetContractError::IllegalRoleCount { + role, + assigned, + allowed, + }); + } + + Ok(()) + } +} + +/// Parameters used for rewarding particular node. +#[cw_serde] +#[derive(Copy)] +pub struct NodeRewardingParameters { + /// Performance of the particular node in the current epoch. + pub performance: Performance, + + /// Amount of work performed by this node in the current epoch + /// also known as 'omega' in the paper + pub work_factor: WorkFactor, +} + +impl NodeRewardingParameters { + pub fn new(performance: Performance, work_factor: WorkFactor) -> Self { + NodeRewardingParameters { + performance, + work_factor, + } + } + + pub fn is_zero(&self) -> bool { + self.performance.is_zero() || self.work_factor.is_zero() } } @@ -282,8 +360,8 @@ pub struct IntervalRewardingParamsUpdate { /// Defines the new value of the interval pool emission rate. pub interval_pool_emission: Option, - /// Defines the new size of the rewarded set. - pub rewarded_set_size: Option, + /// Defines the parameters of the rewarded set. + pub rewarded_set_params: Option, } impl IntervalRewardingParamsUpdate { @@ -295,10 +373,42 @@ impl IntervalRewardingParamsUpdate { || self.sybil_resistance_percent.is_some() || self.active_set_work_factor.is_some() || self.interval_pool_emission.is_some() - || self.rewarded_set_size.is_some() + || self.rewarded_set_params.is_some() } pub fn to_inline_json(&self) -> String { serde_json_wasm::to_string(self).unwrap_or_else(|_| "serialisation failure".into()) } } + +/// Specification on how the active set should be updated. +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/ActiveSetUpdate.ts") +)] +#[cw_serde] +#[derive(Copy, Default)] +pub struct ActiveSetUpdate { + /// The expected number of nodes assigned entry gateway role (i.e. [`Role::EntryGateway`]) + pub entry_gateways: u32, + + /// The expected number of nodes assigned exit gateway role (i.e. [`Role::ExitGateway`]) + pub exit_gateways: u32, + + /// The expected number of nodes assigned the 'mixnode' role, i.e. total of [`Role::Layer1`], [`Role::Layer2`] and [`Role::Layer3`]. + pub mixnodes: u32, +} + +impl ActiveSetUpdate { + pub fn active_set_size(&self) -> u32 { + self.entry_gateways + self.exit_gateways + self.mixnodes + } + + pub fn ensure_non_empty(&self) -> Result<(), MixnetContractError> { + if self.entry_gateways == 0 || self.exit_gateways == 0 || self.mixnodes == 0 { + return Err(MixnetContractError::EmptyRoleAssignment); + } + Ok(()) + } +} diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/helpers.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/helpers.rs index 4d9ca75459..d9c0bb5ea9 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/helpers.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/helpers.rs @@ -18,3 +18,11 @@ pub fn truncate_reward(reward: Decimal, denom: impl Into) -> Coin { pub fn truncate_reward_amount(reward: Decimal) -> Uint128 { truncate_decimal(reward) } + +pub fn legacy_standby_work_factor() -> Decimal { + todo!() +} + +pub fn legacy_active_work_factor() -> Decimal { + todo!() +} diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/mod.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/mod.rs index 4ef9cd433a..18bdc3e562 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/mod.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/mod.rs @@ -1,7 +1,6 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::{MixId, RewardedSetNodeStatus}; use cosmwasm_schema::cw_serde; use cosmwasm_std::{Coin, Decimal}; @@ -63,7 +62,10 @@ pub struct PendingRewardResponse { /// The associated mixnode is still fully bonded, meaning it is neither unbonded /// nor in the process of unbonding that would have finished at the epoch transition. + #[deprecated(note = "this field will be removed. use .node_still_fully_bonded instead")] pub mixnode_still_fully_bonded: bool, + + pub node_still_fully_bonded: bool, } /// Response containing estimation of node rewards for the current epoch. @@ -99,13 +101,3 @@ impl EstimatedCurrentEpochRewardResponse { } } } - -/// Response containing paged list of all mixnodes in the rewarded set. -#[cw_serde] -pub struct PagedRewardedSetResponse { - /// Nodes in the current rewarded set. - pub nodes: Vec<(MixId, RewardedSetNodeStatus)>, - - /// Field indicating paging information for the following queries if the caller wishes to get further entries. - pub start_next_after: Option, -} diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/simulator/mod.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/simulator/mod.rs index 0cc4d06dee..46326a1712 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/simulator/mod.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/simulator/mod.rs @@ -3,23 +3,21 @@ use crate::error::MixnetContractError; use crate::helpers::IntoBaseDecimal; -use crate::reward_params::NodeRewardParams; +use crate::reward_params::{NodeRewardingParameters, WorkFactor}; use crate::rewarding::simulator::simulated_node::SimulatedNode; use crate::rewarding::RewardDistribution; -use crate::{ - Delegation, Interval, IntervalRewardParams, MixId, MixNodeCostParams, RewardingParams, -}; +use crate::{Delegation, Interval, IntervalRewardParams, NodeCostParams, NodeId, RewardingParams}; use cosmwasm_std::{Coin, Decimal}; use std::collections::BTreeMap; pub mod simulated_node; pub struct Simulator { - pub nodes: BTreeMap, + pub nodes: BTreeMap, pub system_rewarding_params: RewardingParams, pub interval: Interval, - next_mix_id: MixId, + next_mix_id: NodeId, pending_reward_pool_emission: Decimal, } @@ -34,6 +32,14 @@ impl Simulator { } } + pub fn legacy_standby_work_factor(&self) -> WorkFactor { + self.system_rewarding_params.standby_node_work() + } + + pub fn legacy_active_work_factor(&self) -> WorkFactor { + self.system_rewarding_params.active_node_work() + } + fn advance_epoch(&mut self) -> Result<(), MixnetContractError> { let updated = self.interval.advance_epoch(); @@ -53,7 +59,7 @@ impl Simulator { let stake_saturation_point = staking_supply / self .system_rewarding_params - .rewarded_set_size + .rewarded_set_size() .into_base_decimal()?; let updated_params = RewardingParams { @@ -67,8 +73,7 @@ impl Simulator { active_set_work_factor: old.active_set_work_factor, interval_pool_emission: old.interval_pool_emission, }, - rewarded_set_size: self.system_rewarding_params.rewarded_set_size, - active_set_size: self.system_rewarding_params.active_set_size, + rewarded_set: self.system_rewarding_params.rewarded_set, }; self.system_rewarding_params = updated_params; @@ -82,8 +87,8 @@ impl Simulator { pub fn bond( &mut self, pledge: Coin, - cost_params: MixNodeCostParams, - ) -> Result { + cost_params: NodeCostParams, + ) -> Result { let mix_id = self.next_mix_id; self.nodes.insert( @@ -105,7 +110,7 @@ impl Simulator { &mut self, delegator: S, delegation: Coin, - mix_id: MixId, + mix_id: NodeId, ) -> Result<(), MixnetContractError> { let node = self .nodes @@ -119,7 +124,7 @@ impl Simulator { pub fn undelegate>( &mut self, delegator: S, - mix_id: MixId, + mix_id: NodeId, ) -> Result<(Coin, Coin), MixnetContractError> { let node = self .nodes @@ -130,7 +135,7 @@ impl Simulator { pub fn simulate_epoch_single_node( &mut self, - params: NodeRewardParams, + params: NodeRewardingParameters, ) -> Result { assert_eq!(self.nodes.len(), 1); @@ -148,8 +153,8 @@ impl Simulator { pub fn simulate_epoch( &mut self, - node_params: &BTreeMap, - ) -> Result, MixnetContractError> { + node_params: &BTreeMap, + ) -> Result, MixnetContractError> { let mut params_keys = node_params.keys().copied().collect::>(); params_keys.sort_unstable(); let mut node_keys = self.nodes.keys().copied().collect::>(); @@ -185,7 +190,7 @@ impl Simulator { &self, delegation: &Delegation, ) -> Result { - Ok(self.nodes[&delegation.mix_id] + Ok(self.nodes[&delegation.node_id] .rewarding_details .determine_delegation_reward(delegation)?) } @@ -206,7 +211,7 @@ impl Simulator { // assume node state doesn't change in the interval (kinda unrealistic) pub fn simulate_full_interval( &mut self, - node_params: &BTreeMap, + node_params: &BTreeMap, ) -> Result<(), MixnetContractError> { for _ in 0..self.interval.epochs_in_interval() { self.simulate_epoch(node_params)?; @@ -219,6 +224,7 @@ impl Simulator { mod tests { use super::*; use crate::helpers::compare_decimals; + use crate::reward_params::RewardedSetParams; use crate::Percent; use cosmwasm_std::testing::mock_env; use std::time::Duration; @@ -226,6 +232,7 @@ mod tests { #[cfg(test)] mod single_node_case { use super::*; + use crate::reward_params::RewardedSetParams; use crate::rewarding::helpers::truncate_reward_amount; use cosmwasm_std::coin; @@ -237,15 +244,23 @@ mod tests { let profit_margin = Percent::from_percentage_value(10).unwrap(); let interval_operating_cost = Coin::new(40_000_000, "unym"); let epochs_in_interval = 720u32; - let rewarded_set_size = 240; - let active_set_size = 100; let interval_pool_emission = Percent::from_percentage_value(2).unwrap(); + // the import values here are active set being 100 and rewarded set being 240 + // since those are the values we were using in the past + let rewarded_set = RewardedSetParams { + entry_gateways: 20, + exit_gateways: 50, + mixnodes: 30, + standby: 140, + }; + let reward_pool = 250_000_000_000_000u128; let staking_supply = 100_000_000_000_000u128; let epoch_reward_budget = interval_pool_emission * Decimal::from_ratio(reward_pool, epochs_in_interval); - let stake_saturation_point = Decimal::from_ratio(staking_supply, rewarded_set_size); + let stake_saturation_point = + Decimal::from_ratio(staking_supply, rewarded_set.rewarded_set_size()); let rewarding_params = RewardingParams { interval: IntervalRewardParams { @@ -258,8 +273,7 @@ mod tests { active_set_work_factor: Decimal::percent(1000), // value '10' interval_pool_emission, }, - rewarded_set_size, - active_set_size, + rewarded_set, }; let interval = Interval::init_interval( @@ -270,7 +284,7 @@ mod tests { let initial_pledge = Coin::new(initial_pledge, "unym"); let mut simulator = Simulator::new(rewarding_params, interval); - let cost_params = MixNodeCostParams { + let cost_params = NodeCostParams { profit_margin_percent: profit_margin, interval_operating_cost, }; @@ -315,8 +329,10 @@ mod tests { fn simulator_returns_expected_values_for_base_case() { let mut simulator = base_simulator(10000_000000); - let epoch_params = - NodeRewardParams::new(Percent::from_percentage_value(100).unwrap(), true); + let epoch_params = NodeRewardingParameters::new( + Percent::from_percentage_value(100).unwrap(), + simulator.legacy_active_work_factor(), + ); let rewards = simulator.simulate_epoch_single_node(epoch_params).unwrap(); assert_eq!(rewards.delegates, Decimal::zero()); @@ -334,8 +350,10 @@ mod tests { .delegate("alice", Coin::new(18000_000000, "unym"), 0) .unwrap(); - let node_params = - NodeRewardParams::new(Percent::from_percentage_value(100).unwrap(), true); + let node_params = NodeRewardingParameters::new( + Percent::from_percentage_value(100).unwrap(), + simulator.legacy_active_work_factor(), + ); let rewards = simulator.simulate_epoch_single_node(node_params).unwrap(); compare_decimals( @@ -364,8 +382,10 @@ mod tests { #[test] fn delegation_and_undelegation() { let mut simulator = base_simulator(10000_000000); - let node_params = - NodeRewardParams::new(Percent::from_percentage_value(100).unwrap(), true); + let node_params = NodeRewardingParameters::new( + Percent::from_percentage_value(100).unwrap(), + simulator.legacy_active_work_factor(), + ); let rewards1 = simulator.simulate_epoch_single_node(node_params).unwrap(); let expected_operator1 = "1128452.5416104363".parse().unwrap(); @@ -411,8 +431,10 @@ mod tests { // essentially all delegators' rewards (and the operator itself) are still correctly computed let original_pledge = coin(10000_000000, "unym"); let mut simulator = base_simulator(original_pledge.amount.u128()); - let node_params = - NodeRewardParams::new(Percent::from_percentage_value(100).unwrap(), true); + let node_params = NodeRewardingParameters::new( + Percent::from_percentage_value(100).unwrap(), + simulator.legacy_active_work_factor(), + ); // add 2 delegations at genesis (because it makes things easier and as shown with previous tests // delegating at different times still work) @@ -454,8 +476,10 @@ mod tests { fn withdrawing_delegator_reward() { // essentially all delegators' rewards (and the operator itself) are still correctly computed let mut simulator = base_simulator(10000_000000); - let node_params = - NodeRewardParams::new(Percent::from_percentage_value(100).unwrap(), true); + let node_params = NodeRewardingParameters::new( + Percent::from_percentage_value(100).unwrap(), + simulator.legacy_active_work_factor(), + ); // add 2 delegations at genesis (because it makes things easier and as shown with previous tests // delegating at different times still work) @@ -524,7 +548,7 @@ mod tests { fn simulating_multiple_epochs() { let mut simulator = base_simulator(10000_000000); - let mut is_active = true; + let mut work_factor = simulator.legacy_active_work_factor(); let mut performance = Percent::from_percentage_value(100).unwrap(); for epoch in 0..720 { if epoch == 0 { @@ -538,7 +562,7 @@ mod tests { .unwrap() } if epoch == 89 { - is_active = false; + work_factor = simulator.legacy_standby_work_factor(); } if epoch == 123 { simulator @@ -560,7 +584,7 @@ mod tests { // TODO: figure out if there's a good way to verify whether `reward` is what we expect it to be } if epoch == 345 { - is_active = true; + work_factor = simulator.legacy_active_work_factor(); } if epoch == 358 { performance = Percent::from_percentage_value(100).unwrap(); @@ -579,7 +603,7 @@ mod tests { // this has to always hold check_rewarding_invariant(&simulator); - let node_params = NodeRewardParams::new(performance, is_active); + let node_params = NodeRewardingParameters::new(performance, work_factor); simulator.simulate_epoch_single_node(node_params).unwrap(); } @@ -600,15 +624,23 @@ mod tests { // rather than just checking the final results let epochs_in_interval = 1u32; - let rewarded_set_size = 10; - let active_set_size = 6; let interval_pool_emission = Percent::from_percentage_value(2).unwrap(); + // the import values here are active set being 6 and rewarded set being 10 + // since those are the values we were using in the past + let rewarded_set = RewardedSetParams { + entry_gateways: 1, + exit_gateways: 2, + mixnodes: 3, + standby: 4, + }; + let reward_pool = 250_000_000_000_000u128; let staking_supply = 100_000_000_000_000u128; let epoch_reward_budget = interval_pool_emission * Decimal::from_ratio(reward_pool, epochs_in_interval); - let stake_saturation_point = Decimal::from_ratio(staking_supply, rewarded_set_size); + let stake_saturation_point = + Decimal::from_ratio(staking_supply, rewarded_set.rewarded_set_size()); let rewarding_params = RewardingParams { interval: IntervalRewardParams { @@ -621,8 +653,7 @@ mod tests { active_set_work_factor: Decimal::percent(1000), // value '10' interval_pool_emission, }, - rewarded_set_size, - active_set_size, + rewarded_set, }; let interval = Interval::init_interval( @@ -636,7 +667,7 @@ mod tests { let n0 = simulator .bond( Coin::new(11_000_000_000000, "unym"), - MixNodeCostParams { + NodeCostParams { profit_margin_percent: Percent::from_percentage_value(10).unwrap(), interval_operating_cost: Coin::new(40_000_000, "unym"), }, @@ -649,7 +680,7 @@ mod tests { let n1 = simulator .bond( Coin::new(1_000_000_000000, "unym"), - MixNodeCostParams { + NodeCostParams { profit_margin_percent: Percent::from_percentage_value(10).unwrap(), interval_operating_cost: Coin::new(40_000_000, "unym"), }, @@ -662,7 +693,7 @@ mod tests { let n2 = simulator .bond( Coin::new(1_000_000_000000, "unym"), - MixNodeCostParams { + NodeCostParams { profit_margin_percent: Percent::from_percentage_value(10).unwrap(), interval_operating_cost: Coin::new(40_000_000, "unym"), }, @@ -675,7 +706,7 @@ mod tests { let n3 = simulator .bond( Coin::new(1_000_000_000000, "unym"), - MixNodeCostParams { + NodeCostParams { profit_margin_percent: Percent::from_percentage_value(0).unwrap(), interval_operating_cost: Coin::new(500_000_000, "unym"), }, @@ -688,7 +719,7 @@ mod tests { let n4 = simulator .bond( Coin::new(1000_000000, "unym"), - MixNodeCostParams { + NodeCostParams { profit_margin_percent: Percent::from_percentage_value(10).unwrap(), interval_operating_cost: Coin::new(40_000_000, "unym"), }, @@ -701,7 +732,7 @@ mod tests { let n5 = simulator .bond( Coin::new(1_000_000_000000, "unym"), - MixNodeCostParams { + NodeCostParams { profit_margin_percent: Percent::from_percentage_value(10).unwrap(), interval_operating_cost: Coin::new(40_000_000, "unym"), }, @@ -714,7 +745,7 @@ mod tests { let n6 = simulator .bond( Coin::new(11_000_000_000000, "unym"), - MixNodeCostParams { + NodeCostParams { profit_margin_percent: Percent::from_percentage_value(10).unwrap(), interval_operating_cost: Coin::new(40_000_000, "unym"), }, @@ -727,7 +758,7 @@ mod tests { let n7 = simulator .bond( Coin::new(1_000_000_000000, "unym"), - MixNodeCostParams { + NodeCostParams { profit_margin_percent: Percent::from_percentage_value(10).unwrap(), interval_operating_cost: Coin::new(40_000_000, "unym"), }, @@ -740,7 +771,7 @@ mod tests { let n8 = simulator .bond( Coin::new(1_000_000_000000, "unym"), - MixNodeCostParams { + NodeCostParams { profit_margin_percent: Percent::from_percentage_value(0).unwrap(), interval_operating_cost: Coin::new(500_000_000, "unym"), }, @@ -753,7 +784,7 @@ mod tests { let n9 = simulator .bond( Coin::new(1_000_000_000000, "unym"), - MixNodeCostParams { + NodeCostParams { profit_margin_percent: Percent::from_percentage_value(10).unwrap(), interval_operating_cost: Coin::new(40_000_000, "unym"), }, @@ -767,17 +798,20 @@ mod tests { let uptime_09 = Percent::from_percentage_value(90).unwrap(); let uptime_0 = Percent::from_percentage_value(0).unwrap(); + let active_work = simulator.legacy_active_work_factor(); + let standby_work = simulator.legacy_standby_work_factor(); + let node_params = [ - (n0, NodeRewardParams::new(uptime_1, true)), - (n1, NodeRewardParams::new(uptime_1, true)), - (n2, NodeRewardParams::new(uptime_1, true)), - (n3, NodeRewardParams::new(uptime_09, true)), - (n4, NodeRewardParams::new(uptime_09, true)), - (n5, NodeRewardParams::new(uptime_0, true)), - (n6, NodeRewardParams::new(uptime_1, false)), - (n7, NodeRewardParams::new(uptime_1, false)), - (n8, NodeRewardParams::new(uptime_09, false)), - (n9, NodeRewardParams::new(uptime_0, false)), + (n0, NodeRewardingParameters::new(uptime_1, active_work)), + (n1, NodeRewardingParameters::new(uptime_1, active_work)), + (n2, NodeRewardingParameters::new(uptime_1, active_work)), + (n3, NodeRewardingParameters::new(uptime_09, active_work)), + (n4, NodeRewardingParameters::new(uptime_09, active_work)), + (n5, NodeRewardingParameters::new(uptime_0, active_work)), + (n6, NodeRewardingParameters::new(uptime_1, standby_work)), + (n7, NodeRewardingParameters::new(uptime_1, standby_work)), + (n8, NodeRewardingParameters::new(uptime_09, standby_work)), + (n9, NodeRewardingParameters::new(uptime_0, standby_work)), ] .into_iter() .collect::>(); diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/simulator/simulated_node.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/simulator/simulated_node.rs index 12a9e967bd..bc6d18a16e 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/simulator/simulated_node.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/simulator/simulated_node.rs @@ -1,7 +1,7 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::{Delegation, EpochId, MixId, MixNodeCostParams, MixNodeRewarding}; +use crate::{Delegation, EpochId, NodeCostParams, NodeId, NodeRewarding}; use cosmwasm_std::{Addr, Coin}; use std::collections::HashMap; @@ -9,21 +9,21 @@ use crate::error::MixnetContractError; use crate::rewarding::helpers::truncate_reward; pub struct SimulatedNode { - pub mix_id: MixId, - pub rewarding_details: MixNodeRewarding, + pub mix_id: NodeId, + pub rewarding_details: NodeRewarding, pub delegations: HashMap, } impl SimulatedNode { pub fn new( - mix_id: MixId, - cost_params: MixNodeCostParams, + mix_id: NodeId, + cost_params: NodeCostParams, initial_pledge: &Coin, current_epoch: EpochId, ) -> Result { Ok(SimulatedNode { mix_id, - rewarding_details: MixNodeRewarding::initialise_new( + rewarding_details: NodeRewarding::initialise_new( cost_params, initial_pledge, current_epoch, @@ -59,8 +59,8 @@ impl SimulatedNode { ) -> Result<(Coin, Coin), MixnetContractError> { let delegator = delegator.into(); let delegation = self.delegations.remove(&delegator).ok_or( - MixnetContractError::NoMixnodeDelegationFound { - mix_id: MixId::MAX, + MixnetContractError::NodeDelegationNotFound { + node_id: NodeId::MAX, address: delegator, proxy: None, }, diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/signing_types.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/signing_types.rs index 261fa99ff0..00130d52d8 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/signing_types.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/signing_types.rs @@ -1,8 +1,8 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::families::FamilyHead; -use crate::{Gateway, IdentityKey, MixNode, MixNodeCostParams}; +use crate::nym_node::NymNode; +use crate::{Gateway, MixNode, NodeCostParams}; use contracts_common::signing::{ ContractMessageContent, LegacyContractMessageContent, MessageType, Nonce, SignableMessage, SigningPurpose, @@ -12,20 +12,20 @@ use serde::Serialize; pub type SignableMixNodeBondingMsg = SignableMessage>; pub type SignableGatewayBondingMsg = SignableMessage>; +pub type SignableNymNodeBondingMsg = SignableMessage>; pub type SignableLegacyMixNodeBondingMsg = SignableMessage>; pub type SignableLegacyGatewayBondingMsg = SignableMessage>; -pub type SignableFamilyJoinPermitMsg = SignableMessage; #[derive(Serialize)] pub struct MixnodeBondingPayload { mix_node: MixNode, - cost_params: MixNodeCostParams, + cost_params: NodeCostParams, } impl MixnodeBondingPayload { - pub fn new(mix_node: MixNode, cost_params: MixNodeCostParams) -> Self { + pub fn new(mix_node: MixNode, cost_params: NodeCostParams) -> Self { Self { mix_node, cost_params, @@ -44,7 +44,7 @@ pub fn construct_mixnode_bonding_sign_payload( sender: Addr, pledge: Coin, mix_node: MixNode, - cost_params: MixNodeCostParams, + cost_params: NodeCostParams, ) -> SignableMixNodeBondingMsg { let payload = MixnodeBondingPayload::new(mix_node, cost_params); let content = ContractMessageContent::new(sender, vec![pledge], payload); @@ -57,7 +57,7 @@ pub fn construct_legacy_mixnode_bonding_sign_payload( sender: Addr, pledge: Coin, mix_node: MixNode, - cost_params: MixNodeCostParams, + cost_params: NodeCostParams, ) -> SignableLegacyMixNodeBondingMsg { let payload = MixnodeBondingPayload::new(mix_node, cost_params); let content: LegacyContractMessageContent<_> = @@ -109,39 +109,48 @@ pub fn construct_legacy_gateway_bonding_sign_payload( } #[derive(Serialize)] -pub struct FamilyJoinPermit { - // the granter of this permit - family_head: FamilyHead, - // the actual member we want to permit to join - member_node: IdentityKey, +pub struct NymNodeBondingPayload { + nym_node: NymNode, + cost_params: NodeCostParams, } -impl FamilyJoinPermit { - pub fn new(family_head: FamilyHead, member_node: IdentityKey) -> Self { - Self { - family_head, - member_node, +impl NymNodeBondingPayload { + pub fn new(nym_node: NymNode, cost_params: NodeCostParams) -> Self { + NymNodeBondingPayload { + nym_node, + cost_params, } } } -impl SigningPurpose for FamilyJoinPermit { +impl SigningPurpose for NymNodeBondingPayload { fn message_type() -> MessageType { - MessageType::new("family-join-permit") + MessageType::new("nym-node-bonding") } } -pub fn construct_family_join_permit( +pub fn construct_nym_node_bonding_sign_payload( nonce: Nonce, - family_head: FamilyHead, - member_node: IdentityKey, -) -> SignableFamilyJoinPermitMsg { - let payload = FamilyJoinPermit::new(family_head, member_node); + sender: Addr, + pledge: Coin, + nym_node: NymNode, + cost_params: NodeCostParams, +) -> SignableNymNodeBondingMsg { + let payload = NymNodeBondingPayload::new(nym_node, cost_params); + let content = ContractMessageContent::new(sender, vec![pledge], payload); - // note: we're NOT wrapping it in `ContractMessageContent` because the family head is not going to be the one - // sending the message to the contract - SignableMessage::new(nonce, payload) + SignableMessage::new(nonce, content) } -// TODO: depending on our threat model, we should perhaps extend it to include all _on_behalf methods -// (update: but we trust our vesting contract since its compromise would be even more devastating so there's no need) +pub fn construct_generic_node_bonding_payload( + nonce: Nonce, + sender: Addr, + pledge: Coin, + payload: T, +) -> SignableMessage> +where + T: SigningPurpose, +{ + let content = ContractMessageContent::new(sender, vec![pledge], payload); + SignableMessage::new(nonce, content) +} diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs index f95c15eb34..35aac81f53 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs @@ -1,22 +1,127 @@ // Copyright 2021-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::error::MixnetContractError; -use crate::Layer; +use crate::nym_node::Role; use contracts_common::Percent; use cosmwasm_schema::cw_serde; use cosmwasm_std::Coin; use cosmwasm_std::{Addr, Uint128}; use std::fmt::{Display, Formatter}; -use std::ops::Index; // type aliases for better reasoning about available data pub type SphinxKey = String; pub type SphinxKeyRef<'a> = &'a str; -pub type MixId = u32; +pub type NodeId = u32; pub type BlockHeight = u64; +#[cw_serde] +pub struct RoleAssignment { + pub role: Role, + pub nodes: Vec, +} + +impl RoleAssignment { + pub fn is_final_assignment(&self) -> bool { + self.role.is_standby() + } +} + +#[cw_serde] +#[derive(Default)] +pub struct RewardedSet { + pub entry_gateways: Vec, + + pub exit_gateways: Vec, + + pub layer1: Vec, + + pub layer2: Vec, + + pub layer3: Vec, + + pub standby: Vec, +} + +impl RewardedSet { + pub fn is_empty(&self) -> bool { + self.entry_gateways.is_empty() + && self.exit_gateways.is_empty() + && self.layer1.is_empty() + && self.layer2.is_empty() + && self.layer3.is_empty() + && self.standby.is_empty() + } + + pub fn active_set_size(&self) -> usize { + self.entry_gateways.len() + + self.exit_gateways.len() + + self.layer1.len() + + self.layer2.len() + + self.layer3.len() + } + + pub fn rewarded_set_size(&self) -> usize { + self.active_set_size() + self.standby.len() + } + + pub fn try_get_mix_layer(&self, node_id: &NodeId) -> Option { + if self.layer1.contains(node_id) { + Some(1) + } else if self.layer2.contains(node_id) { + Some(2) + } else if self.layer3.contains(node_id) { + Some(3) + } else { + None + } + } + + pub fn is_entry(&self, node_id: &NodeId) -> bool { + self.entry_gateways.contains(node_id) + } + + pub fn is_exit(&self, node_id: &NodeId) -> bool { + self.exit_gateways.contains(node_id) + } + + pub fn is_active_mixnode(&self, node_id: &NodeId) -> bool { + self.layer1.contains(node_id) + || self.layer2.contains(node_id) + || self.layer3.contains(node_id) + } + + pub fn gateways(&self) -> Vec { + let mut gateways = Vec::with_capacity(self.entry_gateways.len() + self.exit_gateways.len()); + for entry in &self.entry_gateways { + gateways.push(*entry) + } + for exit in &self.exit_gateways { + gateways.push(*exit) + } + gateways + } + + pub fn active_mixnodes(&self) -> Vec { + let mut mixnodes = + Vec::with_capacity(self.layer1.len() + self.layer2.len() + self.layer3.len()); + for mix in &self.layer1 { + mixnodes.push(*mix) + } + for mix in &self.layer2 { + mixnodes.push(*mix) + } + for mix in &self.layer3 { + mixnodes.push(*mix) + } + mixnodes + } + + pub fn is_standby(&self, node_id: &NodeId) -> bool { + self.standby.contains(node_id) + } +} + #[cw_serde] pub struct RangedValue { pub minimum: T, @@ -76,113 +181,6 @@ where } } -/// Specifies layer assignment for the given mixnode. -#[cw_serde] -pub struct LayerAssignment { - /// The id of the mixnode. - mix_id: MixId, - - /// The layer to which it's going to be assigned - layer: Layer, -} - -impl LayerAssignment { - pub fn new(mix_id: MixId, layer: Layer) -> Self { - LayerAssignment { mix_id, layer } - } - - pub fn mix_id(&self) -> MixId { - self.mix_id - } - - pub fn layer(&self) -> Layer { - self.layer - } -} - -/// The current layer distribution of the mix network. -#[cw_serde] -#[derive(Copy, Default)] -pub struct LayerDistribution { - /// Number of nodes on the first layer. - pub layer1: u64, - - /// Number of nodes on the second layer. - pub layer2: u64, - - /// Number of nodes on the third layer. - pub layer3: u64, -} - -impl LayerDistribution { - pub fn choose_with_fewest(&self) -> Layer { - let layers = [ - (Layer::One, self.layer1), - (Layer::Two, self.layer2), - (Layer::Three, self.layer3), - ]; - - // we explicitly put 3 elements into the iterator, so the iterator is DEFINITELY - // not empty and thus the unwrap cannot fail - #[allow(clippy::unwrap_used)] - layers.iter().min_by_key(|x| x.1).unwrap().0 - } - - pub fn increment_layer_count(&mut self, layer: Layer) { - match layer { - Layer::One => self.layer1 += 1, - Layer::Two => self.layer2 += 1, - Layer::Three => self.layer3 += 1, - } - } - - pub fn decrement_layer_count(&mut self, layer: Layer) -> Result<(), MixnetContractError> { - match layer { - Layer::One => { - self.layer1 = - self.layer1 - .checked_sub(1) - .ok_or(MixnetContractError::OverflowSubtraction { - minuend: self.layer1, - subtrahend: 1, - })? - } - Layer::Two => { - self.layer2 = - self.layer2 - .checked_sub(1) - .ok_or(MixnetContractError::OverflowSubtraction { - minuend: self.layer2, - subtrahend: 1, - })? - } - Layer::Three => { - self.layer3 = - self.layer3 - .checked_sub(1) - .ok_or(MixnetContractError::OverflowSubtraction { - minuend: self.layer3, - subtrahend: 1, - })? - } - } - - Ok(()) - } -} - -impl Index for LayerDistribution { - type Output = u64; - - fn index(&self, index: Layer) -> &Self::Output { - match index { - Layer::One => &self.layer1, - Layer::Two => &self.layer2, - Layer::Three => &self.layer3, - } - } -} - /// The current state of the mixnet contract. #[cw_serde] pub struct ContractState { @@ -212,13 +210,10 @@ pub struct ContractState { #[cw_serde] pub struct ContractStateParams { /// Minimum amount a delegator must stake in orders for his delegation to get accepted. - pub minimum_mixnode_delegation: Option, + pub minimum_delegation: Option, - /// Minimum amount a mixnode must pledge to get into the system. - pub minimum_mixnode_pledge: Coin, - - /// Minimum amount a gateway must pledge to get into the system. - pub minimum_gateway_pledge: Coin, + /// Minimum amount a node must pledge to get into the system. + pub minimum_pledge: Coin, /// Defines the allowed profit margin range of operators. /// default: 0% - 100% diff --git a/common/cosmwasm-smart-contracts/vesting-contract/src/error.rs b/common/cosmwasm-smart-contracts/vesting-contract/src/error.rs index 65825c6ad9..da31bdf30a 100644 --- a/common/cosmwasm-smart-contracts/vesting-contract/src/error.rs +++ b/common/cosmwasm-smart-contracts/vesting-contract/src/error.rs @@ -3,7 +3,7 @@ use crate::account::VestingAccountStorageKey; use cosmwasm_std::{Addr, Coin, OverflowError, StdError, Uint128}; -use mixnet_contract_common::MixId; +use mixnet_contract_common::NodeId; use thiserror::Error; #[derive(Error, Debug, PartialEq)] @@ -50,7 +50,7 @@ pub enum VestingContractError { MultipleDenoms, #[error("VESTING ({}): No delegations found for account {0}, mix_identity {1}", line!())] - NoSuchDelegation(Addr, MixId), + NoSuchDelegation(Addr, NodeId), #[error("VESTING ({}): Only mixnet contract can perform this operation, got {0}", line!())] NotMixnetContract(Addr), @@ -95,7 +95,7 @@ pub enum VestingContractError { TooManyDelegations { address: Addr, acc_id: VestingAccountStorageKey, - mix_id: MixId, + mix_id: NodeId, num: u32, cap: u32, }, diff --git a/common/cosmwasm-smart-contracts/vesting-contract/src/lib.rs b/common/cosmwasm-smart-contracts/vesting-contract/src/lib.rs index 31fab7a7ef..741fde4f7b 100644 --- a/common/cosmwasm-smart-contracts/vesting-contract/src/lib.rs +++ b/common/cosmwasm-smart-contracts/vesting-contract/src/lib.rs @@ -6,7 +6,7 @@ use cosmwasm_schema::cw_serde; use cosmwasm_std::{Addr, Coin}; -use mixnet_contract_common::MixId; +use mixnet_contract_common::NodeId; pub mod account; pub mod error; @@ -64,7 +64,7 @@ pub struct DelegationTimesResponse { pub account_id: u32, /// Id of the mixnode towards which the delegation was made - pub mix_id: MixId, + pub mix_id: NodeId, /// All timestamps where a delegation was made pub delegation_timestamps: Vec, @@ -77,7 +77,7 @@ pub struct AllDelegationsResponse { pub delegations: Vec, /// Field indicating paging information for the following queries if the caller wishes to get further entries. - pub start_next_after: Option<(u32, MixId, u64)>, + pub start_next_after: Option<(u32, NodeId, u64)>, } /// Basic information regarding particular vesting account alongside the amount of vesting coins. diff --git a/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs b/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs index eefe07e9f1..303fe1e89f 100644 --- a/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs +++ b/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs @@ -5,11 +5,10 @@ use crate::{PledgeCap, VestingSpecification}; use contracts_common::signing::MessageSignature; use cosmwasm_schema::cw_serde; use cosmwasm_std::{Coin, Timestamp}; -use mixnet_contract_common::families::FamilyHead; use mixnet_contract_common::{ gateway::GatewayConfigUpdate, - mixnode::{MixNodeConfigUpdate, MixNodeCostParams}, - Gateway, IdentityKey, MixId, MixNode, + mixnode::{MixNodeConfigUpdate, NodeCostParams}, + Gateway, MixNode, NodeId, }; #[cfg(feature = "schema")] @@ -36,32 +35,16 @@ pub struct MigrateMsg {} #[cw_serde] pub enum ExecuteMsg { - // Families - /// Only owner of the node can crate the family with node as head - CreateFamily { - label: String, - }, - /// Family head needs to sign the joining node IdentityKey, the Node provides its signature signaling consent to join the family - JoinFamily { - join_permit: MessageSignature, - family_head: FamilyHead, - }, - LeaveFamily { - family_head: FamilyHead, - }, - KickFamilyMember { - member: IdentityKey, - }, TrackReward { amount: Coin, address: String, }, ClaimOperatorReward {}, ClaimDelegatorReward { - mix_id: MixId, + mix_id: NodeId, }, UpdateMixnodeCostParams { - new_costs: MixNodeCostParams, + new_costs: NodeCostParams, }, UpdateMixnodeConfig { new_config: MixNodeConfigUpdate, @@ -70,12 +53,12 @@ pub enum ExecuteMsg { address: String, }, DelegateToMixnode { - mix_id: MixId, + mix_id: NodeId, amount: Coin, on_behalf_of: Option, }, UndelegateFromMixnode { - mix_id: MixId, + mix_id: NodeId, on_behalf_of: Option, }, CreateAccount { @@ -89,12 +72,12 @@ pub enum ExecuteMsg { }, TrackUndelegation { owner: String, - mix_id: MixId, + mix_id: NodeId, amount: Coin, }, BondMixnode { mix_node: MixNode, - cost_params: MixNodeCostParams, + cost_params: NodeCostParams, owner_signature: MessageSignature, amount: Coin, }, @@ -142,17 +125,13 @@ pub enum ExecuteMsg { // no need to track migrated gateways as there are no vesting gateways on mainnet TrackMigratedDelegation { owner: String, - mix_id: MixId, + mix_id: NodeId, }, } impl ExecuteMsg { pub fn name(&self) -> &str { match self { - ExecuteMsg::CreateFamily { .. } => "VestingExecuteMsg::CreateFamily", - ExecuteMsg::JoinFamily { .. } => "VestingExecuteMsg::JoinFamily", - ExecuteMsg::LeaveFamily { .. } => "VestingExecuteMsg::LeaveFamily", - ExecuteMsg::KickFamilyMember { .. } => "VestingExecuteMsg::KickFamilyMember", ExecuteMsg::TrackReward { .. } => "VestingExecuteMsg::TrackReward", ExecuteMsg::ClaimOperatorReward { .. } => "VestingExecuteMsg::ClaimOperatorReward", ExecuteMsg::ClaimDelegatorReward { .. } => "VestingExecuteMsg::ClaimDelegatorReward", @@ -374,7 +353,7 @@ pub enum QueryMsg { address: String, /// Id of the mixnode towards which the delegation has been made. - mix_id: MixId, + mix_id: NodeId, /// Block timestamp of the delegation. block_timestamp_secs: u64, @@ -387,7 +366,7 @@ pub enum QueryMsg { address: String, /// Id of the mixnode towards which the delegations have been made. - mix_id: MixId, + mix_id: NodeId, }, /// Returns timestamps of delegations made towards particular mixnode by the provided vesting account address. @@ -397,14 +376,14 @@ pub enum QueryMsg { address: String, /// Id of the mixnode towards which the delegations have been made. - mix_id: MixId, + mix_id: NodeId, }, /// Returns all active delegations made with vesting tokens stored in this contract. #[cfg_attr(feature = "schema", returns(AllDelegationsResponse))] GetAllDelegations { /// Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response. - start_after: Option<(u32, MixId, u64)>, + start_after: Option<(u32, NodeId, u64)>, /// Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default. limit: Option, diff --git a/common/cosmwasm-smart-contracts/vesting-contract/src/types.rs b/common/cosmwasm-smart-contracts/vesting-contract/src/types.rs index e4c1644a94..da3adbbada 100644 --- a/common/cosmwasm-smart-contracts/vesting-contract/src/types.rs +++ b/common/cosmwasm-smart-contracts/vesting-contract/src/types.rs @@ -4,7 +4,7 @@ use contracts_common::Percent; use cosmwasm_schema::cw_serde; use cosmwasm_std::{Coin, Timestamp, Uint128}; -use mixnet_contract_common::MixId; +use mixnet_contract_common::NodeId; use std::str::FromStr; #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] @@ -155,7 +155,7 @@ pub struct VestingDelegation { pub account_id: u32, /// The id of the mixnode towards which the delegation has been made. - pub mix_id: MixId, + pub mix_id: NodeId, /// The block timestamp when the delegation has been made. pub block_timestamp: u64, @@ -165,7 +165,7 @@ pub struct VestingDelegation { } impl VestingDelegation { - pub fn storage_key(&self) -> (u32, MixId, u64) { + pub fn storage_key(&self) -> (u32, NodeId, u64) { (self.account_id, self.mix_id, self.block_timestamp) } } diff --git a/common/node-tester-utils/src/error.rs b/common/node-tester-utils/src/error.rs index 96337d0bc6..4c83ae7d3d 100644 --- a/common/node-tester-utils/src/error.rs +++ b/common/node-tester-utils/src/error.rs @@ -1,7 +1,7 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::MixId; +use crate::NodeId; use nym_sphinx::chunking::ChunkingError; use nym_sphinx::receiver::MessageRecoveryError; use nym_topology::NymTopologyError; @@ -19,7 +19,7 @@ pub enum NetworkTestingError { InvalidTopology(#[from] NymTopologyError), #[error("The specified mixnode (id: {mix_id}) doesn't exist")] - NonExistentMixnode { mix_id: MixId }, + NonExistentMixnode { mix_id: NodeId }, #[error("The specified mixnode (identity: {mix_identity}) doesn't exist")] NonExistentMixnodeIdentity { mix_identity: String }, diff --git a/common/node-tester-utils/src/lib.rs b/common/node-tester-utils/src/lib.rs index 91d2c7c03f..444d1780f8 100644 --- a/common/node-tester-utils/src/lib.rs +++ b/common/node-tester-utils/src/lib.rs @@ -15,7 +15,7 @@ pub use nym_sphinx::{ pub use tester::NodeTester; // it feels wrong to redefine it, but I don't want to import the whole of contract commons just for this one type -pub(crate) type MixId = u32; +pub(crate) type NodeId = u32; #[macro_export] macro_rules! log_err { diff --git a/common/node-tester-utils/src/message.rs b/common/node-tester-utils/src/message.rs index 41dc517956..b17515d5b6 100644 --- a/common/node-tester-utils/src/message.rs +++ b/common/node-tester-utils/src/message.rs @@ -3,6 +3,7 @@ use crate::error::NetworkTestingError; use crate::node::TestableNode; +use crate::NodeId; use nym_sphinx::message::NymMessage; use nym_topology::{gateway, mix}; use serde::de::DeserializeOwned; @@ -34,13 +35,13 @@ impl TestMessage { } } - pub fn new_mix(node: &mix::Node, msg_id: u32, total_msgs: u32, ext: T) -> Self { + pub fn new_mix(node: &mix::LegacyNode, msg_id: u32, total_msgs: u32, ext: T) -> Self { Self::new(node, msg_id, total_msgs, ext) } - pub fn new_gateway(node: &gateway::Node, msg_id: u32, total_msgs: u32, ext: T) -> Self { - Self::new(node, msg_id, total_msgs, ext) - } + // pub fn new_gateway(node: &gateway::Node, msg_id: u32, total_msgs: u32, ext: T) -> Self { + // Self::new(node, msg_id, total_msgs, ext) + // } pub fn new_serialized( node: N, @@ -72,7 +73,7 @@ impl TestMessage { } pub fn mix_plaintexts( - node: &mix::Node, + node: &mix::LegacyNode, total_msgs: u32, ext: T, ) -> Result>, NetworkTestingError> @@ -82,15 +83,16 @@ impl TestMessage { Self::new_plaintexts(node, total_msgs, ext) } - pub fn gateway_plaintexts( - node: &gateway::Node, + pub fn legacy_gateway_plaintexts( + node: &gateway::LegacyNode, + node_id: NodeId, total_msgs: u32, ext: T, ) -> Result>, NetworkTestingError> where T: Serialize + Clone, { - Self::new_plaintexts(node, total_msgs, ext) + Self::new_plaintexts(&(node, node_id), total_msgs, ext) } pub fn as_json_string(&self) -> Result diff --git a/common/node-tester-utils/src/node.rs b/common/node-tester-utils/src/node.rs index ed4aa3b52e..d60623a6e2 100644 --- a/common/node-tester-utils/src/node.rs +++ b/common/node-tester-utils/src/node.rs @@ -1,7 +1,7 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::MixId; +use crate::NodeId; use nym_topology::{gateway, mix}; use serde::{Deserialize, Serialize}; use std::fmt::{Display, Formatter}; @@ -9,27 +9,27 @@ use std::fmt::{Display, Formatter}; #[derive(Serialize, Deserialize, Debug, Clone, Hash, Eq, PartialEq)] pub struct TestableNode { pub encoded_identity: String, - pub owner: String, + pub node_id: NodeId, #[serde(rename = "type")] pub typ: NodeType, } impl TestableNode { - pub fn new(encoded_identity: String, owner: String, typ: NodeType) -> Self { + pub fn new(encoded_identity: String, typ: NodeType, node_id: NodeId) -> Self { TestableNode { encoded_identity, - owner, + node_id, typ, } } - pub fn new_mixnode(encoded_identity: String, owner: String, mix_id: MixId) -> Self { - TestableNode::new(encoded_identity, owner, NodeType::Mixnode { mix_id }) + pub fn new_mixnode(encoded_identity: String, node_id: NodeId) -> Self { + TestableNode::new(encoded_identity, NodeType::Mixnode, node_id) } - pub fn new_gateway(encoded_identity: String, owner: String) -> Self { - TestableNode::new(encoded_identity, owner, NodeType::Gateway) + pub fn new_gateway(encoded_identity: String, node_id: NodeId) -> Self { + TestableNode::new(encoded_identity, NodeType::Gateway, node_id) } pub fn is_mixnode(&self) -> bool { @@ -37,24 +37,34 @@ impl TestableNode { } } -impl<'a> From<&'a mix::Node> for TestableNode { - fn from(value: &'a mix::Node) -> Self { +impl<'a> From<&'a mix::LegacyNode> for TestableNode { + fn from(value: &'a mix::LegacyNode) -> Self { TestableNode { encoded_identity: value.identity_key.to_base58_string(), - owner: value.owner.as_ref().cloned().unwrap_or_default(), - typ: NodeType::Mixnode { - mix_id: value.mix_id, - }, + typ: NodeType::Mixnode, + node_id: value.mix_id, } } } -impl<'a> From<&'a gateway::Node> for TestableNode { - fn from(value: &'a gateway::Node) -> Self { +impl<'a> From<(&'a gateway::LegacyNode, NodeId)> for TestableNode { + fn from((gateway, node_id): (&'a gateway::LegacyNode, NodeId)) -> Self { + (&(gateway, node_id)).into() + } +} + +impl<'a> From<&'a (gateway::LegacyNode, NodeId)> for TestableNode { + fn from((gateway, node_id): &'a (gateway::LegacyNode, NodeId)) -> Self { + (gateway, *node_id).into() + } +} + +impl<'a, 'b> From<&'a (&'b gateway::LegacyNode, NodeId)> for TestableNode { + fn from((gateway, node_id): &'a (&'b gateway::LegacyNode, NodeId)) -> Self { TestableNode { - encoded_identity: value.identity_key.to_base58_string(), - owner: value.owner.as_ref().cloned().unwrap_or_default(), + encoded_identity: gateway.identity_key.to_base58_string(), typ: NodeType::Gateway, + node_id: *node_id, } } } @@ -63,8 +73,8 @@ impl Display for TestableNode { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!( f, - "{} {} owned by {}", - self.typ, self.encoded_identity, self.owner + "{}-{}: {}", + self.typ, self.node_id, self.encoded_identity ) } } @@ -72,7 +82,7 @@ impl Display for TestableNode { #[derive(Serialize, Deserialize, Hash, Clone, Copy, Debug, Eq, PartialEq)] #[serde(rename_all = "snake_case")] pub enum NodeType { - Mixnode { mix_id: MixId }, + Mixnode, Gateway, } @@ -85,7 +95,7 @@ impl NodeType { impl Display for NodeType { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { - NodeType::Mixnode { mix_id } => write!(f, "mixnode (mix_id {mix_id})"), + NodeType::Mixnode => write!(f, "mixnode"), NodeType::Gateway => write!(f, "gateway"), } } diff --git a/common/node-tester-utils/src/tester.rs b/common/node-tester-utils/src/tester.rs index e31d632cc7..2697c60831 100644 --- a/common/node-tester-utils/src/tester.rs +++ b/common/node-tester-utils/src/tester.rs @@ -3,7 +3,7 @@ use crate::error::NetworkTestingError; use crate::Empty; -use crate::MixId; +use crate::NodeId; use crate::TestMessage; use nym_sphinx::acknowledgements::AckKey; use nym_sphinx::addressing::clients::Recipient; @@ -76,13 +76,13 @@ where self } - pub fn testable_mix_topology(&self, node: &mix::Node) -> NymTopology { + pub fn testable_mix_topology(&self, node: &mix::LegacyNode) -> NymTopology { let mut topology = self.base_topology.clone(); topology.set_mixes_in_layer(node.layer as u8, vec![node.clone()]); topology } - pub fn testable_gateway_topology(&self, gateway: &gateway::Node) -> NymTopology { + pub fn testable_gateway_topology(&self, gateway: &gateway::LegacyNode) -> NymTopology { let mut topology = self.base_topology.clone(); topology.set_gateways(vec![gateway.clone()]); topology @@ -90,7 +90,7 @@ where pub fn simple_mixnode_test_packets( &mut self, - mix: &mix::Node, + mix: &mix::LegacyNode, test_packets: u32, ) -> Result, NetworkTestingError> { self.mixnode_test_packets(mix, Empty, test_packets, None) @@ -98,7 +98,7 @@ where pub fn mixnode_test_packets( &mut self, - mix: &mix::Node, + mix: &mix::LegacyNode, msg_ext: T, test_packets: u32, custom_recipient: Option, @@ -122,7 +122,7 @@ where pub fn mixnodes_test_packets( &mut self, - nodes: &[mix::Node], + nodes: &[mix::LegacyNode], msg_ext: T, test_packets: u32, custom_recipient: Option, @@ -145,7 +145,7 @@ where pub fn existing_mixnode_test_packets( &mut self, - mix_id: MixId, + mix_id: NodeId, msg_ext: T, test_packets: u32, custom_recipient: Option, @@ -182,9 +182,10 @@ where self.mixnode_test_packets(&node.clone(), msg_ext, test_packets, custom_recipient) } - pub fn gateway_test_packets( + pub fn legacy_gateway_test_packets( &mut self, - gateway: &gateway::Node, + gateway: &gateway::LegacyNode, + node_id: NodeId, msg_ext: T, test_packets: u32, custom_recipient: Option, @@ -195,7 +196,9 @@ where let ephemeral_topology = self.testable_gateway_topology(gateway); let mut packets = Vec::with_capacity(test_packets as usize); - for plaintext in TestMessage::gateway_plaintexts(gateway, test_packets, msg_ext)? { + for plaintext in + TestMessage::legacy_gateway_plaintexts(gateway, node_id, test_packets, msg_ext)? + { packets.push(self.wrap_plaintext_data( plaintext, &ephemeral_topology, @@ -208,6 +211,7 @@ where pub fn existing_gateway_test_packets( &mut self, + node_id: NodeId, encoded_gateway_identity: String, msg_ext: T, test_packets: u32, @@ -222,7 +226,13 @@ where }); }; - self.gateway_test_packets(&node.clone(), msg_ext, test_packets, custom_recipient) + self.legacy_gateway_test_packets( + &node.clone(), + node_id, + msg_ext, + test_packets, + custom_recipient, + ) } pub fn wrap_plaintext_data( diff --git a/common/nymsphinx/src/receiver.rs b/common/nymsphinx/src/receiver.rs index ac1857fd82..9799d774fd 100644 --- a/common/nymsphinx/src/receiver.rs +++ b/common/nymsphinx/src/receiver.rs @@ -220,7 +220,7 @@ impl Default for SphinxMessageReceiver { mod message_receiver { use super::*; use nym_crypto::asymmetric::identity; - use nym_mixnet_contract_common::Layer; + use nym_mixnet_contract_common::LegacyMixLayer; use nym_topology::{gateway, mix, NymTopology}; use std::collections::BTreeMap; @@ -233,7 +233,7 @@ mod message_receiver { let mut mixes = BTreeMap::new(); mixes.insert( 1, - vec![mix::Node { + vec![mix::LegacyNode { mix_id: 123, owner: None, host: "10.20.30.40".parse().unwrap(), @@ -246,14 +246,14 @@ mod message_receiver { "B3GzG62aXAZNg14RoMCp3BhELNBrySLr2JqrwyfYFzRc", ) .unwrap(), - layer: Layer::One, + layer: LegacyMixLayer::One, version: "0.8.0-dev".into(), }], ); mixes.insert( 2, - vec![mix::Node { + vec![mix::LegacyNode { mix_id: 234, owner: None, host: "11.21.31.41".parse().unwrap(), @@ -266,14 +266,14 @@ mod message_receiver { "5Z1VqYwM2xeKxd8H7fJpGWasNiDFijYBAee7MErkZ5QT", ) .unwrap(), - layer: Layer::Two, + layer: LegacyMixLayer::Two, version: "0.8.0-dev".into(), }], ); mixes.insert( 3, - vec![mix::Node { + vec![mix::LegacyNode { mix_id: 456, owner: None, host: "12.22.32.42".parse().unwrap(), @@ -286,7 +286,7 @@ mod message_receiver { "9EyjhCggr2QEA2nakR88YHmXgpy92DWxoe2draDRkYof", ) .unwrap(), - layer: Layer::Three, + layer: LegacyMixLayer::Three, version: "0.8.0-dev".into(), }], ); @@ -294,7 +294,7 @@ mod message_receiver { NymTopology::new( // currently coco_nodes don't really exist so this is still to be determined mixes, - vec![gateway::Node { + vec![gateway::LegacyNode { owner: None, host: "1.2.3.4".parse().unwrap(), mix_host: "1.2.3.4:1789".parse().unwrap(), diff --git a/common/topology/src/gateway.rs b/common/topology/src/gateway.rs index ae20786400..3596b7e38f 100644 --- a/common/topology/src/gateway.rs +++ b/common/topology/src/gateway.rs @@ -2,13 +2,11 @@ // SPDX-License-Identifier: Apache-2.0 use crate::{filter, NetworkAddress, NodeVersion}; -use nym_api_requests::models::DescribedGateway; +use nym_api_requests::nym_nodes::SkimmedNode; use nym_crypto::asymmetric::{encryption, identity}; -use nym_mixnet_contract_common::GatewayBond; +use nym_mixnet_contract_common::NodeId; use nym_sphinx_addressing::nodes::{NodeIdentity, NymNodeRoutingAddress}; use nym_sphinx_types::Node as SphinxNode; - -use nym_api_requests::nym_nodes::SkimmedNode; use rand::seq::SliceRandom; use rand::thread_rng; use std::fmt; @@ -49,7 +47,9 @@ pub enum GatewayConversionError { } #[derive(Clone)] -pub struct Node { +pub struct LegacyNode { + pub node_id: NodeId, + pub host: NetworkAddress, // we're keeping this as separate resolved field since we do not want to be resolving the potential // hostname every time we want to construct a path via this node @@ -65,15 +65,13 @@ pub struct Node { pub sphinx_key: encryption::PublicKey, // TODO: or nymsphinx::PublicKey? both are x25519 // to be removed: - pub owner: Option, pub version: NodeVersion, } -impl std::fmt::Debug for Node { +impl std::fmt::Debug for LegacyNode { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { f.debug_struct("gateway::Node") .field("host", &self.host) - .field("owner", &self.owner) .field("mix_host", &self.mix_host) .field("clients_ws_port", &self.clients_ws_port) .field("clients_wss_port", &self.clients_wss_port) @@ -84,7 +82,7 @@ impl std::fmt::Debug for Node { } } -impl Node { +impl LegacyNode { pub fn parse_host(raw: &str) -> Result { // safety: this conversion is infallible // (but we retain result return type for legacy reasons) @@ -122,25 +120,21 @@ impl Node { } } -impl fmt::Display for Node { +impl fmt::Display for LegacyNode { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "Node(id: {}, owner: {:?}, host: {})", - self.identity_key, self.owner, self.host, - ) + write!(f, "legacy gateway {} @ {}", self.node_id, self.host) } } -impl filter::Versioned for Node { +impl filter::Versioned for LegacyNode { fn version(&self) -> String { // TODO: return semver instead self.version.to_string() } } -impl<'a> From<&'a Node> for SphinxNode { - fn from(node: &'a Node) -> Self { +impl<'a> From<&'a LegacyNode> for SphinxNode { + fn from(node: &'a LegacyNode) -> Self { let node_address_bytes = NymNodeRoutingAddress::from(node.mix_host) .try_into() .unwrap(); @@ -149,83 +143,7 @@ impl<'a> From<&'a Node> for SphinxNode { } } -impl<'a> TryFrom<&'a GatewayBond> for Node { - type Error = GatewayConversionError; - - fn try_from(bond: &'a GatewayBond) -> Result { - let host = Self::parse_host(&bond.gateway.host)?; - - // try to completely resolve the host in the mix situation to avoid doing it every - // single time we want to construct a path - let mix_host = Self::extract_mix_host(&host, bond.gateway.mix_port)?; - - Ok(Node { - owner: Some(bond.owner.as_str().to_owned()), - host, - mix_host, - clients_ws_port: bond.gateway.clients_port, - clients_wss_port: None, - identity_key: identity::PublicKey::from_base58_string(&bond.gateway.identity_key)?, - sphinx_key: encryption::PublicKey::from_base58_string(&bond.gateway.sphinx_key)?, - version: bond.gateway.version.as_str().into(), - }) - } -} - -impl TryFrom for Node { - type Error = GatewayConversionError; - - fn try_from(bond: GatewayBond) -> Result { - Node::try_from(&bond) - } -} - -impl<'a> TryFrom<&'a DescribedGateway> for Node { - type Error = GatewayConversionError; - - fn try_from(value: &'a DescribedGateway) -> Result { - let Some(ref self_described) = value.self_described else { - return (&value.bond).try_into(); - }; - - let ips = &self_described.host_information.ip_address; - if ips.is_empty() { - return Err(GatewayConversionError::NoIpAddressesProvided { - gateway: value.bond.gateway.identity_key.clone(), - }); - } - - let host = match &self_described.host_information.hostname { - None => NetworkAddress::IpAddr(ips[0]), - Some(hostname) => NetworkAddress::Hostname(hostname.clone()), - }; - - // get ip from the self-reported values so we wouldn't need to do any hostname resolution - // (which doesn't really work in wasm) - let mix_host = SocketAddr::new(ips[0], value.bond.gateway.mix_port); - - Ok(Node { - owner: Some(value.bond.owner.as_str().to_owned()), - host, - mix_host, - clients_ws_port: self_described.mixnet_websockets.ws_port, - clients_wss_port: self_described.mixnet_websockets.wss_port, - identity_key: identity::PublicKey::from_base58_string( - &self_described.host_information.keys.ed25519, - )?, - sphinx_key: encryption::PublicKey::from_base58_string( - &self_described.host_information.keys.x25519, - )?, - version: self_described - .build_information - .build_version - .as_str() - .into(), - }) - } -} - -impl<'a> TryFrom<&'a SkimmedNode> for Node { +impl<'a> TryFrom<&'a SkimmedNode> for LegacyNode { type Error = GatewayConversionError; fn try_from(value: &'a SkimmedNode) -> Result { @@ -249,23 +167,15 @@ impl<'a> TryFrom<&'a SkimmedNode> for Node { NetworkAddress::IpAddr(*ip) }; - Ok(Node { + Ok(LegacyNode { + node_id: value.node_id, host, mix_host: SocketAddr::new(*ip, value.mix_port), clients_ws_port: entry_details.ws_port, clients_wss_port: entry_details.wss_port, identity_key: value.ed25519_identity_pubkey.parse()?, sphinx_key: value.x25519_sphinx_pubkey.parse()?, - owner: None, version: NodeVersion::Unknown, }) } } - -impl TryFrom for Node { - type Error = GatewayConversionError; - - fn try_from(value: DescribedGateway) -> Result { - Node::try_from(&value) - } -} diff --git a/common/topology/src/lib.rs b/common/topology/src/lib.rs index 5630122d0c..62c0c378bc 100644 --- a/common/topology/src/lib.rs +++ b/common/topology/src/lib.rs @@ -7,17 +7,15 @@ use crate::filter::VersionFilterable; pub use error::NymTopologyError; use log::{debug, info, warn}; -use mix::Node; +use nym_api_requests::nym_nodes::{CachedNodesResponse, SkimmedNode}; use nym_config::defaults::var_names::NYM_API; -use nym_mixnet_contract_common::mixnode::MixNodeDetails; -use nym_mixnet_contract_common::{GatewayBond, IdentityKeyRef, MixId}; +use nym_mixnet_contract_common::{IdentityKeyRef, NodeId}; use nym_sphinx_addressing::nodes::NodeIdentity; use nym_sphinx_types::Node as SphinxNode; use rand::prelude::SliceRandom; use rand::{CryptoRng, Rng}; use std::collections::BTreeMap; use std::convert::Infallible; - use std::fmt::{self, Display, Formatter}; use std::io; use std::net::{IpAddr, SocketAddr, ToSocketAddrs}; @@ -25,7 +23,6 @@ use std::str::FromStr; #[cfg(feature = "serializable")] use ::serde::{Deserialize, Deserializer, Serialize, Serializer}; -use nym_api_requests::models::DescribedGateway; pub mod error; pub mod filter; @@ -118,45 +115,54 @@ impl Display for NetworkAddress { pub type MixLayer = u8; +// the reason for those having `Legacy` prefix is that eventually they should be using +// exactly the same types #[derive(Debug, Clone, Default)] pub struct NymTopology { - mixes: BTreeMap>, - gateways: Vec, + mixes: BTreeMap>, + gateways: Vec, } impl NymTopology { pub async fn new_from_env() -> Result { let api_url = std::env::var(NYM_API)?; - info!("Generating topology from {}", api_url); + info!("Generating topology from {api_url}"); - let mixnodes = reqwest::get(&format!("{}/v1/mixnodes", api_url)) + let mixnodes = reqwest::get(&format!("{api_url}/v1/unstable/nym-nodes/mixnodes/skimmed",)) .await? - .json::>() + .json::>() .await? - .into_iter() - .map(|details| details.bond_information) - .map(mix::Node::try_from) + .nodes + .iter() + .map(mix::LegacyNode::try_from) .filter(Result::is_ok) .collect::, _>>()?; - let gateways = reqwest::get(&format!("{}/v1/gateways", api_url)) + let gateways = reqwest::get(&format!("{api_url}/v1/unstable/nym-nodes/gateways/skimmed",)) .await? - .json::>() + .json::>() .await? - .into_iter() - .map(gateway::Node::try_from) + .nodes + .iter() + .map(gateway::LegacyNode::try_from) .filter(Result::is_ok) .collect::, _>>()?; let topology = NymTopology::new_unordered(mixnodes, gateways); Ok(topology) } - pub fn new(mixes: BTreeMap>, gateways: Vec) -> Self { + pub fn new( + mixes: BTreeMap>, + gateways: Vec, + ) -> Self { NymTopology { mixes, gateways } } - pub fn new_unordered(unordered_mixes: Vec, gateways: Vec) -> Self { + pub fn new_unordered( + unordered_mixes: Vec, + gateways: Vec, + ) -> Self { let mut mixes = BTreeMap::new(); for node in unordered_mixes.into_iter() { let layer = node.layer as MixLayer; @@ -171,10 +177,10 @@ impl NymTopology { where MI: Iterator, GI: Iterator, - G: TryInto, - M: TryInto, - >::Error: Display, - >::Error: Display, + G: TryInto, + M: TryInto, + >::Error: Display, + >::Error: Display, { let mut mixes = BTreeMap::new(); let mut gateways = Vec::new(); @@ -205,14 +211,11 @@ impl NymTopology { serde_json::from_reader(file).map_err(Into::into) } - pub fn from_detailed( - mix_details: Vec, - gateway_bonds: Vec, - ) -> Self { - nym_topology_from_detailed(mix_details, gateway_bonds) + pub fn from_basic(basic_mixes: &[SkimmedNode], basic_gateways: &[SkimmedNode]) -> Self { + nym_topology_from_basic_info(basic_mixes, basic_gateways) } - pub fn find_mix(&self, mix_id: MixId) -> Option<&mix::Node> { + pub fn find_mix(&self, mix_id: NodeId) -> Option<&mix::LegacyNode> { for nodes in self.mixes.values() { for node in nodes { if node.mix_id == mix_id { @@ -223,7 +226,10 @@ impl NymTopology { None } - pub fn find_mix_by_identity(&self, mixnode_identity: IdentityKeyRef) -> Option<&mix::Node> { + pub fn find_mix_by_identity( + &self, + mixnode_identity: IdentityKeyRef, + ) -> Option<&mix::LegacyNode> { for nodes in self.mixes.values() { for node in nodes { if node.identity_key.to_base58_string() == mixnode_identity { @@ -234,13 +240,13 @@ impl NymTopology { None } - pub fn find_gateway(&self, gateway_identity: IdentityKeyRef) -> Option<&gateway::Node> { + pub fn find_gateway(&self, gateway_identity: IdentityKeyRef) -> Option<&gateway::LegacyNode> { self.gateways .iter() .find(|&gateway| gateway.identity_key.to_base58_string() == gateway_identity) } - pub fn mixes(&self) -> &BTreeMap> { + pub fn mixes(&self) -> &BTreeMap> { &self.mixes } @@ -248,8 +254,8 @@ impl NymTopology { self.mixes.values().map(|m| m.len()).sum() } - pub fn mixes_as_vec(&self) -> Vec { - let mut mixes: Vec = vec![]; + pub fn mixes_as_vec(&self) -> Vec { + let mut mixes: Vec = vec![]; for layer in self.mixes().values() { mixes.extend(layer.to_owned()) @@ -257,20 +263,20 @@ impl NymTopology { mixes } - pub fn mixes_in_layer(&self, layer: MixLayer) -> Vec { + pub fn mixes_in_layer(&self, layer: MixLayer) -> Vec { assert!([1, 2, 3].contains(&layer)); self.mixes.get(&layer).unwrap().to_owned() } - pub fn gateways(&self) -> &[gateway::Node] { + pub fn gateways(&self) -> &[gateway::LegacyNode] { &self.gateways } - pub fn get_gateways(&self) -> Vec { + pub fn get_gateways(&self) -> Vec { self.gateways.clone() } - pub fn get_gateway(&self, gateway_identity: &NodeIdentity) -> Option<&gateway::Node> { + pub fn get_gateway(&self, gateway_identity: &NodeIdentity) -> Option<&gateway::LegacyNode> { self.gateways .iter() .find(|gateway| gateway.identity() == gateway_identity) @@ -280,11 +286,11 @@ impl NymTopology { self.get_gateway(gateway_identity).is_some() } - pub fn set_gateways(&mut self, gateways: Vec) { + pub fn set_gateways(&mut self, gateways: Vec) { self.gateways = gateways } - pub fn random_gateway(&self, rng: &mut R) -> Result<&gateway::Node, NymTopologyError> + pub fn random_gateway(&self, rng: &mut R) -> Result<&gateway::LegacyNode, NymTopologyError> where R: Rng + CryptoRng, { @@ -299,7 +305,7 @@ impl NymTopology { &self, rng: &mut R, num_mix_hops: u8, - ) -> Result, NymTopologyError> + ) -> Result, NymTopologyError> where R: Rng + CryptoRng + ?Sized, { @@ -335,7 +341,7 @@ impl NymTopology { rng: &mut R, num_mix_hops: u8, gateway_identity: &NodeIdentity, - ) -> Result<(Vec, gateway::Node), NymTopologyError> + ) -> Result<(Vec, gateway::LegacyNode), NymTopologyError> where R: Rng + CryptoRng + ?Sized, { @@ -376,7 +382,7 @@ impl NymTopology { } /// Overwrites the existing nodes in the specified layer - pub fn set_mixes_in_layer(&mut self, layer: u8, mixes: Vec) { + pub fn set_mixes_in_layer(&mut self, layer: u8, mixes: Vec) { self.mixes.insert(layer, mixes); } @@ -491,66 +497,33 @@ impl<'de> Deserialize<'de> for NymTopology { } } -pub trait IntoGatewayNode: TryInto -where - >::Error: Display, -{ - fn identity(&self) -> IdentityKeyRef; -} - -impl IntoGatewayNode for GatewayBond { - fn identity(&self) -> IdentityKeyRef { - &self.gateway.identity_key - } -} - -impl IntoGatewayNode for DescribedGateway { - fn identity(&self) -> IdentityKeyRef { - &self.bond.gateway.identity_key - } -} - -pub fn nym_topology_from_detailed( - mix_details: Vec, - gateway_bonds: Vec, -) -> NymTopology -where - G: IntoGatewayNode, - >::Error: Display, -{ +pub fn nym_topology_from_basic_info( + basic_mixes: &[SkimmedNode], + basic_gateways: &[SkimmedNode], +) -> NymTopology { let mut mixes = BTreeMap::new(); - for bond in mix_details - .into_iter() - .map(|details| details.bond_information) - { - let layer = bond.layer as MixLayer; - if layer == 0 || layer > 3 { - warn!( - "{} says it's on invalid layer {layer}!", - bond.mix_node.identity_key - ); + for mix in basic_mixes { + let Some(layer) = mix.get_mix_layer() else { + warn!("node {} doesn't have any assigned mix layer!", mix.node_id); continue; - } - let mix_id = bond.mix_id; - let mix_identity = bond.mix_node.identity_key.clone(); + }; let layer_entry = mixes.entry(layer).or_insert_with(Vec::new); - match bond.try_into() { + match mix.try_into() { Ok(mix) => layer_entry.push(mix), Err(err) => { - warn!("Mix {mix_id} / {mix_identity} is malformed: {err}"); + warn!("node (mixnode) {} is malformed: {err}", mix.node_id); continue; } } } - let mut gateways = Vec::with_capacity(gateway_bonds.len()); - for bond in gateway_bonds.into_iter() { - let gate_id = bond.identity().to_owned(); - match bond.try_into() { + let mut gateways = Vec::with_capacity(basic_gateways.len()); + for gateway in basic_gateways { + match gateway.try_into() { Ok(gate) => gateways.push(gate), Err(err) => { - warn!("Gateway {gate_id} is malformed: {err}"); + warn!("node (gateway) {} is malformed: {err}", gateway.node_id); continue; } } @@ -568,13 +541,12 @@ mod converting_mixes_to_vec { use nym_crypto::asymmetric::{encryption, identity}; use super::*; - use nym_mixnet_contract_common::Layer; + use nym_mixnet_contract_common::LegacyMixLayer; #[test] fn returns_a_vec_with_hashmap_values() { - let node1 = mix::Node { + let node1 = mix::LegacyNode { mix_id: 42, - owner: Some("N/A".to_string()), host: "3.3.3.3".parse().unwrap(), mix_host: "3.3.3.3:1789".parse().unwrap(), identity_key: identity::PublicKey::from_base58_string( @@ -585,21 +557,15 @@ mod converting_mixes_to_vec { "C7cown6dYCLZpLiMFC1PaBmhvLvmJmLDJGeRTbPD45bX", ) .unwrap(), - layer: Layer::One, + layer: LegacyMixLayer::One, version: "0.2.0".into(), }; - let node2 = mix::Node { - owner: Some("Alice".to_string()), - ..node1.clone() - }; + let node2 = mix::LegacyNode { ..node1.clone() }; - let node3 = mix::Node { - owner: Some("Bob".to_string()), - ..node1.clone() - }; + let node3 = mix::LegacyNode { ..node1.clone() }; - let mut mixes: BTreeMap> = BTreeMap::new(); + let mut mixes = BTreeMap::new(); mixes.insert(1, vec![node1, node2]); mixes.insert(2, vec![node3]); @@ -607,7 +573,8 @@ mod converting_mixes_to_vec { let mixvec = topology.mixes_as_vec(); assert!(mixvec .iter() - .any(|node| node.owner.as_ref() == Some(&"N/A".to_string()))); + .any(|node| &node.identity_key.to_base58_string() + == "3ebjp1Fb9hdcS1AR6AZihgeJiMHkB5jjJUsvqNnfQwU7")); } } diff --git a/common/topology/src/mix.rs b/common/topology/src/mix.rs index c1f6efeaa0..6a0ff57ec3 100644 --- a/common/topology/src/mix.rs +++ b/common/topology/src/mix.rs @@ -2,13 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 use crate::{filter, NetworkAddress, NodeVersion}; +use nym_api_requests::nym_nodes::{NodeRole, SkimmedNode}; use nym_crypto::asymmetric::{encryption, identity}; -pub use nym_mixnet_contract_common::Layer; -use nym_mixnet_contract_common::{MixId, MixNodeBond}; +pub use nym_mixnet_contract_common::LegacyMixLayer; +use nym_mixnet_contract_common::NodeId; use nym_sphinx_addressing::nodes::NymNodeRoutingAddress; use nym_sphinx_types::Node as SphinxNode; - -use nym_api_requests::nym_nodes::{NodeRole, SkimmedNode}; use rand::seq::SliceRandom; use rand::thread_rng; use std::fmt::Formatter; @@ -42,26 +41,24 @@ pub enum MixnodeConversionError { } #[derive(Clone)] -pub struct Node { - pub mix_id: MixId, +pub struct LegacyNode { + pub mix_id: NodeId, pub host: NetworkAddress, // we're keeping this as separate resolved field since we do not want to be resolving the potential // hostname every time we want to construct a path via this node pub mix_host: SocketAddr, pub identity_key: identity::PublicKey, pub sphinx_key: encryption::PublicKey, // TODO: or nymsphinx::PublicKey? both are x25519 - pub layer: Layer, + pub layer: LegacyMixLayer, // to be removed: pub version: NodeVersion, - pub owner: Option, } -impl std::fmt::Debug for Node { +impl std::fmt::Debug for LegacyNode { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_struct("mix::Node") .field("mix_id", &self.mix_id) - .field("owner", &self.owner) .field("host", &self.host) .field("mix_host", &self.mix_host) .field("identity_key", &self.identity_key.to_base58_string()) @@ -72,7 +69,7 @@ impl std::fmt::Debug for Node { } } -impl Node { +impl LegacyNode { pub fn parse_host(raw: &str) -> Result { // safety: this conversion is infallible // (but we retain result return type for legacy reasons) @@ -92,15 +89,15 @@ impl Node { } } -impl filter::Versioned for Node { +impl filter::Versioned for LegacyNode { fn version(&self) -> String { // TODO: return semver instead self.version.to_string() } } -impl<'a> From<&'a Node> for SphinxNode { - fn from(node: &'a Node) -> Self { +impl<'a> From<&'a LegacyNode> for SphinxNode { + fn from(node: &'a LegacyNode) -> Self { let node_address_bytes = NymNodeRoutingAddress::from(node.mix_host) .try_into() .unwrap(); @@ -109,30 +106,7 @@ impl<'a> From<&'a Node> for SphinxNode { } } -impl<'a> TryFrom<&'a MixNodeBond> for Node { - type Error = MixnodeConversionError; - - fn try_from(bond: &'a MixNodeBond) -> Result { - let host = Self::parse_host(&bond.mix_node.host)?; - - // try to completely resolve the host in the mix situation to avoid doing it every - // single time we want to construct a path - let mix_host = Self::extract_mix_host(&host, bond.mix_node.mix_port)?; - - Ok(Node { - mix_id: bond.mix_id, - owner: Some(bond.owner.as_str().to_owned()), - host, - mix_host, - identity_key: identity::PublicKey::from_base58_string(&bond.mix_node.identity_key)?, - sphinx_key: encryption::PublicKey::from_base58_string(&bond.mix_node.sphinx_key)?, - layer: bond.layer, - version: bond.mix_node.version.as_str().into(), - }) - } -} - -impl<'a> TryFrom<&'a SkimmedNode> for Node { +impl<'a> TryFrom<&'a SkimmedNode> for LegacyNode { type Error = MixnodeConversionError; fn try_from(value: &'a SkimmedNode) -> Result { @@ -155,23 +129,14 @@ impl<'a> TryFrom<&'a SkimmedNode> for Node { let host = NetworkAddress::IpAddr(*ip); - Ok(Node { + Ok(LegacyNode { mix_id: value.node_id, host, mix_host: SocketAddr::new(*ip, value.mix_port), identity_key: value.ed25519_identity_pubkey.parse()?, sphinx_key: value.x25519_sphinx_pubkey.parse()?, layer, - owner: None, version: NodeVersion::Unknown, }) } } - -impl TryFrom for Node { - type Error = MixnodeConversionError; - - fn try_from(bond: MixNodeBond) -> Result { - Node::try_from(&bond) - } -} diff --git a/common/topology/src/serde.rs b/common/topology/src/serde.rs index b68dd470bc..601b78dfd3 100644 --- a/common/topology/src/serde.rs +++ b/common/topology/src/serde.rs @@ -20,6 +20,7 @@ use thiserror::Error; #[cfg(feature = "wasm-serde-types")] use tsify::Tsify; +use nym_mixnet_contract_common::NodeId; #[cfg(feature = "wasm-serde-types")] use wasm_bindgen::{prelude::wasm_bindgen, JsValue}; @@ -109,9 +110,6 @@ pub struct SerializableMixNode { #[serde(alias = "mix_id")] pub mix_id: u32, - #[cfg_attr(feature = "wasm-serde-types", tsify(optional))] - pub owner: Option, - pub host: String, #[cfg_attr(feature = "wasm-serde-types", tsify(optional))] @@ -131,40 +129,38 @@ pub struct SerializableMixNode { pub version: Option, } -impl TryFrom for mix::Node { +impl TryFrom for mix::LegacyNode { type Error = SerializableTopologyError; fn try_from(value: SerializableMixNode) -> Result { - let host = mix::Node::parse_host(&value.host)?; + let host = mix::LegacyNode::parse_host(&value.host)?; let mix_port = value.mix_port.unwrap_or(DEFAULT_MIX_LISTENING_PORT); let version = value.version.map(|v| v.as_str().into()).unwrap_or_default(); // try to completely resolve the host in the mix situation to avoid doing it every // single time we want to construct a path - let mix_host = mix::Node::extract_mix_host(&host, mix_port)?; + let mix_host = mix::LegacyNode::extract_mix_host(&host, mix_port)?; - Ok(mix::Node { + Ok(mix::LegacyNode { mix_id: value.mix_id, - owner: value.owner, host, mix_host, identity_key: identity::PublicKey::from_base58_string(&value.identity_key) .map_err(MixnodeConversionError::from)?, sphinx_key: encryption::PublicKey::from_base58_string(&value.sphinx_key) .map_err(MixnodeConversionError::from)?, - layer: mix::Layer::try_from(value.layer) + layer: mix::LegacyMixLayer::try_from(value.layer) .map_err(|_| SerializableTopologyError::InvalidMixLayer { value: value.layer })?, version, }) } } -impl<'a> From<&'a mix::Node> for SerializableMixNode { - fn from(value: &'a mix::Node) -> Self { +impl<'a> From<&'a mix::LegacyNode> for SerializableMixNode { + fn from(value: &'a mix::LegacyNode) -> Self { SerializableMixNode { mix_id: value.mix_id, - owner: value.owner.clone(), host: value.host.to_string(), mix_port: Some(value.mix_host.port()), identity_key: value.identity_key.to_base58_string(), @@ -181,11 +177,10 @@ impl<'a> From<&'a mix::Node> for SerializableMixNode { #[serde(rename_all = "camelCase")] #[serde(deny_unknown_fields)] pub struct SerializableGateway { - #[cfg_attr(feature = "wasm-serde-types", tsify(optional))] - pub owner: Option, - pub host: String, + pub node_id: NodeId, + // optional ip address in the case of host being a hostname that can't be resolved // (thank you wasm) #[cfg_attr(feature = "wasm-serde-types", tsify(optional))] @@ -215,11 +210,11 @@ pub struct SerializableGateway { pub version: Option, } -impl TryFrom for gateway::Node { +impl TryFrom for gateway::LegacyNode { type Error = SerializableTopologyError; fn try_from(value: SerializableGateway) -> Result { - let host = gateway::Node::parse_host(&value.host)?; + let host = gateway::LegacyNode::parse_host(&value.host)?; let mix_port = value.mix_port.unwrap_or(DEFAULT_MIX_LISTENING_PORT); let clients_ws_port = value @@ -232,11 +227,11 @@ impl TryFrom for gateway::Node { let mix_host = if let Some(explicit_ip) = value.explicit_ip { SocketAddr::new(explicit_ip, mix_port) } else { - gateway::Node::extract_mix_host(&host, mix_port)? + gateway::LegacyNode::extract_mix_host(&host, mix_port)? }; - Ok(gateway::Node { - owner: value.owner, + Ok(gateway::LegacyNode { + node_id: value.node_id, host, mix_host, clients_ws_port, @@ -250,11 +245,11 @@ impl TryFrom for gateway::Node { } } -impl<'a> From<&'a gateway::Node> for SerializableGateway { - fn from(value: &'a gateway::Node) -> Self { +impl<'a> From<&'a gateway::LegacyNode> for SerializableGateway { + fn from(value: &'a gateway::LegacyNode) -> Self { SerializableGateway { - owner: value.owner.clone(), host: value.host.to_string(), + node_id: value.node_id, explicit_ip: Some(value.mix_host.ip()), mix_port: Some(value.mix_host.port()), clients_ws_port: Some(value.clients_ws_port), diff --git a/common/types/src/delegation.rs b/common/types/src/delegation.rs index e98c56fd40..51ffa01e4e 100644 --- a/common/types/src/delegation.rs +++ b/common/types/src/delegation.rs @@ -1,9 +1,9 @@ use crate::currency::{DecCoin, RegisteredCoins}; use crate::deprecated::DelegationEvent; use crate::error::TypesError; -use crate::mixnode::MixNodeCostParams; +use crate::mixnode::NodeCostParams; use cosmwasm_std::Decimal; -use nym_mixnet_contract_common::{Delegation as MixnetContractDelegation, MixId}; +use nym_mixnet_contract_common::{Delegation as MixnetContractDelegation, NodeId}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -15,7 +15,7 @@ use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)] pub struct Delegation { pub owner: String, - pub mix_id: MixId, + pub mix_id: NodeId, pub amount: DecCoin, pub height: u64, pub proxy: Option, // proxy address used to delegate the funds on behalf of another address @@ -28,7 +28,7 @@ impl Delegation { ) -> Result { Ok(Delegation { owner: delegation.owner.to_string(), - mix_id: delegation.mix_id, + mix_id: delegation.node_id, amount: reg.attempt_convert_to_display_dec_coin(delegation.amount.into())?, height: delegation.height, proxy: delegation.proxy.map(|d| d.to_string()), @@ -44,14 +44,14 @@ impl Delegation { #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, JsonSchema)] pub struct DelegationWithEverything { pub owner: String, - pub mix_id: MixId, + pub mix_id: NodeId, pub node_identity: String, pub amount: DecCoin, pub accumulated_by_delegates: Option, pub accumulated_by_operator: Option, pub block_height: u64, pub delegated_on_iso_datetime: Option, - pub cost_params: Option, + pub cost_params: Option, pub avg_uptime_percent: Option, #[cfg_attr(feature = "generate-ts", ts(type = "string | null"))] diff --git a/common/types/src/deprecated.rs b/common/types/src/deprecated.rs index 87f1f04e50..8d74026b3a 100644 --- a/common/types/src/deprecated.rs +++ b/common/types/src/deprecated.rs @@ -4,7 +4,7 @@ use crate::currency::DecCoin; use crate::error::TypesError; use crate::pending_events::{PendingEpochEvent, PendingEpochEventData}; -use nym_mixnet_contract_common::{IdentityKey, MixId}; +use nym_mixnet_contract_common::{IdentityKey, NodeId}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -27,7 +27,7 @@ pub enum DelegationEventKind { #[derive(Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Debug)] pub struct DelegationEvent { pub kind: DelegationEventKind, - pub mix_id: MixId, + pub mix_id: NodeId, pub address: String, pub amount: Option, pub proxy: Option, diff --git a/common/types/src/lib.rs b/common/types/src/lib.rs index 76aa6b2662..dbe40858d6 100644 --- a/common/types/src/lib.rs +++ b/common/types/src/lib.rs @@ -1,6 +1,8 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +#![warn(clippy::todo)] + pub mod account; pub mod currency; pub mod delegation; @@ -12,6 +14,7 @@ pub mod gateway; pub mod helpers; pub mod mixnode; pub mod monitoring; +pub mod nym_node; pub mod pending_events; pub mod transaction; pub mod vesting; diff --git a/common/types/src/mixnode.rs b/common/types/src/mixnode.rs index 02246aad68..c06d0f9ece 100644 --- a/common/types/src/mixnode.rs +++ b/common/types/src/mixnode.rs @@ -5,10 +5,9 @@ use crate::currency::{DecCoin, RegisteredCoins}; use crate::error::TypesError; use cosmwasm_std::Decimal; use nym_mixnet_contract_common::{ - EpochId, MixId, MixNode, MixNodeBond as MixnetContractMixNodeBond, - MixNodeCostParams as MixnetContractMixNodeCostParams, - MixNodeDetails as MixnetContractMixNodeDetails, - MixNodeRewarding as MixnetContractMixNodeRewarding, Percent, + EpochId, MixNode, MixNodeBond as MixnetContractMixNodeBond, + MixNodeDetails as MixnetContractMixNodeDetails, NodeCostParams as MixnetContractNodeCostParams, + NodeId, NodeRewarding as MixnetContractNodeRewarding, Percent, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -23,7 +22,7 @@ use std::net::IpAddr; #[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] pub struct MixNodeDetails { pub bond_information: MixNodeBond, - pub rewarding_details: MixNodeRewarding, + pub rewarding_details: NodeRewarding, } impl MixNodeDetails { @@ -36,7 +35,7 @@ impl MixNodeDetails { details.bond_information, reg, )?, - rewarding_details: MixNodeRewarding::from_mixnet_contract_mixnode_rewarding( + rewarding_details: NodeRewarding::from_mixnet_contract_node_rewarding( details.rewarding_details, reg, )?, @@ -51,10 +50,9 @@ impl MixNodeDetails { )] #[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] pub struct MixNodeBond { - pub mix_id: MixId, + pub mix_id: NodeId, pub owner: String, pub original_pledge: DecCoin, - pub layer: String, pub mix_node: MixNode, pub proxy: Option, pub bonding_height: u64, @@ -71,7 +69,6 @@ impl MixNodeBond { owner: bond.owner.into_string(), original_pledge: reg .attempt_convert_to_display_dec_coin(bond.original_pledge.into())?, - layer: bond.layer.into(), mix_node: bond.mix_node, proxy: bond.proxy.map(|p| p.into_string()), bonding_height: bond.bonding_height, @@ -83,11 +80,11 @@ impl MixNodeBond { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/MixNodeRewarding.ts") + ts(export_to = "ts-packages/types/src/types/rust/NodeRewarding.ts") )] #[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] -pub struct MixNodeRewarding { - pub cost_params: MixNodeCostParams, +pub struct NodeRewarding { + pub cost_params: NodeCostParams, #[cfg_attr(feature = "generate-ts", ts(type = "string"))] pub operator: Decimal, @@ -106,13 +103,13 @@ pub struct MixNodeRewarding { pub unique_delegations: u32, } -impl MixNodeRewarding { - pub fn from_mixnet_contract_mixnode_rewarding( - mix_rewarding: MixnetContractMixNodeRewarding, +impl NodeRewarding { + pub fn from_mixnet_contract_node_rewarding( + mix_rewarding: MixnetContractNodeRewarding, reg: &RegisteredCoins, - ) -> Result { - Ok(MixNodeRewarding { - cost_params: MixNodeCostParams::from_mixnet_contract_mixnode_cost_params( + ) -> Result { + Ok(NodeRewarding { + cost_params: NodeCostParams::from_mixnet_contract_mixnode_cost_params( mix_rewarding.cost_params, reg, )?, @@ -132,19 +129,19 @@ impl MixNodeRewarding { ts(export_to = "ts-packages/types/src/types/rust/MixNodeCostParams.ts") )] #[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] -pub struct MixNodeCostParams { +pub struct NodeCostParams { #[cfg_attr(feature = "generate-ts", ts(type = "string"))] pub profit_margin_percent: Percent, pub interval_operating_cost: DecCoin, } -impl MixNodeCostParams { +impl NodeCostParams { pub fn from_mixnet_contract_mixnode_cost_params( - cost_params: MixnetContractMixNodeCostParams, + cost_params: MixnetContractNodeCostParams, reg: &RegisteredCoins, - ) -> Result { - Ok(MixNodeCostParams { + ) -> Result { + Ok(NodeCostParams { profit_margin_percent: cost_params.profit_margin_percent, interval_operating_cost: reg .attempt_convert_to_display_dec_coin(cost_params.interval_operating_cost.into())?, @@ -154,8 +151,8 @@ impl MixNodeCostParams { pub fn try_convert_to_mixnet_contract_cost_params( self, reg: &RegisteredCoins, - ) -> Result { - Ok(MixnetContractMixNodeCostParams { + ) -> Result { + Ok(MixnetContractNodeCostParams { profit_margin_percent: self.profit_margin_percent, interval_operating_cost: reg .attempt_convert_to_base_coin(self.interval_operating_cost)? diff --git a/common/types/src/monitoring.rs b/common/types/src/monitoring.rs index 7769e61384..4768964fc1 100644 --- a/common/types/src/monitoring.rs +++ b/common/types/src/monitoring.rs @@ -1,9 +1,8 @@ -use std::{collections::HashSet, sync::LazyLock, time::SystemTime}; - use nym_crypto::asymmetric::identity::{PrivateKey, PublicKey, Signature}; -use nym_mixnet_contract_common::MixId; +use nym_mixnet_contract_common::NodeId; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use std::{collections::HashSet, sync::LazyLock, time::SystemTime}; static NETWORK_MONITORS: LazyLock> = LazyLock::new(|| { let mut nm = HashSet::new(); @@ -13,54 +12,26 @@ static NETWORK_MONITORS: LazyLock> = LazyLock::new(|| { #[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)] pub struct NodeResult { - pub node_id: MixId, + pub node_id: NodeId, pub identity: String, pub reliability: u8, } -#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)] -pub struct MixnodeResult { - pub mix_id: MixId, - pub identity: String, - pub owner: String, - pub reliability: u8, -} - -impl MixnodeResult { - pub fn new(mix_id: MixId, identity: String, owner: String, reliability: u8) -> Self { - MixnodeResult { - mix_id, +impl NodeResult { + pub fn new(node_id: NodeId, identity: String, reliability: u8) -> Self { + NodeResult { + node_id, identity, - owner, reliability, } } } -#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone)] -pub struct GatewayResult { - pub identity: String, - pub owner: String, - pub reliability: u8, - pub mix_id: MixId, -} - -impl GatewayResult { - pub fn new(identity: String, owner: String, reliability: u8) -> Self { - GatewayResult { - identity, - owner, - reliability, - mix_id: 0, - } - } -} - #[derive(Serialize, Deserialize, JsonSchema)] #[serde(untagged)] pub enum MonitorResults { - Mixnode(Vec), - Gateway(Vec), + Mixnode(Vec), + Gateway(Vec), } #[derive(Serialize, Deserialize, JsonSchema)] @@ -105,7 +76,7 @@ impl MonitorMessage { } } - pub fn from_allowed(&self) -> bool { + pub fn is_in_allowed(&self) -> bool { NETWORK_MONITORS.contains(&self.signer) } diff --git a/common/types/src/nym_node.rs b/common/types/src/nym_node.rs new file mode 100644 index 0000000000..4b4e46212a --- /dev/null +++ b/common/types/src/nym_node.rs @@ -0,0 +1,94 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::currency::{DecCoin, RegisteredCoins}; +use crate::error::TypesError; +use crate::mixnode::NodeRewarding; +use nym_mixnet_contract_common::{NodeId, NymNode, PendingNodeChanges}; +use nym_mixnet_contract_common::{ + NymNodeBond as MixnetContractNymNodeBond, NymNodeDetails as MixnetContractNymNodeDetails, +}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +/// Full details associated with given node. +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/NymNodeDetails.ts") +)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] +pub struct NymNodeDetails { + /// Basic bond information of this node, such as owner address, original pledge, etc. + pub bond_information: NymNodeBond, + + /// Details used for computation of rewarding related data. + pub rewarding_details: NodeRewarding, + + /// Adjustments to the node that are scheduled to happen during future epoch/interval transitions. + pub pending_changes: PendingNodeChanges, +} + +impl NymNodeDetails { + pub fn from_mixnet_contract_nym_node_details( + details: MixnetContractNymNodeDetails, + reg: &RegisteredCoins, + ) -> Result { + Ok(NymNodeDetails { + bond_information: NymNodeBond::from_mixnet_contract_mixnode_bond( + details.bond_information, + reg, + )?, + rewarding_details: NodeRewarding::from_mixnet_contract_node_rewarding( + details.rewarding_details, + reg, + )?, + pending_changes: details.pending_changes, + }) + } +} + +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/NymNodeBond.ts") +)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] +pub struct NymNodeBond { + /// Unique id assigned to the bonded node. + pub node_id: NodeId, + + /// Address of the owner of this nym-node. + pub owner: String, + + /// Original amount pledged by the operator of this node. + pub original_pledge: DecCoin, + + /// Block height at which this nym-node has been bonded. + pub bonding_height: u64, + + /// Flag to indicate whether this node is in the process of unbonding, + /// that will conclude upon the epoch finishing. + pub is_unbonding: bool, + + #[serde(flatten)] + /// Information provided by the operator for the purposes of bonding. + pub node: NymNode, +} + +impl NymNodeBond { + pub fn from_mixnet_contract_mixnode_bond( + bond: MixnetContractNymNodeBond, + reg: &RegisteredCoins, + ) -> Result { + Ok(NymNodeBond { + node_id: bond.node_id, + owner: bond.owner.into_string(), + original_pledge: reg + .attempt_convert_to_display_dec_coin(bond.original_pledge.into())?, + node: bond.node, + bonding_height: bond.bonding_height, + is_unbonding: bond.is_unbonding, + }) + } +} diff --git a/common/types/src/pending_events.rs b/common/types/src/pending_events.rs index d36e0b978c..a78ba568da 100644 --- a/common/types/src/pending_events.rs +++ b/common/types/src/pending_events.rs @@ -3,9 +3,9 @@ use crate::currency::{DecCoin, RegisteredCoins}; use crate::error::TypesError; -use crate::mixnode::MixNodeCostParams; +use crate::mixnode::NodeCostParams; use nym_mixnet_contract_common::{ - BlockHeight, EpochEventId, IntervalEventId, IntervalRewardingParamsUpdate, MixId, + BlockHeight, EpochEventId, IntervalEventId, IntervalRewardingParamsUpdate, NodeId, PendingEpochEvent as MixnetContractPendingEpochEvent, PendingEpochEventKind as MixnetContractPendingEpochEventKind, PendingIntervalEvent as MixnetContractPendingIntervalEvent, @@ -48,25 +48,25 @@ impl PendingEpochEvent { pub enum PendingEpochEventData { Delegate { owner: String, - mix_id: MixId, + mix_id: NodeId, amount: DecCoin, proxy: Option, }, Undelegate { owner: String, - mix_id: MixId, + mix_id: NodeId, proxy: Option, }, PledgeMore { - mix_id: MixId, + mix_id: NodeId, amount: DecCoin, }, DecreasePledge { - mix_id: MixId, + mix_id: NodeId, decrease_by: DecCoin, }, UnbondMixnode { - mix_id: MixId, + mix_id: NodeId, }, UpdateActiveSetSize { new_size: u32, @@ -81,7 +81,7 @@ impl PendingEpochEventData { match pending_event { MixnetContractPendingEpochEventKind::Delegate { owner, - mix_id, + node_id: mix_id, amount, proxy, .. @@ -93,7 +93,7 @@ impl PendingEpochEventData { }), MixnetContractPendingEpochEventKind::Undelegate { owner, - mix_id, + node_id: mix_id, proxy, .. } => Ok(PendingEpochEventData::Undelegate { @@ -101,13 +101,13 @@ impl PendingEpochEventData { mix_id, proxy: proxy.map(|p| p.into_string()), }), - MixnetContractPendingEpochEventKind::PledgeMore { mix_id, amount } => { + MixnetContractPendingEpochEventKind::MixnodePledgeMore { mix_id, amount } => { Ok(PendingEpochEventData::PledgeMore { mix_id, amount: reg.attempt_convert_to_display_dec_coin(amount.into())?, }) } - MixnetContractPendingEpochEventKind::DecreasePledge { + MixnetContractPendingEpochEventKind::MixnodeDecreasePledge { mix_id, decrease_by, } => Ok(PendingEpochEventData::DecreasePledge { @@ -117,9 +117,7 @@ impl PendingEpochEventData { MixnetContractPendingEpochEventKind::UnbondMixnode { mix_id } => { Ok(PendingEpochEventData::UnbondMixnode { mix_id }) } - MixnetContractPendingEpochEventKind::UpdateActiveSetSize { new_size } => { - Ok(PendingEpochEventData::UpdateActiveSetSize { new_size }) - } + _ => todo!(), } } } @@ -160,8 +158,8 @@ impl PendingIntervalEvent { #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, JsonSchema)] pub enum PendingIntervalEventData { ChangeMixCostParams { - mix_id: MixId, - new_costs: MixNodeCostParams, + mix_id: NodeId, + new_costs: NodeCostParams, }, UpdateRewardingParams { @@ -182,7 +180,7 @@ impl PendingIntervalEventData { MixnetContractPendingIntervalEventKind::ChangeMixCostParams { mix_id, new_costs } => { Ok(PendingIntervalEventData::ChangeMixCostParams { mix_id, - new_costs: MixNodeCostParams::from_mixnet_contract_mixnode_cost_params( + new_costs: NodeCostParams::from_mixnet_contract_mixnode_cost_params( new_costs, reg, )?, }) @@ -197,6 +195,7 @@ impl PendingIntervalEventData { epochs_in_interval, epoch_duration_secs, }), + _ => todo!(), } } } diff --git a/common/wasm/client-core/src/helpers.rs b/common/wasm/client-core/src/helpers.rs index 05caa7d88b..8e001d94e3 100644 --- a/common/wasm/client-core/src/helpers.rs +++ b/common/wasm/client-core/src/helpers.rs @@ -67,10 +67,10 @@ pub async fn current_network_topology_async( }; let api_client = NymApiClient::new(url); - let mixnodes = api_client.get_cached_active_mixnodes().await?; - let gateways = api_client.get_cached_gateways().await?; + let mixnodes = api_client.get_basic_mixnodes().await?; + let gateways = api_client.get_basic_gateways().await?; - Ok(NymTopology::from_detailed(mixnodes, gateways).into()) + Ok(NymTopology::from_basic(&mixnodes, g & ateways).into()) } #[wasm_bindgen(js_name = "currentNetworkTopology")] @@ -88,7 +88,7 @@ pub async fn setup_gateway_wasm( client_store: &ClientStorage, force_tls: bool, chosen_gateway: Option, - gateways: &[gateway::Node], + gateways: &[gateway::LegacyNode], ) -> Result { // TODO: so much optimization and extra features could be added here, but that's for the future diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock index 6185826bbf..e15bd4a459 100644 --- a/contracts/Cargo.lock +++ b/contracts/Cargo.lock @@ -35,9 +35,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.82" +version = "1.0.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" +checksum = "4e1496f8fb1fbf272686b8d37f523dab3e4a7443300055e74cdaa449f3114356" [[package]] name = "arrayref" @@ -1271,6 +1271,7 @@ dependencies = [ name = "nym-mixnet-contract" version = "1.5.1" dependencies = [ + "anyhow", "bs58 0.4.0", "cosmwasm-derive", "cosmwasm-schema", @@ -1297,6 +1298,7 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-controllers", + "cw-storage-plus", "cw2", "humantime-serde", "log", diff --git a/contracts/Cargo.toml b/contracts/Cargo.toml index a7c49c9099..104d1357e4 100644 --- a/contracts/Cargo.toml +++ b/contracts/Cargo.toml @@ -3,7 +3,7 @@ resolver = "2" members = [ "coconut-bandwidth", "coconut-dkg", - "coconut-test", + "coconut-test", "ecash", "mixnet", "mixnet-vesting-integration-tests", @@ -32,6 +32,7 @@ incremental = false overflow-checks = true [workspace.dependencies] +anyhow = "1.0.86" bs58 = "0.4.0" cosmwasm-crypto = "=1.4.3" cosmwasm-derive = "=1.4.3" diff --git a/contracts/mixnet-vesting-integration-tests/src/support/setup.rs b/contracts/mixnet-vesting-integration-tests/src/support/setup.rs index 4ef1b04cba..e2fc5deb38 100644 --- a/contracts/mixnet-vesting-integration-tests/src/support/setup.rs +++ b/contracts/mixnet-vesting-integration-tests/src/support/setup.rs @@ -11,7 +11,7 @@ use nym_contracts_common::signing::{ContractMessageContent, MessageSignature, No use nym_crypto::asymmetric::identity; use nym_mixnet_contract_common::reward_params::Performance; use nym_mixnet_contract_common::{ - CurrentIntervalResponse, LayerAssignment, MixNodeCostParams, MixnodeBondingPayload, + CurrentIntervalResponse, LayerAssignment, MixnodeBondingPayload, NodeCostParams, PagedRewardedSetResponse, RewardingParams, SignableMixNodeBondingMsg, }; use nym_mixnet_contract_common::{ @@ -156,8 +156,8 @@ impl TestSetup { .execute_contract( rewarding_validator(), self.mixnet_contract(), - &MixnetExecuteMsg::RewardMixnode { - mix_id: *mix_id, + &MixnetExecuteMsg::RewardNode { + node_id: *mix_id, performance: Performance::hundred(), }, &[], @@ -206,7 +206,7 @@ impl TestSetup { pub fn valid_mixnode_with_sig( &mut self, owner: &str, - cost_params: MixNodeCostParams, + cost_params: NodeCostParams, stake: Coin, ) -> (MixNode, MessageSignature) { let signing_nonce: Nonce = self diff --git a/contracts/mixnet/Cargo.toml b/contracts/mixnet/Cargo.toml index 957f981f2e..67f63d1007 100644 --- a/contracts/mixnet/Cargo.toml +++ b/contracts/mixnet/Cargo.toml @@ -44,6 +44,7 @@ thiserror = { workspace = true } time = { version = "0.3", features = ["macros"] } [dev-dependencies] +anyhow.workspace = true rand_chacha = "0.3" nym-crypto = { path = "../../common/crypto", features = ["asymmetric", "rand"] } diff --git a/contracts/mixnet/artifacts/checksums.txt b/contracts/mixnet/artifacts/checksums.txt deleted file mode 100644 index 770528b866..0000000000 --- a/contracts/mixnet/artifacts/checksums.txt +++ /dev/null @@ -1 +0,0 @@ -d8a3bddfd2d9f530ca373dfadf60a83eb1a199febec596a3ed37024a22012767 mixnode.wasm diff --git a/contracts/mixnet/artifacts/mixnode.wasm b/contracts/mixnet/artifacts/mixnode.wasm deleted file mode 100644 index 2d7f2adaa3..0000000000 Binary files a/contracts/mixnet/artifacts/mixnode.wasm and /dev/null differ diff --git a/contracts/mixnet/schema/nym-mixnet-contract.json b/contracts/mixnet/schema/nym-mixnet-contract.json index c754e65fdb..95f69ca095 100644 --- a/contracts/mixnet/schema/nym-mixnet-contract.json +++ b/contracts/mixnet/schema/nym-mixnet-contract.json @@ -86,21 +86,15 @@ "InitialRewardingParams": { "type": "object", "required": [ - "active_set_size", "active_set_work_factor", "initial_reward_pool", "initial_staking_supply", "interval_pool_emission", - "rewarded_set_size", + "rewarded_set_params", "staking_supply_scale_factor", "sybil_resistance" ], "properties": { - "active_set_size": { - "type": "integer", - "format": "uint32", - "minimum": 0.0 - }, "active_set_work_factor": { "$ref": "#/definitions/Decimal" }, @@ -113,10 +107,8 @@ "interval_pool_emission": { "$ref": "#/definitions/Percent" }, - "rewarded_set_size": { - "type": "integer", - "format": "uint32", - "minimum": 0.0 + "rewarded_set_params": { + "$ref": "#/definitions/RewardedSetParams" }, "staking_supply_scale_factor": { "$ref": "#/definitions/Percent" @@ -167,6 +159,42 @@ }, "additionalProperties": false }, + "RewardedSetParams": { + "type": "object", + "required": [ + "entry_gateways", + "exit_gateways", + "mixnodes", + "standby" + ], + "properties": { + "entry_gateways": { + "description": "The expected number of nodes assigned entry gateway role (i.e. [`Role::EntryGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "exit_gateways": { + "description": "The expected number of nodes assigned exit gateway role (i.e. [`Role::ExitGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "mixnodes": { + "description": "The expected number of nodes assigned the 'mixnode' role, i.e. total of [`Role::Layer1`], [`Role::Layer2`] and [`Role::Layer3`].", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "standby": { + "description": "Number of nodes in the 'standby' set. (i.e. [`Role::Standby`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, "Uint128": { "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", "type": "string" @@ -199,228 +227,6 @@ }, "additionalProperties": false }, - { - "type": "object", - "required": [ - "assign_node_layer" - ], - "properties": { - "assign_node_layer": { - "type": "object", - "required": [ - "layer", - "mix_id" - ], - "properties": { - "layer": { - "$ref": "#/definitions/Layer" - }, - "mix_id": { - "type": "integer", - "format": "uint32", - "minimum": 0.0 - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Only owner of the node can crate the family with node as head", - "type": "object", - "required": [ - "create_family" - ], - "properties": { - "create_family": { - "type": "object", - "required": [ - "label" - ], - "properties": { - "label": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Family head needs to sign the joining node IdentityKey", - "type": "object", - "required": [ - "join_family" - ], - "properties": { - "join_family": { - "type": "object", - "required": [ - "family_head", - "join_permit" - ], - "properties": { - "family_head": { - "$ref": "#/definitions/FamilyHead" - }, - "join_permit": { - "$ref": "#/definitions/MessageSignature" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "leave_family" - ], - "properties": { - "leave_family": { - "type": "object", - "required": [ - "family_head" - ], - "properties": { - "family_head": { - "$ref": "#/definitions/FamilyHead" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "kick_family_member" - ], - "properties": { - "kick_family_member": { - "type": "object", - "required": [ - "member" - ], - "properties": { - "member": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "create_family_on_behalf" - ], - "properties": { - "create_family_on_behalf": { - "type": "object", - "required": [ - "label", - "owner_address" - ], - "properties": { - "label": { - "type": "string" - }, - "owner_address": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Family head needs to sign the joining node IdentityKey, MixNode needs to provide its signature proving that it wants to join the family", - "type": "object", - "required": [ - "join_family_on_behalf" - ], - "properties": { - "join_family_on_behalf": { - "type": "object", - "required": [ - "family_head", - "join_permit", - "member_address" - ], - "properties": { - "family_head": { - "$ref": "#/definitions/FamilyHead" - }, - "join_permit": { - "$ref": "#/definitions/MessageSignature" - }, - "member_address": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "leave_family_on_behalf" - ], - "properties": { - "leave_family_on_behalf": { - "type": "object", - "required": [ - "family_head", - "member_address" - ], - "properties": { - "family_head": { - "$ref": "#/definitions/FamilyHead" - }, - "member_address": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "kick_family_member_on_behalf" - ], - "properties": { - "kick_family_member_on_behalf": { - "type": "object", - "required": [ - "head_address", - "member" - ], - "properties": { - "head_address": { - "type": "string" - }, - "member": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, { "type": "object", "required": [ @@ -466,23 +272,21 @@ { "type": "object", "required": [ - "update_active_set_size" + "update_active_set_distribution" ], "properties": { - "update_active_set_size": { + "update_active_set_distribution": { "type": "object", "required": [ - "active_set_size", - "force_immediately" + "force_immediately", + "update" ], "properties": { - "active_set_size": { - "type": "integer", - "format": "uint32", - "minimum": 0.0 - }, "force_immediately": { "type": "boolean" + }, + "update": { + "$ref": "#/definitions/ActiveSetUpdate" } }, "additionalProperties": false @@ -561,36 +365,6 @@ }, "additionalProperties": false }, - { - "type": "object", - "required": [ - "advance_current_epoch" - ], - "properties": { - "advance_current_epoch": { - "type": "object", - "required": [ - "expected_active_set_size", - "new_rewarded_set" - ], - "properties": { - "expected_active_set_size": { - "type": "integer", - "format": "uint32", - "minimum": 0.0 - }, - "new_rewarded_set": { - "type": "array", - "items": { - "$ref": "#/definitions/LayerAssignment" - } - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, { "type": "object", "required": [ @@ -614,6 +388,27 @@ }, "additionalProperties": false }, + { + "type": "object", + "required": [ + "assign_roles" + ], + "properties": { + "assign_roles": { + "type": "object", + "required": [ + "assignment" + ], + "properties": { + "assignment": { + "$ref": "#/definitions/RoleAssignment" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "type": "object", "required": [ @@ -629,7 +424,7 @@ ], "properties": { "cost_params": { - "$ref": "#/definitions/MixNodeCostParams" + "$ref": "#/definitions/NodeCostParams" }, "mix_node": { "$ref": "#/definitions/MixNode" @@ -659,7 +454,7 @@ ], "properties": { "cost_params": { - "$ref": "#/definitions/MixNodeCostParams" + "$ref": "#/definitions/NodeCostParams" }, "mix_node": { "$ref": "#/definitions/MixNode" @@ -793,17 +588,17 @@ { "type": "object", "required": [ - "update_mixnode_cost_params" + "update_cost_params" ], "properties": { - "update_mixnode_cost_params": { + "update_cost_params": { "type": "object", "required": [ "new_costs" ], "properties": { "new_costs": { - "$ref": "#/definitions/MixNodeCostParams" + "$ref": "#/definitions/NodeCostParams" } }, "additionalProperties": false @@ -825,7 +620,7 @@ ], "properties": { "new_costs": { - "$ref": "#/definitions/MixNodeCostParams" + "$ref": "#/definitions/NodeCostParams" }, "owner": { "type": "string" @@ -882,6 +677,19 @@ }, "additionalProperties": false }, + { + "type": "object", + "required": [ + "migrate_mixnode" + ], + "properties": { + "migrate_mixnode": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "type": "object", "required": [ @@ -1019,16 +827,104 @@ { "type": "object", "required": [ - "delegate_to_mixnode" + "migrate_gateway" ], "properties": { - "delegate_to_mixnode": { + "migrate_gateway": { + "type": "object", + "properties": { + "cost_params": { + "anyOf": [ + { + "$ref": "#/definitions/NodeCostParams" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "bond_nym_node" + ], + "properties": { + "bond_nym_node": { "type": "object", "required": [ - "mix_id" + "cost_params", + "node", + "owner_signature" ], "properties": { - "mix_id": { + "cost_params": { + "$ref": "#/definitions/NodeCostParams" + }, + "node": { + "$ref": "#/definitions/NymNode" + }, + "owner_signature": { + "$ref": "#/definitions/MessageSignature" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "unbond_nym_node" + ], + "properties": { + "unbond_nym_node": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "update_node_config" + ], + "properties": { + "update_node_config": { + "type": "object", + "required": [ + "update" + ], + "properties": { + "update": { + "$ref": "#/definitions/NodeConfigUpdate" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "delegate" + ], + "properties": { + "delegate": { + "type": "object", + "required": [ + "node_id" + ], + "properties": { + "node_id": { "type": "integer", "format": "uint32", "minimum": 0.0 @@ -1069,16 +965,16 @@ { "type": "object", "required": [ - "undelegate_from_mixnode" + "undelegate" ], "properties": { - "undelegate_from_mixnode": { + "undelegate": { "type": "object", "required": [ - "mix_id" + "node_id" ], "properties": { - "mix_id": { + "node_id": { "type": "integer", "format": "uint32", "minimum": 0.0 @@ -1119,23 +1015,23 @@ { "type": "object", "required": [ - "reward_mixnode" + "reward_node" ], "properties": { - "reward_mixnode": { + "reward_node": { "type": "object", "required": [ - "mix_id", - "performance" + "node_id", + "params" ], "properties": { - "mix_id": { + "node_id": { "type": "integer", "format": "uint32", "minimum": 0.0 }, - "performance": { - "$ref": "#/definitions/Percent" + "params": { + "$ref": "#/definitions/NodeRewardingParameters" } }, "additionalProperties": false @@ -1186,10 +1082,10 @@ "withdraw_delegator_reward": { "type": "object", "required": [ - "mix_id" + "node_id" ], "properties": { - "mix_id": { + "node_id": { "type": "integer", "format": "uint32", "minimum": 0.0 @@ -1262,9 +1158,81 @@ } }, "additionalProperties": false + }, + { + "type": "object", + "required": [ + "testing_unchecked_bond_legacy_mixnode" + ], + "properties": { + "testing_unchecked_bond_legacy_mixnode": { + "type": "object", + "required": [ + "node" + ], + "properties": { + "node": { + "$ref": "#/definitions/MixNode" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "testing_unchecked_bond_legacy_gateway" + ], + "properties": { + "testing_unchecked_bond_legacy_gateway": { + "type": "object", + "required": [ + "node" + ], + "properties": { + "node": { + "$ref": "#/definitions/Gateway" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false } ], "definitions": { + "ActiveSetUpdate": { + "description": "Specification on how the active set should be updated.", + "type": "object", + "required": [ + "entry_gateways", + "exit_gateways", + "mixnodes" + ], + "properties": { + "entry_gateways": { + "description": "The expected number of nodes assigned entry gateway role (i.e. [`Role::EntryGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "exit_gateways": { + "description": "The expected number of nodes assigned exit gateway role (i.e. [`Role::ExitGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "mixnodes": { + "description": "The expected number of nodes assigned the 'mixnode' role, i.e. total of [`Role::Layer1`], [`Role::Layer2`] and [`Role::Layer3`].", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, "Coin": { "type": "object", "required": [ @@ -1284,8 +1252,7 @@ "description": "Contract parameters that could be adjusted in a transaction by the contract admin.", "type": "object", "required": [ - "minimum_gateway_pledge", - "minimum_mixnode_pledge" + "minimum_pledge" ], "properties": { "interval_operating_cost": { @@ -1300,15 +1267,7 @@ } ] }, - "minimum_gateway_pledge": { - "description": "Minimum amount a gateway must pledge to get into the system.", - "allOf": [ - { - "$ref": "#/definitions/Coin" - } - ] - }, - "minimum_mixnode_delegation": { + "minimum_delegation": { "description": "Minimum amount a delegator must stake in orders for his delegation to get accepted.", "anyOf": [ { @@ -1319,8 +1278,8 @@ } ] }, - "minimum_mixnode_pledge": { - "description": "Minimum amount a mixnode must pledge to get into the system.", + "minimum_pledge": { + "description": "Minimum amount a node must pledge to get into the system.", "allOf": [ { "$ref": "#/definitions/Coin" @@ -1346,10 +1305,6 @@ "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", "type": "string" }, - "FamilyHead": { - "description": "Head of particular family as identified by its identity key (i.e. public component of its ed25519 keypair stringified into base58).", - "type": "string" - }, "Gateway": { "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", "type": "object", @@ -1467,14 +1422,16 @@ } ] }, - "rewarded_set_size": { - "description": "Defines the new size of the rewarded set.", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 + "rewarded_set_params": { + "description": "Defines the parameters of the rewarded set.", + "anyOf": [ + { + "$ref": "#/definitions/RewardedSetParams" + }, + { + "type": "null" + } + ] }, "staking_supply": { "description": "Defines the new value of the staking supply.", @@ -1512,39 +1469,6 @@ }, "additionalProperties": false }, - "Layer": { - "type": "string", - "enum": [ - "One", - "Two", - "Three" - ] - }, - "LayerAssignment": { - "description": "Specifies layer assignment for the given mixnode.", - "type": "object", - "required": [ - "layer", - "mix_id" - ], - "properties": { - "layer": { - "description": "The layer to which it's going to be assigned", - "allOf": [ - { - "$ref": "#/definitions/Layer" - } - ] - }, - "mix_id": { - "description": "The id of the mixnode.", - "type": "integer", - "format": "uint32", - "minimum": 0.0 - } - }, - "additionalProperties": false - }, "MessageSignature": { "type": "array", "items": { @@ -1637,7 +1561,31 @@ }, "additionalProperties": false }, - "MixNodeCostParams": { + "NodeConfigUpdate": { + "type": "object", + "properties": { + "custom_http_port": { + "type": [ + "integer", + "null" + ], + "format": "uint16", + "minimum": 0.0 + }, + "host": { + "type": [ + "string", + "null" + ] + }, + "restore_default_http_port": { + "default": false, + "type": "boolean" + } + }, + "additionalProperties": false + }, + "NodeCostParams": { "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", "type": "object", "required": [ @@ -1646,7 +1594,7 @@ ], "properties": { "interval_operating_cost": { - "description": "Operating cost of the associated mixnode per the entire interval.", + "description": "Operating cost of the associated node per the entire interval.", "allOf": [ { "$ref": "#/definitions/Coin" @@ -1654,7 +1602,7 @@ ] }, "profit_margin_percent": { - "description": "The profit margin of the associated mixnode, i.e. the desired percent of the reward to be distributed to the operator.", + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", "allOf": [ { "$ref": "#/definitions/Percent" @@ -1664,6 +1612,61 @@ }, "additionalProperties": false }, + "NodeRewardingParameters": { + "description": "Parameters used for rewarding particular node.", + "type": "object", + "required": [ + "performance", + "work_factor" + ], + "properties": { + "performance": { + "description": "Performance of the particular node in the current epoch.", + "allOf": [ + { + "$ref": "#/definitions/Percent" + } + ] + }, + "work_factor": { + "description": "Amount of work performed by this node in the current epoch also known as 'omega' in the paper", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + }, + "additionalProperties": false + }, + "NymNode": { + "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", + "type": "object", + "required": [ + "host", + "identity_key" + ], + "properties": { + "custom_http_port": { + "description": "Allow specifying custom port for accessing the http, and thus self-described, api of this node for the capabilities discovery.", + "type": [ + "integer", + "null" + ], + "format": "uint16", + "minimum": 0.0 + }, + "host": { + "description": "Network address of this nym-node, for example 1.1.1.1 or foo.mixnode.com that is used to discover other capabilities of this node.", + "type": "string" + }, + "identity_key": { + "description": "Base58-encoded ed25519 EdDSA public key.", + "type": "string" + } + }, + "additionalProperties": false + }, "Percent": { "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", "allOf": [ @@ -1704,6 +1707,74 @@ }, "additionalProperties": false }, + "RewardedSetParams": { + "type": "object", + "required": [ + "entry_gateways", + "exit_gateways", + "mixnodes", + "standby" + ], + "properties": { + "entry_gateways": { + "description": "The expected number of nodes assigned entry gateway role (i.e. [`Role::EntryGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "exit_gateways": { + "description": "The expected number of nodes assigned exit gateway role (i.e. [`Role::ExitGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "mixnodes": { + "description": "The expected number of nodes assigned the 'mixnode' role, i.e. total of [`Role::Layer1`], [`Role::Layer2`] and [`Role::Layer3`].", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "standby": { + "description": "Number of nodes in the 'standby' set. (i.e. [`Role::Standby`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "Role": { + "type": "string", + "enum": [ + "eg", + "l1", + "l2", + "l3", + "xg", + "stb" + ] + }, + "RoleAssignment": { + "type": "object", + "required": [ + "nodes", + "role" + ], + "properties": { + "nodes": { + "type": "array", + "items": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "role": { + "$ref": "#/definitions/Role" + } + }, + "additionalProperties": false + }, "Uint128": { "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", "type": "string" @@ -1727,158 +1798,6 @@ }, "additionalProperties": false }, - { - "description": "Gets the list of families registered in this contract.", - "type": "object", - "required": [ - "get_all_families_paged" - ], - "properties": { - "get_all_families_paged": { - "type": "object", - "properties": { - "limit": { - "description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - }, - "start_after": { - "description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.", - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Gets the list of all family members registered in this contract.", - "type": "object", - "required": [ - "get_all_members_paged" - ], - "properties": { - "get_all_members_paged": { - "type": "object", - "properties": { - "limit": { - "description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - }, - "start_after": { - "description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.", - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Attempts to lookup family information given the family head.", - "type": "object", - "required": [ - "get_family_by_head" - ], - "properties": { - "get_family_by_head": { - "type": "object", - "required": [ - "head" - ], - "properties": { - "head": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Attempts to lookup family information given the family label.", - "type": "object", - "required": [ - "get_family_by_label" - ], - "properties": { - "get_family_by_label": { - "type": "object", - "required": [ - "label" - ], - "properties": { - "label": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Attempts to retrieve family members given the family head.", - "type": "object", - "required": [ - "get_family_members_by_head" - ], - "properties": { - "get_family_members_by_head": { - "type": "object", - "required": [ - "head" - ], - "properties": { - "head": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Attempts to retrieve family members given the family label.", - "type": "object", - "required": [ - "get_family_members_by_label" - ], - "properties": { - "get_family_members_by_label": { - "type": "object", - "required": [ - "label" - ], - "properties": { - "label": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, { "description": "Gets build information of this contract, such as the commit hash used for the build or rustc version.", "type": "object", @@ -1991,40 +1910,6 @@ }, "additionalProperties": false }, - { - "description": "Gets the current list of mixnodes in the rewarded set.", - "type": "object", - "required": [ - "get_rewarded_set" - ], - "properties": { - "get_rewarded_set": { - "type": "object", - "properties": { - "limit": { - "description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - }, - "start_after": { - "description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, { "description": "Gets the basic list of all currently bonded mixnodes.", "type": "object", @@ -2150,7 +2035,7 @@ "minimum": 0.0 }, "owner": { - "description": "The address of the owner of the the mixnodes used for the query.", + "description": "The address of the owner of the mixnodes used for the query.", "type": "string" }, "start_after": { @@ -2355,20 +2240,6 @@ }, "additionalProperties": false }, - { - "description": "Gets the current layer configuration of the mix network.", - "type": "object", - "required": [ - "get_layer_distribution" - ], - "properties": { - "get_layer_distribution": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, { "description": "Gets the basic list of all currently bonded gateways.", "type": "object", @@ -2448,16 +2319,175 @@ "additionalProperties": false }, { - "description": "Gets all delegations associated with particular mixnode", + "description": "Get the `NodeId`s of all the legacy gateways that they will get assigned once migrated into NymNodes", "type": "object", "required": [ - "get_mixnode_delegations" + "get_preassigned_gateway_ids" ], "properties": { - "get_mixnode_delegations": { + "get_preassigned_gateway_ids": { + "type": "object", + "properties": { + "limit": { + "description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Gets the basic list of all currently bonded nymnodes.", + "type": "object", + "required": [ + "get_nym_node_bonds_paged" + ], + "properties": { + "get_nym_node_bonds_paged": { + "type": "object", + "properties": { + "limit": { + "description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Gets the detailed list of all currently bonded nymnodes.", + "type": "object", + "required": [ + "get_nym_nodes_detailed_paged" + ], + "properties": { + "get_nym_nodes_detailed_paged": { + "type": "object", + "properties": { + "limit": { + "description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Gets the basic information of an unbonded nym-node with the provided id.", + "type": "object", + "required": [ + "get_unbonded_nym_node" + ], + "properties": { + "get_unbonded_nym_node": { "type": "object", "required": [ - "mix_id" + "node_id" + ], + "properties": { + "node_id": { + "description": "Id of the node to query.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Gets the basic list of all unbonded nymnodes.", + "type": "object", + "required": [ + "get_unbonded_nym_nodes_paged" + ], + "properties": { + "get_unbonded_nym_nodes_paged": { + "type": "object", + "properties": { + "limit": { + "description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Gets the basic list of all unbonded nymnodes that belonged to a particular owner.", + "type": "object", + "required": [ + "get_unbonded_nym_nodes_by_owner_paged" + ], + "properties": { + "get_unbonded_nym_nodes_by_owner_paged": { + "type": "object", + "required": [ + "owner" ], "properties": { "limit": { @@ -2469,7 +2499,244 @@ "format": "uint32", "minimum": 0.0 }, - "mix_id": { + "owner": { + "description": "The address of the owner of the nym-node used for the query", + "type": "string" + }, + "start_after": { + "description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Gets the basic list of all unbonded nymnodes that used the particular identity key.", + "type": "object", + "required": [ + "get_unbonded_nym_nodes_by_identity_key_paged" + ], + "properties": { + "get_unbonded_nym_nodes_by_identity_key_paged": { + "type": "object", + "required": [ + "identity_key" + ], + "properties": { + "identity_key": { + "description": "The identity key (base58-encoded ed25519 public key) of the node used for the query.", + "type": "string" + }, + "limit": { + "description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Gets the detailed nymnode information belonging to the particular owner.", + "type": "object", + "required": [ + "get_owned_nym_node" + ], + "properties": { + "get_owned_nym_node": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "description": "Address of the node owner to use for the query.", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Gets the detailed nymnode information of a node with the provided id.", + "type": "object", + "required": [ + "get_nym_node_details" + ], + "properties": { + "get_nym_node_details": { + "type": "object", + "required": [ + "node_id" + ], + "properties": { + "node_id": { + "description": "Id of the node to query.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Gets the detailed nym-node information given its current identity key.", + "type": "object", + "required": [ + "get_nym_node_details_by_identity_key" + ], + "properties": { + "get_nym_node_details_by_identity_key": { + "type": "object", + "required": [ + "node_identity" + ], + "properties": { + "node_identity": { + "description": "The identity key (base58-encoded ed25519 public key) of the nym-node used for the query.", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Gets the rewarding information of a nym-node with the provided id.", + "type": "object", + "required": [ + "get_node_rewarding_details" + ], + "properties": { + "get_node_rewarding_details": { + "type": "object", + "required": [ + "node_id" + ], + "properties": { + "node_id": { + "description": "Id of the node to query.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Gets the stake saturation of a nym-node with the provided id.", + "type": "object", + "required": [ + "get_node_stake_saturation" + ], + "properties": { + "get_node_stake_saturation": { + "type": "object", + "required": [ + "node_id" + ], + "properties": { + "node_id": { + "description": "Id of the node to query.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "get_role_assignment" + ], + "properties": { + "get_role_assignment": { + "type": "object", + "required": [ + "role" + ], + "properties": { + "role": { + "$ref": "#/definitions/Role" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "get_rewarded_set_metadata" + ], + "properties": { + "get_rewarded_set_metadata": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Gets all delegations associated with particular node", + "type": "object", + "required": [ + "get_node_delegations" + ], + "properties": { + "get_node_delegations": { + "type": "object", + "required": [ + "node_id" + ], + "properties": { + "limit": { + "description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "node_id": { "description": "Id of the node to query.", "type": "integer", "format": "uint32", @@ -2550,14 +2817,14 @@ "type": "object", "required": [ "delegator", - "mix_id" + "node_id" ], "properties": { "delegator": { "description": "The address of the owner of the delegation.", "type": "string" }, - "mix_id": { + "node_id": { "description": "Id of the node to query.", "type": "integer", "format": "uint32", @@ -2647,16 +2914,16 @@ "description": "Gets the reward amount accrued by the particular mixnode that has not yet been claimed.", "type": "object", "required": [ - "get_pending_mix_node_operator_reward" + "get_pending_node_operator_reward" ], "properties": { - "get_pending_mix_node_operator_reward": { + "get_pending_node_operator_reward": { "type": "object", "required": [ - "mix_id" + "node_id" ], "properties": { - "mix_id": { + "node_id": { "description": "Id of the node to query.", "type": "integer", "format": "uint32", @@ -2679,14 +2946,14 @@ "type": "object", "required": [ "address", - "mix_id" + "node_id" ], "properties": { "address": { "description": "Address of the delegator to use for the query.", "type": "string" }, - "mix_id": { + "node_id": { "description": "Id of the node to query.", "type": "integer", "format": "uint32", @@ -2716,7 +2983,7 @@ "type": "object", "required": [ "estimated_performance", - "mix_id" + "node_id" ], "properties": { "estimated_performance": { @@ -2727,7 +2994,18 @@ } ] }, - "mix_id": { + "estimated_work": { + "description": "The estimated work for the current epoch of the given node.", + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + }, + "node_id": { "description": "Id of the node to query.", "type": "integer", "format": "uint32", @@ -2751,7 +3029,7 @@ "required": [ "address", "estimated_performance", - "mix_id" + "node_id" ], "properties": { "address": { @@ -2766,18 +3044,22 @@ } ] }, - "mix_id": { + "estimated_work": { + "description": "The estimated work for the current epoch of the given node.", + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + }, + "node_id": { "description": "Id of the node to query.", "type": "integer", "format": "uint32", "minimum": 0.0 - }, - "proxy": { - "description": "Entity who made the delegation on behalf of the owner. If present, it's most likely the address of the vesting contract.", - "type": [ - "string", - "null" - ] } }, "additionalProperties": false @@ -2953,6 +3235,17 @@ "$ref": "#/definitions/Decimal" } ] + }, + "Role": { + "type": "string", + "enum": [ + "eg", + "l1", + "l2", + "l3", + "xg", + "stb" + ] } } }, @@ -3055,7 +3348,7 @@ "amount", "cumulative_reward_ratio", "height", - "mix_id", + "node_id", "owner" ], "properties": { @@ -3081,8 +3374,8 @@ "format": "uint64", "minimum": 0.0 }, - "mix_id": { - "description": "Id of the MixNode that this delegation was performed against.", + "node_id": { + "description": "Id of the Node that this delegation was performed against.", "type": "integer", "format": "uint32", "minimum": 0.0 @@ -3115,110 +3408,6 @@ } } }, - "get_all_families_paged": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "PagedFamiliesResponse", - "description": "Response containing paged list of all families registered in the contract.", - "type": "object", - "required": [ - "families" - ], - "properties": { - "families": { - "description": "The families registered in the contract.", - "type": "array", - "items": { - "$ref": "#/definitions/Family" - } - }, - "start_next_after": { - "description": "Field indicating paging information for the following queries if the caller wishes to get further entries.", - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false, - "definitions": { - "Family": { - "description": "A group of mixnodes associated with particular staking entity. When defined all nodes belonging to the same family will be prioritised to be put onto the same layer.", - "type": "object", - "required": [ - "head", - "label" - ], - "properties": { - "head": { - "description": "Owner of this family.", - "allOf": [ - { - "$ref": "#/definitions/FamilyHead" - } - ] - }, - "label": { - "description": "Human readable label for this family.", - "type": "string" - }, - "proxy": { - "description": "Optional proxy (i.e. vesting contract address) used when creating the family.", - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - }, - "FamilyHead": { - "description": "Head of particular family as identified by its identity key (i.e. public component of its ed25519 keypair stringified into base58).", - "type": "string" - } - } - }, - "get_all_members_paged": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "PagedMembersResponse", - "description": "Response containing paged list of all family members (of ALL families) registered in the contract.", - "type": "object", - "required": [ - "members" - ], - "properties": { - "members": { - "description": "The members alongside their family heads.", - "type": "array", - "items": { - "type": "array", - "items": [ - { - "type": "string" - }, - { - "$ref": "#/definitions/FamilyHead" - } - ], - "maxItems": 2, - "minItems": 2 - } - }, - "start_next_after": { - "description": "Field indicating paging information for the following queries if the caller wishes to get further entries.", - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false, - "definitions": { - "FamilyHead": { - "description": "Head of particular family as identified by its identity key (i.e. public component of its ed25519 keypair stringified into base58).", - "type": "string" - } - } - }, "get_bonded_mixnode_details_by_identity": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "MixnodeDetailsByIdentityResponse", @@ -3269,14 +3458,6 @@ "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", "type": "string" }, - "Layer": { - "type": "string", - "enum": [ - "One", - "Two", - "Three" - ] - }, "MixNode": { "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", "type": "object", @@ -3333,7 +3514,6 @@ "required": [ "bonding_height", "is_unbonding", - "layer", "mix_id", "mix_node", "original_pledge", @@ -3350,14 +3530,6 @@ "description": "Flag to indicate whether this node is in the process of unbonding, that will conclude upon the epoch finishing.", "type": "boolean" }, - "layer": { - "description": "Layer assigned to this mixnode.", - "allOf": [ - { - "$ref": "#/definitions/Layer" - } - ] - }, "mix_id": { "description": "Unique id assigned to the bonded mixnode.", "type": "integer", @@ -3399,35 +3571,7 @@ } ] } - }, - "additionalProperties": false - }, - "MixNodeCostParams": { - "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", - "type": "object", - "required": [ - "interval_operating_cost", - "profit_margin_percent" - ], - "properties": { - "interval_operating_cost": { - "description": "Operating cost of the associated mixnode per the entire interval.", - "allOf": [ - { - "$ref": "#/definitions/Coin" - } - ] - }, - "profit_margin_percent": { - "description": "The profit margin of the associated mixnode, i.e. the desired percent of the reward to be distributed to the operator.", - "allOf": [ - { - "$ref": "#/definitions/Percent" - } - ] - } - }, - "additionalProperties": false + } }, "MixNodeDetails": { "description": "Full details associated with given mixnode.", @@ -3448,6 +3592,7 @@ "pending_changes": { "description": "Adjustments to the mixnode that are ought to happen during future epoch transitions.", "default": { + "cost_params_change": null, "pledge_change": null }, "allOf": [ @@ -3460,14 +3605,41 @@ "description": "Details used for computation of rewarding related data.", "allOf": [ { - "$ref": "#/definitions/MixNodeRewarding" + "$ref": "#/definitions/NodeRewarding" } ] } }, "additionalProperties": false }, - "MixNodeRewarding": { + "NodeCostParams": { + "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", + "type": "object", + "required": [ + "interval_operating_cost", + "profit_margin_percent" + ], + "properties": { + "interval_operating_cost": { + "description": "Operating cost of the associated node per the entire interval.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "profit_margin_percent": { + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", + "allOf": [ + { + "$ref": "#/definitions/Percent" + } + ] + } + }, + "additionalProperties": false + }, + "NodeRewarding": { "type": "object", "required": [ "cost_params", @@ -3483,7 +3655,7 @@ "description": "Information provided by the operator that influence the cost function.", "allOf": [ { - "$ref": "#/definitions/MixNodeCostParams" + "$ref": "#/definitions/NodeCostParams" } ] }, @@ -3523,7 +3695,7 @@ "minimum": 0.0 }, "unit_delegation": { - "description": "Value of the theoretical \"unit delegation\" that has delegated to this mixnode at block 0.", + "description": "Value of the theoretical \"unit delegation\" that has delegated to this node at block 0.", "allOf": [ { "$ref": "#/definitions/Decimal" @@ -3536,6 +3708,15 @@ "PendingMixNodeChanges": { "type": "object", "properties": { + "cost_params_change": { + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, "pledge_change": { "type": [ "integer", @@ -3735,11 +3916,12 @@ }, "get_delegation_details": { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "MixNodeDelegationResponse", + "title": "NodeDelegationResponse", "description": "Response containing delegation details.", "type": "object", "required": [ - "mixnode_still_bonded" + "mixnode_still_bonded", + "node_still_bonded" ], "properties": { "delegation": { @@ -3755,6 +3937,10 @@ }, "mixnode_still_bonded": { "description": "Flag indicating whether the node towards which the delegation was made is still bonded in the network.", + "deprecated": true, + "type": "boolean" + }, + "node_still_bonded": { "type": "boolean" } }, @@ -3790,7 +3976,7 @@ "amount", "cumulative_reward_ratio", "height", - "mix_id", + "node_id", "owner" ], "properties": { @@ -3816,8 +4002,8 @@ "format": "uint64", "minimum": 0.0 }, - "mix_id": { - "description": "Id of the MixNode that this delegation was performed against.", + "node_id": { + "description": "Id of the Node that this delegation was performed against.", "type": "integer", "format": "uint32", "minimum": 0.0 @@ -3918,7 +4104,7 @@ "amount", "cumulative_reward_ratio", "height", - "mix_id", + "node_id", "owner" ], "properties": { @@ -3944,8 +4130,8 @@ "format": "uint64", "minimum": 0.0 }, - "mix_id": { - "description": "Id of the MixNode that this delegation was performed against.", + "node_id": { + "description": "Id of the Node that this delegation was performed against.", "type": "integer", "format": "uint32", "minimum": 0.0 @@ -4061,13 +4247,39 @@ ] }, { - "description": "Represents the state of an epoch when all mixnodes have already been rewarded for their work in this epoch, all issued actions got resolved and the epoch should now be advanced whilst assigning new rewarded set.", - "type": "string", - "enum": [ - "advancing_epoch" - ] + "description": "Represents the state of an epoch when all nodes have already been rewarded for their work in this epoch, all issued actions got resolved and node roles should now be assigned before advancing into the next epoch.", + "type": "object", + "required": [ + "role_assignment" + ], + "properties": { + "role_assignment": { + "type": "object", + "required": [ + "next" + ], + "properties": { + "next": { + "$ref": "#/definitions/Role" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false } ] + }, + "Role": { + "type": "string", + "enum": [ + "eg", + "l1", + "l2", + "l3", + "xg", + "stb" + ] } } }, @@ -4249,194 +4461,6 @@ } } }, - "get_family_by_head": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "FamilyByHeadResponse", - "description": "Response containing family information.", - "type": "object", - "required": [ - "head" - ], - "properties": { - "family": { - "description": "If applicable, the family associated with the provided head.", - "anyOf": [ - { - "$ref": "#/definitions/Family" - }, - { - "type": "null" - } - ] - }, - "head": { - "description": "The family head used for the query.", - "allOf": [ - { - "$ref": "#/definitions/FamilyHead" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Family": { - "description": "A group of mixnodes associated with particular staking entity. When defined all nodes belonging to the same family will be prioritised to be put onto the same layer.", - "type": "object", - "required": [ - "head", - "label" - ], - "properties": { - "head": { - "description": "Owner of this family.", - "allOf": [ - { - "$ref": "#/definitions/FamilyHead" - } - ] - }, - "label": { - "description": "Human readable label for this family.", - "type": "string" - }, - "proxy": { - "description": "Optional proxy (i.e. vesting contract address) used when creating the family.", - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - }, - "FamilyHead": { - "description": "Head of particular family as identified by its identity key (i.e. public component of its ed25519 keypair stringified into base58).", - "type": "string" - } - } - }, - "get_family_by_label": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "FamilyByLabelResponse", - "description": "Response containing family information.", - "type": "object", - "required": [ - "label" - ], - "properties": { - "family": { - "description": "If applicable, the family associated with the provided label.", - "anyOf": [ - { - "$ref": "#/definitions/Family" - }, - { - "type": "null" - } - ] - }, - "label": { - "description": "The family label used for the query.", - "type": "string" - } - }, - "additionalProperties": false, - "definitions": { - "Family": { - "description": "A group of mixnodes associated with particular staking entity. When defined all nodes belonging to the same family will be prioritised to be put onto the same layer.", - "type": "object", - "required": [ - "head", - "label" - ], - "properties": { - "head": { - "description": "Owner of this family.", - "allOf": [ - { - "$ref": "#/definitions/FamilyHead" - } - ] - }, - "label": { - "description": "Human readable label for this family.", - "type": "string" - }, - "proxy": { - "description": "Optional proxy (i.e. vesting contract address) used when creating the family.", - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - }, - "FamilyHead": { - "description": "Head of particular family as identified by its identity key (i.e. public component of its ed25519 keypair stringified into base58).", - "type": "string" - } - } - }, - "get_family_members_by_head": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "FamilyMembersByHeadResponse", - "description": "Response containing family members information.", - "type": "object", - "required": [ - "head", - "members" - ], - "properties": { - "head": { - "description": "The family head used for the query.", - "allOf": [ - { - "$ref": "#/definitions/FamilyHead" - } - ] - }, - "members": { - "description": "All members belonging to the specified family.", - "type": "array", - "items": { - "type": "string" - } - } - }, - "additionalProperties": false, - "definitions": { - "FamilyHead": { - "description": "Head of particular family as identified by its identity key (i.e. public component of its ed25519 keypair stringified into base58).", - "type": "string" - } - } - }, - "get_family_members_by_label": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "FamilyMembersByLabelResponse", - "description": "Response containing family members information.", - "type": "object", - "required": [ - "label", - "members" - ], - "properties": { - "label": { - "description": "The family label used for the query.", - "type": "string" - }, - "members": { - "description": "All members belonging to the specified family.", - "type": "array", - "items": { - "type": "string" - } - } - }, - "additionalProperties": false - }, "get_gateway_bond": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "GatewayBondResponse", @@ -4751,38 +4775,6 @@ } } }, - "get_layer_distribution": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "LayerDistribution", - "description": "The current layer distribution of the mix network.", - "type": "object", - "required": [ - "layer1", - "layer2", - "layer3" - ], - "properties": { - "layer1": { - "description": "Number of nodes on the first layer.", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "layer2": { - "description": "Number of nodes on the second layer.", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "layer3": { - "description": "Number of nodes on the third layer.", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - }, "get_mix_node_bonds": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "PagedMixnodeBondsResponse", @@ -4837,14 +4829,6 @@ } } }, - "Layer": { - "type": "string", - "enum": [ - "One", - "Two", - "Three" - ] - }, "MixNode": { "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", "type": "object", @@ -4901,7 +4885,6 @@ "required": [ "bonding_height", "is_unbonding", - "layer", "mix_id", "mix_node", "original_pledge", @@ -4918,14 +4901,6 @@ "description": "Flag to indicate whether this node is in the process of unbonding, that will conclude upon the epoch finishing.", "type": "boolean" }, - "layer": { - "description": "Layer assigned to this mixnode.", - "allOf": [ - { - "$ref": "#/definitions/Layer" - } - ] - }, "mix_id": { "description": "Unique id assigned to the bonded mixnode.", "type": "integer", @@ -4967,8 +4942,7 @@ } ] } - }, - "additionalProperties": false + } }, "Uint128": { "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", @@ -5034,14 +5008,6 @@ "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", "type": "string" }, - "Layer": { - "type": "string", - "enum": [ - "One", - "Two", - "Three" - ] - }, "MixNode": { "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", "type": "object", @@ -5098,7 +5064,6 @@ "required": [ "bonding_height", "is_unbonding", - "layer", "mix_id", "mix_node", "original_pledge", @@ -5115,14 +5080,6 @@ "description": "Flag to indicate whether this node is in the process of unbonding, that will conclude upon the epoch finishing.", "type": "boolean" }, - "layer": { - "description": "Layer assigned to this mixnode.", - "allOf": [ - { - "$ref": "#/definitions/Layer" - } - ] - }, "mix_id": { "description": "Unique id assigned to the bonded mixnode.", "type": "integer", @@ -5164,35 +5121,7 @@ } ] } - }, - "additionalProperties": false - }, - "MixNodeCostParams": { - "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", - "type": "object", - "required": [ - "interval_operating_cost", - "profit_margin_percent" - ], - "properties": { - "interval_operating_cost": { - "description": "Operating cost of the associated mixnode per the entire interval.", - "allOf": [ - { - "$ref": "#/definitions/Coin" - } - ] - }, - "profit_margin_percent": { - "description": "The profit margin of the associated mixnode, i.e. the desired percent of the reward to be distributed to the operator.", - "allOf": [ - { - "$ref": "#/definitions/Percent" - } - ] - } - }, - "additionalProperties": false + } }, "MixNodeDetails": { "description": "Full details associated with given mixnode.", @@ -5213,6 +5142,7 @@ "pending_changes": { "description": "Adjustments to the mixnode that are ought to happen during future epoch transitions.", "default": { + "cost_params_change": null, "pledge_change": null }, "allOf": [ @@ -5225,14 +5155,41 @@ "description": "Details used for computation of rewarding related data.", "allOf": [ { - "$ref": "#/definitions/MixNodeRewarding" + "$ref": "#/definitions/NodeRewarding" } ] } }, "additionalProperties": false }, - "MixNodeRewarding": { + "NodeCostParams": { + "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", + "type": "object", + "required": [ + "interval_operating_cost", + "profit_margin_percent" + ], + "properties": { + "interval_operating_cost": { + "description": "Operating cost of the associated node per the entire interval.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "profit_margin_percent": { + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", + "allOf": [ + { + "$ref": "#/definitions/Percent" + } + ] + } + }, + "additionalProperties": false + }, + "NodeRewarding": { "type": "object", "required": [ "cost_params", @@ -5248,7 +5205,7 @@ "description": "Information provided by the operator that influence the cost function.", "allOf": [ { - "$ref": "#/definitions/MixNodeCostParams" + "$ref": "#/definitions/NodeCostParams" } ] }, @@ -5288,7 +5245,7 @@ "minimum": 0.0 }, "unit_delegation": { - "description": "Value of the theoretical \"unit delegation\" that has delegated to this mixnode at block 0.", + "description": "Value of the theoretical \"unit delegation\" that has delegated to this node at block 0.", "allOf": [ { "$ref": "#/definitions/Decimal" @@ -5301,6 +5258,15 @@ "PendingMixNodeChanges": { "type": "object", "properties": { + "cost_params_change": { + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, "pledge_change": { "type": [ "integer", @@ -5326,122 +5292,6 @@ } } }, - "get_mixnode_delegations": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "PagedMixNodeDelegationsResponse", - "description": "Response containing paged list of all delegations made towards particular mixnode.", - "type": "object", - "required": [ - "delegations" - ], - "properties": { - "delegations": { - "description": "Each individual delegation made.", - "type": "array", - "items": { - "$ref": "#/definitions/Delegation" - } - }, - "start_next_after": { - "description": "Field indicating paging information for the following queries if the caller wishes to get further entries.", - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Coin": { - "type": "object", - "required": [ - "amount", - "denom" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - }, - "denom": { - "type": "string" - } - } - }, - "Decimal": { - "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", - "type": "string" - }, - "Delegation": { - "description": "Information about tokens being delegated towards given mixnode in order to accrue rewards with their work.", - "type": "object", - "required": [ - "amount", - "cumulative_reward_ratio", - "height", - "mix_id", - "owner" - ], - "properties": { - "amount": { - "description": "Original delegation amount. Note that it is never mutated as delegation accumulates rewards.", - "allOf": [ - { - "$ref": "#/definitions/Coin" - } - ] - }, - "cumulative_reward_ratio": { - "description": "Value of the \"unit delegation\" associated with the mixnode at the time of delegation.", - "allOf": [ - { - "$ref": "#/definitions/Decimal" - } - ] - }, - "height": { - "description": "Block height where this delegation occurred.", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "mix_id": { - "description": "Id of the MixNode that this delegation was performed against.", - "type": "integer", - "format": "uint32", - "minimum": 0.0 - }, - "owner": { - "description": "Address of the owner of this delegation.", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, - "proxy": { - "description": "Proxy address used to delegate the funds on behalf of another address", - "anyOf": [ - { - "$ref": "#/definitions/Addr" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } - }, "get_mixnode_details": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "MixnodeDetailsResponse", @@ -5494,14 +5344,6 @@ "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", "type": "string" }, - "Layer": { - "type": "string", - "enum": [ - "One", - "Two", - "Three" - ] - }, "MixNode": { "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", "type": "object", @@ -5558,7 +5400,6 @@ "required": [ "bonding_height", "is_unbonding", - "layer", "mix_id", "mix_node", "original_pledge", @@ -5575,14 +5416,6 @@ "description": "Flag to indicate whether this node is in the process of unbonding, that will conclude upon the epoch finishing.", "type": "boolean" }, - "layer": { - "description": "Layer assigned to this mixnode.", - "allOf": [ - { - "$ref": "#/definitions/Layer" - } - ] - }, "mix_id": { "description": "Unique id assigned to the bonded mixnode.", "type": "integer", @@ -5624,35 +5457,7 @@ } ] } - }, - "additionalProperties": false - }, - "MixNodeCostParams": { - "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", - "type": "object", - "required": [ - "interval_operating_cost", - "profit_margin_percent" - ], - "properties": { - "interval_operating_cost": { - "description": "Operating cost of the associated mixnode per the entire interval.", - "allOf": [ - { - "$ref": "#/definitions/Coin" - } - ] - }, - "profit_margin_percent": { - "description": "The profit margin of the associated mixnode, i.e. the desired percent of the reward to be distributed to the operator.", - "allOf": [ - { - "$ref": "#/definitions/Percent" - } - ] - } - }, - "additionalProperties": false + } }, "MixNodeDetails": { "description": "Full details associated with given mixnode.", @@ -5673,6 +5478,7 @@ "pending_changes": { "description": "Adjustments to the mixnode that are ought to happen during future epoch transitions.", "default": { + "cost_params_change": null, "pledge_change": null }, "allOf": [ @@ -5685,14 +5491,41 @@ "description": "Details used for computation of rewarding related data.", "allOf": [ { - "$ref": "#/definitions/MixNodeRewarding" + "$ref": "#/definitions/NodeRewarding" } ] } }, "additionalProperties": false }, - "MixNodeRewarding": { + "NodeCostParams": { + "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", + "type": "object", + "required": [ + "interval_operating_cost", + "profit_margin_percent" + ], + "properties": { + "interval_operating_cost": { + "description": "Operating cost of the associated node per the entire interval.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "profit_margin_percent": { + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", + "allOf": [ + { + "$ref": "#/definitions/Percent" + } + ] + } + }, + "additionalProperties": false + }, + "NodeRewarding": { "type": "object", "required": [ "cost_params", @@ -5708,7 +5541,7 @@ "description": "Information provided by the operator that influence the cost function.", "allOf": [ { - "$ref": "#/definitions/MixNodeCostParams" + "$ref": "#/definitions/NodeCostParams" } ] }, @@ -5748,7 +5581,7 @@ "minimum": 0.0 }, "unit_delegation": { - "description": "Value of the theoretical \"unit delegation\" that has delegated to this mixnode at block 0.", + "description": "Value of the theoretical \"unit delegation\" that has delegated to this node at block 0.", "allOf": [ { "$ref": "#/definitions/Decimal" @@ -5761,6 +5594,15 @@ "PendingMixNodeChanges": { "type": "object", "properties": { + "cost_params_change": { + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, "pledge_change": { "type": [ "integer", @@ -5805,7 +5647,7 @@ "description": "If there exists a mixnode with the provided id, this field contains its rewarding information.", "anyOf": [ { - "$ref": "#/definitions/MixNodeRewarding" + "$ref": "#/definitions/NodeRewarding" }, { "type": "null" @@ -5834,7 +5676,7 @@ "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", "type": "string" }, - "MixNodeCostParams": { + "NodeCostParams": { "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", "type": "object", "required": [ @@ -5843,7 +5685,7 @@ ], "properties": { "interval_operating_cost": { - "description": "Operating cost of the associated mixnode per the entire interval.", + "description": "Operating cost of the associated node per the entire interval.", "allOf": [ { "$ref": "#/definitions/Coin" @@ -5851,7 +5693,7 @@ ] }, "profit_margin_percent": { - "description": "The profit margin of the associated mixnode, i.e. the desired percent of the reward to be distributed to the operator.", + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", "allOf": [ { "$ref": "#/definitions/Percent" @@ -5861,7 +5703,7 @@ }, "additionalProperties": false }, - "MixNodeRewarding": { + "NodeRewarding": { "type": "object", "required": [ "cost_params", @@ -5877,7 +5719,7 @@ "description": "Information provided by the operator that influence the cost function.", "allOf": [ { - "$ref": "#/definitions/MixNodeCostParams" + "$ref": "#/definitions/NodeCostParams" } ] }, @@ -5917,7 +5759,7 @@ "minimum": 0.0 }, "unit_delegation": { - "description": "Value of the theoretical \"unit delegation\" that has delegated to this mixnode at block 0.", + "description": "Value of the theoretical \"unit delegation\" that has delegated to this node at block 0.", "allOf": [ { "$ref": "#/definitions/Decimal" @@ -5941,6 +5783,323 @@ } } }, + "get_node_delegations": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PagedNodeDelegationsResponse", + "description": "Response containing paged list of all delegations made towards particular node.", + "type": "object", + "required": [ + "delegations" + ], + "properties": { + "delegations": { + "description": "Each individual delegation made.", + "type": "array", + "items": { + "$ref": "#/definitions/Delegation" + } + }, + "start_next_after": { + "description": "Field indicating paging information for the following queries if the caller wishes to get further entries.", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "Delegation": { + "description": "Information about tokens being delegated towards given mixnode in order to accrue rewards with their work.", + "type": "object", + "required": [ + "amount", + "cumulative_reward_ratio", + "height", + "node_id", + "owner" + ], + "properties": { + "amount": { + "description": "Original delegation amount. Note that it is never mutated as delegation accumulates rewards.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "cumulative_reward_ratio": { + "description": "Value of the \"unit delegation\" associated with the mixnode at the time of delegation.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "height": { + "description": "Block height where this delegation occurred.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "node_id": { + "description": "Id of the Node that this delegation was performed against.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "owner": { + "description": "Address of the owner of this delegation.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "proxy": { + "description": "Proxy address used to delegate the funds on behalf of another address", + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, + "get_node_rewarding_details": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NodeRewardingDetailsResponse", + "description": "Response containing rewarding information of a node with the provided id.", + "type": "object", + "required": [ + "node_id" + ], + "properties": { + "node_id": { + "description": "Id of the requested node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "rewarding_details": { + "description": "If there exists a node with the provided id, this field contains its rewarding information.", + "anyOf": [ + { + "$ref": "#/definitions/NodeRewarding" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "NodeCostParams": { + "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", + "type": "object", + "required": [ + "interval_operating_cost", + "profit_margin_percent" + ], + "properties": { + "interval_operating_cost": { + "description": "Operating cost of the associated node per the entire interval.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "profit_margin_percent": { + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", + "allOf": [ + { + "$ref": "#/definitions/Percent" + } + ] + } + }, + "additionalProperties": false + }, + "NodeRewarding": { + "type": "object", + "required": [ + "cost_params", + "delegates", + "last_rewarded_epoch", + "operator", + "total_unit_reward", + "unique_delegations", + "unit_delegation" + ], + "properties": { + "cost_params": { + "description": "Information provided by the operator that influence the cost function.", + "allOf": [ + { + "$ref": "#/definitions/NodeCostParams" + } + ] + }, + "delegates": { + "description": "Total delegation and compounded reward earned by all node delegators.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "last_rewarded_epoch": { + "description": "Marks the epoch when this node was last rewarded so that we wouldn't accidentally attempt to reward it multiple times in the same epoch.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "operator": { + "description": "Total pledge and compounded reward earned by the node operator.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "total_unit_reward": { + "description": "Cumulative reward earned by the \"unit delegation\" since the block 0.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "unique_delegations": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "unit_delegation": { + "description": "Value of the theoretical \"unit delegation\" that has delegated to this node at block 0.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + }, + "additionalProperties": false + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, + "get_node_stake_saturation": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "StakeSaturationResponse", + "description": "Response containing the current state of the stake saturation of a node with the provided id.", + "type": "object", + "required": [ + "node_id" + ], + "properties": { + "current_saturation": { + "description": "The current stake saturation of this node that is indirectly used in reward calculation formulas. Note that it can't be larger than 1.", + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + }, + "node_id": { + "description": "Id of the requested node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "uncapped_saturation": { + "description": "The current, absolute, stake saturation of this node. Note that as the name suggests it can be larger than 1. However, anything beyond that value has no effect on the total node reward.", + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + } + } + }, "get_number_of_pending_events": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "NumberOfPendingEventsResponse", @@ -5966,6 +6125,1033 @@ }, "additionalProperties": false }, + "get_nym_node_bonds_paged": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PagedNymNodeBondsResponse", + "type": "object", + "required": [ + "nodes" + ], + "properties": { + "nodes": { + "description": "The nym node bond information present in the contract.", + "type": "array", + "items": { + "$ref": "#/definitions/NymNodeBond" + } + }, + "start_next_after": { + "description": "Field indicating paging information for the following queries if the caller wishes to get further entries.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "NymNode": { + "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", + "type": "object", + "required": [ + "host", + "identity_key" + ], + "properties": { + "custom_http_port": { + "description": "Allow specifying custom port for accessing the http, and thus self-described, api of this node for the capabilities discovery.", + "type": [ + "integer", + "null" + ], + "format": "uint16", + "minimum": 0.0 + }, + "host": { + "description": "Network address of this nym-node, for example 1.1.1.1 or foo.mixnode.com that is used to discover other capabilities of this node.", + "type": "string" + }, + "identity_key": { + "description": "Base58-encoded ed25519 EdDSA public key.", + "type": "string" + } + }, + "additionalProperties": false + }, + "NymNodeBond": { + "type": "object", + "required": [ + "bonding_height", + "is_unbonding", + "node", + "node_id", + "original_pledge", + "owner" + ], + "properties": { + "bonding_height": { + "description": "Block height at which this nym-node has been bonded.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "is_unbonding": { + "description": "Flag to indicate whether this node is in the process of unbonding, that will conclude upon the epoch finishing.", + "type": "boolean" + }, + "node": { + "description": "Information provided by the operator for the purposes of bonding.", + "allOf": [ + { + "$ref": "#/definitions/NymNode" + } + ] + }, + "node_id": { + "description": "Unique id assigned to the bonded node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "original_pledge": { + "description": "Original amount pledged by the operator of this node.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "owner": { + "description": "Address of the owner of this nym-node.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + } + }, + "additionalProperties": false + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, + "get_nym_node_details": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NodeDetailsResponse", + "description": "Response containing details of a node with the provided id.", + "type": "object", + "required": [ + "node_id" + ], + "properties": { + "details": { + "description": "If there exists a node with the provided id, this field contains its detailed information.", + "anyOf": [ + { + "$ref": "#/definitions/NymNodeDetails" + }, + { + "type": "null" + } + ] + }, + "node_id": { + "description": "Id of the requested node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "NodeCostParams": { + "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", + "type": "object", + "required": [ + "interval_operating_cost", + "profit_margin_percent" + ], + "properties": { + "interval_operating_cost": { + "description": "Operating cost of the associated node per the entire interval.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "profit_margin_percent": { + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", + "allOf": [ + { + "$ref": "#/definitions/Percent" + } + ] + } + }, + "additionalProperties": false + }, + "NodeRewarding": { + "type": "object", + "required": [ + "cost_params", + "delegates", + "last_rewarded_epoch", + "operator", + "total_unit_reward", + "unique_delegations", + "unit_delegation" + ], + "properties": { + "cost_params": { + "description": "Information provided by the operator that influence the cost function.", + "allOf": [ + { + "$ref": "#/definitions/NodeCostParams" + } + ] + }, + "delegates": { + "description": "Total delegation and compounded reward earned by all node delegators.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "last_rewarded_epoch": { + "description": "Marks the epoch when this node was last rewarded so that we wouldn't accidentally attempt to reward it multiple times in the same epoch.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "operator": { + "description": "Total pledge and compounded reward earned by the node operator.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "total_unit_reward": { + "description": "Cumulative reward earned by the \"unit delegation\" since the block 0.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "unique_delegations": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "unit_delegation": { + "description": "Value of the theoretical \"unit delegation\" that has delegated to this node at block 0.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + }, + "additionalProperties": false + }, + "NymNode": { + "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", + "type": "object", + "required": [ + "host", + "identity_key" + ], + "properties": { + "custom_http_port": { + "description": "Allow specifying custom port for accessing the http, and thus self-described, api of this node for the capabilities discovery.", + "type": [ + "integer", + "null" + ], + "format": "uint16", + "minimum": 0.0 + }, + "host": { + "description": "Network address of this nym-node, for example 1.1.1.1 or foo.mixnode.com that is used to discover other capabilities of this node.", + "type": "string" + }, + "identity_key": { + "description": "Base58-encoded ed25519 EdDSA public key.", + "type": "string" + } + }, + "additionalProperties": false + }, + "NymNodeBond": { + "type": "object", + "required": [ + "bonding_height", + "is_unbonding", + "node", + "node_id", + "original_pledge", + "owner" + ], + "properties": { + "bonding_height": { + "description": "Block height at which this nym-node has been bonded.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "is_unbonding": { + "description": "Flag to indicate whether this node is in the process of unbonding, that will conclude upon the epoch finishing.", + "type": "boolean" + }, + "node": { + "description": "Information provided by the operator for the purposes of bonding.", + "allOf": [ + { + "$ref": "#/definitions/NymNode" + } + ] + }, + "node_id": { + "description": "Unique id assigned to the bonded node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "original_pledge": { + "description": "Original amount pledged by the operator of this node.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "owner": { + "description": "Address of the owner of this nym-node.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + } + }, + "additionalProperties": false + }, + "NymNodeDetails": { + "description": "Full details associated with given node.", + "type": "object", + "required": [ + "bond_information", + "pending_changes", + "rewarding_details" + ], + "properties": { + "bond_information": { + "description": "Basic bond information of this node, such as owner address, original pledge, etc.", + "allOf": [ + { + "$ref": "#/definitions/NymNodeBond" + } + ] + }, + "pending_changes": { + "description": "Adjustments to the node that are scheduled to happen during future epoch/interval transitions.", + "allOf": [ + { + "$ref": "#/definitions/PendingNodeChanges" + } + ] + }, + "rewarding_details": { + "description": "Details used for computation of rewarding related data.", + "allOf": [ + { + "$ref": "#/definitions/NodeRewarding" + } + ] + } + }, + "additionalProperties": false + }, + "PendingNodeChanges": { + "type": "object", + "properties": { + "cost_params_change": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "pledge_change": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, + "get_nym_node_details_by_identity_key": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NodeDetailsByIdentityResponse", + "description": "Response containing details of a bonded node with the provided identity key.", + "type": "object", + "required": [ + "identity_key" + ], + "properties": { + "details": { + "description": "If there exists a bonded node with the provided identity key, this field contains its detailed information.", + "anyOf": [ + { + "$ref": "#/definitions/NymNodeDetails" + }, + { + "type": "null" + } + ] + }, + "identity_key": { + "description": "The identity key (base58-encoded ed25519 public key) of the node.", + "type": "string" + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "NodeCostParams": { + "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", + "type": "object", + "required": [ + "interval_operating_cost", + "profit_margin_percent" + ], + "properties": { + "interval_operating_cost": { + "description": "Operating cost of the associated node per the entire interval.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "profit_margin_percent": { + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", + "allOf": [ + { + "$ref": "#/definitions/Percent" + } + ] + } + }, + "additionalProperties": false + }, + "NodeRewarding": { + "type": "object", + "required": [ + "cost_params", + "delegates", + "last_rewarded_epoch", + "operator", + "total_unit_reward", + "unique_delegations", + "unit_delegation" + ], + "properties": { + "cost_params": { + "description": "Information provided by the operator that influence the cost function.", + "allOf": [ + { + "$ref": "#/definitions/NodeCostParams" + } + ] + }, + "delegates": { + "description": "Total delegation and compounded reward earned by all node delegators.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "last_rewarded_epoch": { + "description": "Marks the epoch when this node was last rewarded so that we wouldn't accidentally attempt to reward it multiple times in the same epoch.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "operator": { + "description": "Total pledge and compounded reward earned by the node operator.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "total_unit_reward": { + "description": "Cumulative reward earned by the \"unit delegation\" since the block 0.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "unique_delegations": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "unit_delegation": { + "description": "Value of the theoretical \"unit delegation\" that has delegated to this node at block 0.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + }, + "additionalProperties": false + }, + "NymNode": { + "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", + "type": "object", + "required": [ + "host", + "identity_key" + ], + "properties": { + "custom_http_port": { + "description": "Allow specifying custom port for accessing the http, and thus self-described, api of this node for the capabilities discovery.", + "type": [ + "integer", + "null" + ], + "format": "uint16", + "minimum": 0.0 + }, + "host": { + "description": "Network address of this nym-node, for example 1.1.1.1 or foo.mixnode.com that is used to discover other capabilities of this node.", + "type": "string" + }, + "identity_key": { + "description": "Base58-encoded ed25519 EdDSA public key.", + "type": "string" + } + }, + "additionalProperties": false + }, + "NymNodeBond": { + "type": "object", + "required": [ + "bonding_height", + "is_unbonding", + "node", + "node_id", + "original_pledge", + "owner" + ], + "properties": { + "bonding_height": { + "description": "Block height at which this nym-node has been bonded.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "is_unbonding": { + "description": "Flag to indicate whether this node is in the process of unbonding, that will conclude upon the epoch finishing.", + "type": "boolean" + }, + "node": { + "description": "Information provided by the operator for the purposes of bonding.", + "allOf": [ + { + "$ref": "#/definitions/NymNode" + } + ] + }, + "node_id": { + "description": "Unique id assigned to the bonded node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "original_pledge": { + "description": "Original amount pledged by the operator of this node.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "owner": { + "description": "Address of the owner of this nym-node.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + } + }, + "additionalProperties": false + }, + "NymNodeDetails": { + "description": "Full details associated with given node.", + "type": "object", + "required": [ + "bond_information", + "pending_changes", + "rewarding_details" + ], + "properties": { + "bond_information": { + "description": "Basic bond information of this node, such as owner address, original pledge, etc.", + "allOf": [ + { + "$ref": "#/definitions/NymNodeBond" + } + ] + }, + "pending_changes": { + "description": "Adjustments to the node that are scheduled to happen during future epoch/interval transitions.", + "allOf": [ + { + "$ref": "#/definitions/PendingNodeChanges" + } + ] + }, + "rewarding_details": { + "description": "Details used for computation of rewarding related data.", + "allOf": [ + { + "$ref": "#/definitions/NodeRewarding" + } + ] + } + }, + "additionalProperties": false + }, + "PendingNodeChanges": { + "type": "object", + "properties": { + "cost_params_change": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "pledge_change": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, + "get_nym_nodes_detailed_paged": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PagedNymNodeDetailsResponse", + "type": "object", + "required": [ + "nodes" + ], + "properties": { + "nodes": { + "description": "All nym-node details stored in the contract. Apart from the basic bond information it also contains details required for all future reward calculation as well as any pending changes requested by the operator.", + "type": "array", + "items": { + "$ref": "#/definitions/NymNodeDetails" + } + }, + "start_next_after": { + "description": "Field indicating paging information for the following queries if the caller wishes to get further entries.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "NodeCostParams": { + "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", + "type": "object", + "required": [ + "interval_operating_cost", + "profit_margin_percent" + ], + "properties": { + "interval_operating_cost": { + "description": "Operating cost of the associated node per the entire interval.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "profit_margin_percent": { + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", + "allOf": [ + { + "$ref": "#/definitions/Percent" + } + ] + } + }, + "additionalProperties": false + }, + "NodeRewarding": { + "type": "object", + "required": [ + "cost_params", + "delegates", + "last_rewarded_epoch", + "operator", + "total_unit_reward", + "unique_delegations", + "unit_delegation" + ], + "properties": { + "cost_params": { + "description": "Information provided by the operator that influence the cost function.", + "allOf": [ + { + "$ref": "#/definitions/NodeCostParams" + } + ] + }, + "delegates": { + "description": "Total delegation and compounded reward earned by all node delegators.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "last_rewarded_epoch": { + "description": "Marks the epoch when this node was last rewarded so that we wouldn't accidentally attempt to reward it multiple times in the same epoch.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "operator": { + "description": "Total pledge and compounded reward earned by the node operator.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "total_unit_reward": { + "description": "Cumulative reward earned by the \"unit delegation\" since the block 0.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "unique_delegations": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "unit_delegation": { + "description": "Value of the theoretical \"unit delegation\" that has delegated to this node at block 0.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + }, + "additionalProperties": false + }, + "NymNode": { + "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", + "type": "object", + "required": [ + "host", + "identity_key" + ], + "properties": { + "custom_http_port": { + "description": "Allow specifying custom port for accessing the http, and thus self-described, api of this node for the capabilities discovery.", + "type": [ + "integer", + "null" + ], + "format": "uint16", + "minimum": 0.0 + }, + "host": { + "description": "Network address of this nym-node, for example 1.1.1.1 or foo.mixnode.com that is used to discover other capabilities of this node.", + "type": "string" + }, + "identity_key": { + "description": "Base58-encoded ed25519 EdDSA public key.", + "type": "string" + } + }, + "additionalProperties": false + }, + "NymNodeBond": { + "type": "object", + "required": [ + "bonding_height", + "is_unbonding", + "node", + "node_id", + "original_pledge", + "owner" + ], + "properties": { + "bonding_height": { + "description": "Block height at which this nym-node has been bonded.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "is_unbonding": { + "description": "Flag to indicate whether this node is in the process of unbonding, that will conclude upon the epoch finishing.", + "type": "boolean" + }, + "node": { + "description": "Information provided by the operator for the purposes of bonding.", + "allOf": [ + { + "$ref": "#/definitions/NymNode" + } + ] + }, + "node_id": { + "description": "Unique id assigned to the bonded node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "original_pledge": { + "description": "Original amount pledged by the operator of this node.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "owner": { + "description": "Address of the owner of this nym-node.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + } + }, + "additionalProperties": false + }, + "NymNodeDetails": { + "description": "Full details associated with given node.", + "type": "object", + "required": [ + "bond_information", + "pending_changes", + "rewarding_details" + ], + "properties": { + "bond_information": { + "description": "Basic bond information of this node, such as owner address, original pledge, etc.", + "allOf": [ + { + "$ref": "#/definitions/NymNodeBond" + } + ] + }, + "pending_changes": { + "description": "Adjustments to the node that are scheduled to happen during future epoch/interval transitions.", + "allOf": [ + { + "$ref": "#/definitions/PendingNodeChanges" + } + ] + }, + "rewarding_details": { + "description": "Details used for computation of rewarding related data.", + "allOf": [ + { + "$ref": "#/definitions/NodeRewarding" + } + ] + } + }, + "additionalProperties": false + }, + "PendingNodeChanges": { + "type": "object", + "properties": { + "cost_params_change": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "pledge_change": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, "get_owned_gateway": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "GatewayOwnershipResponse", @@ -6178,14 +7364,6 @@ "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", "type": "string" }, - "Layer": { - "type": "string", - "enum": [ - "One", - "Two", - "Three" - ] - }, "MixNode": { "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", "type": "object", @@ -6242,7 +7420,6 @@ "required": [ "bonding_height", "is_unbonding", - "layer", "mix_id", "mix_node", "original_pledge", @@ -6259,14 +7436,6 @@ "description": "Flag to indicate whether this node is in the process of unbonding, that will conclude upon the epoch finishing.", "type": "boolean" }, - "layer": { - "description": "Layer assigned to this mixnode.", - "allOf": [ - { - "$ref": "#/definitions/Layer" - } - ] - }, "mix_id": { "description": "Unique id assigned to the bonded mixnode.", "type": "integer", @@ -6308,35 +7477,7 @@ } ] } - }, - "additionalProperties": false - }, - "MixNodeCostParams": { - "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", - "type": "object", - "required": [ - "interval_operating_cost", - "profit_margin_percent" - ], - "properties": { - "interval_operating_cost": { - "description": "Operating cost of the associated mixnode per the entire interval.", - "allOf": [ - { - "$ref": "#/definitions/Coin" - } - ] - }, - "profit_margin_percent": { - "description": "The profit margin of the associated mixnode, i.e. the desired percent of the reward to be distributed to the operator.", - "allOf": [ - { - "$ref": "#/definitions/Percent" - } - ] - } - }, - "additionalProperties": false + } }, "MixNodeDetails": { "description": "Full details associated with given mixnode.", @@ -6357,6 +7498,7 @@ "pending_changes": { "description": "Adjustments to the mixnode that are ought to happen during future epoch transitions.", "default": { + "cost_params_change": null, "pledge_change": null }, "allOf": [ @@ -6369,14 +7511,41 @@ "description": "Details used for computation of rewarding related data.", "allOf": [ { - "$ref": "#/definitions/MixNodeRewarding" + "$ref": "#/definitions/NodeRewarding" } ] } }, "additionalProperties": false }, - "MixNodeRewarding": { + "NodeCostParams": { + "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", + "type": "object", + "required": [ + "interval_operating_cost", + "profit_margin_percent" + ], + "properties": { + "interval_operating_cost": { + "description": "Operating cost of the associated node per the entire interval.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "profit_margin_percent": { + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", + "allOf": [ + { + "$ref": "#/definitions/Percent" + } + ] + } + }, + "additionalProperties": false + }, + "NodeRewarding": { "type": "object", "required": [ "cost_params", @@ -6392,7 +7561,7 @@ "description": "Information provided by the operator that influence the cost function.", "allOf": [ { - "$ref": "#/definitions/MixNodeCostParams" + "$ref": "#/definitions/NodeCostParams" } ] }, @@ -6432,7 +7601,7 @@ "minimum": 0.0 }, "unit_delegation": { - "description": "Value of the theoretical \"unit delegation\" that has delegated to this mixnode at block 0.", + "description": "Value of the theoretical \"unit delegation\" that has delegated to this node at block 0.", "allOf": [ { "$ref": "#/definitions/Decimal" @@ -6445,6 +7614,316 @@ "PendingMixNodeChanges": { "type": "object", "properties": { + "cost_params_change": { + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "pledge_change": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, + "get_owned_nym_node": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NodeOwnershipResponse", + "description": "Response containing details of a node belonging to the particular owner.", + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "description": "Validated address of the node owner.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "details": { + "description": "If the provided address owns a nym-node, this field contains its detailed information.", + "anyOf": [ + { + "$ref": "#/definitions/NymNodeDetails" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "NodeCostParams": { + "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", + "type": "object", + "required": [ + "interval_operating_cost", + "profit_margin_percent" + ], + "properties": { + "interval_operating_cost": { + "description": "Operating cost of the associated node per the entire interval.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "profit_margin_percent": { + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", + "allOf": [ + { + "$ref": "#/definitions/Percent" + } + ] + } + }, + "additionalProperties": false + }, + "NodeRewarding": { + "type": "object", + "required": [ + "cost_params", + "delegates", + "last_rewarded_epoch", + "operator", + "total_unit_reward", + "unique_delegations", + "unit_delegation" + ], + "properties": { + "cost_params": { + "description": "Information provided by the operator that influence the cost function.", + "allOf": [ + { + "$ref": "#/definitions/NodeCostParams" + } + ] + }, + "delegates": { + "description": "Total delegation and compounded reward earned by all node delegators.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "last_rewarded_epoch": { + "description": "Marks the epoch when this node was last rewarded so that we wouldn't accidentally attempt to reward it multiple times in the same epoch.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "operator": { + "description": "Total pledge and compounded reward earned by the node operator.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "total_unit_reward": { + "description": "Cumulative reward earned by the \"unit delegation\" since the block 0.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "unique_delegations": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "unit_delegation": { + "description": "Value of the theoretical \"unit delegation\" that has delegated to this node at block 0.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + }, + "additionalProperties": false + }, + "NymNode": { + "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", + "type": "object", + "required": [ + "host", + "identity_key" + ], + "properties": { + "custom_http_port": { + "description": "Allow specifying custom port for accessing the http, and thus self-described, api of this node for the capabilities discovery.", + "type": [ + "integer", + "null" + ], + "format": "uint16", + "minimum": 0.0 + }, + "host": { + "description": "Network address of this nym-node, for example 1.1.1.1 or foo.mixnode.com that is used to discover other capabilities of this node.", + "type": "string" + }, + "identity_key": { + "description": "Base58-encoded ed25519 EdDSA public key.", + "type": "string" + } + }, + "additionalProperties": false + }, + "NymNodeBond": { + "type": "object", + "required": [ + "bonding_height", + "is_unbonding", + "node", + "node_id", + "original_pledge", + "owner" + ], + "properties": { + "bonding_height": { + "description": "Block height at which this nym-node has been bonded.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "is_unbonding": { + "description": "Flag to indicate whether this node is in the process of unbonding, that will conclude upon the epoch finishing.", + "type": "boolean" + }, + "node": { + "description": "Information provided by the operator for the purposes of bonding.", + "allOf": [ + { + "$ref": "#/definitions/NymNode" + } + ] + }, + "node_id": { + "description": "Unique id assigned to the bonded node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "original_pledge": { + "description": "Original amount pledged by the operator of this node.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "owner": { + "description": "Address of the owner of this nym-node.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + } + }, + "additionalProperties": false + }, + "NymNodeDetails": { + "description": "Full details associated with given node.", + "type": "object", + "required": [ + "bond_information", + "pending_changes", + "rewarding_details" + ], + "properties": { + "bond_information": { + "description": "Basic bond information of this node, such as owner address, original pledge, etc.", + "allOf": [ + { + "$ref": "#/definitions/NymNodeBond" + } + ] + }, + "pending_changes": { + "description": "Adjustments to the node that are scheduled to happen during future epoch/interval transitions.", + "allOf": [ + { + "$ref": "#/definitions/PendingNodeChanges" + } + ] + }, + "rewarding_details": { + "description": "Details used for computation of rewarding related data.", + "allOf": [ + { + "$ref": "#/definitions/NodeRewarding" + } + ] + } + }, + "additionalProperties": false + }, + "PendingNodeChanges": { + "type": "object", + "properties": { + "cost_params_change": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, "pledge_change": { "type": [ "integer", @@ -6476,7 +7955,8 @@ "description": "Response containing information about accrued rewards.", "type": "object", "required": [ - "mixnode_still_fully_bonded" + "mixnode_still_fully_bonded", + "node_still_fully_bonded" ], "properties": { "amount_earned": { @@ -6514,6 +7994,10 @@ }, "mixnode_still_fully_bonded": { "description": "The associated mixnode is still fully bonded, meaning it is neither unbonded nor in the process of unbonding that would have finished at the epoch transition.", + "deprecated": true, + "type": "boolean" + }, + "node_still_fully_bonded": { "type": "boolean" } }, @@ -6570,6 +8054,36 @@ }, "additionalProperties": false, "definitions": { + "ActiveSetUpdate": { + "description": "Specification on how the active set should be updated.", + "type": "object", + "required": [ + "entry_gateways", + "exit_gateways", + "mixnodes" + ], + "properties": { + "entry_gateways": { + "description": "The expected number of nodes assigned entry gateway role (i.e. [`Role::EntryGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "exit_gateways": { + "description": "The expected number of nodes assigned exit gateway role (i.e. [`Role::ExitGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "mixnodes": { + "description": "The expected number of nodes assigned the 'mixnode' role, i.e. total of [`Role::Layer1`], [`Role::Layer2`] and [`Role::Layer3`].", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, "Addr": { "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", "type": "string" @@ -6618,7 +8132,7 @@ "description": "Enum encompassing all possible epoch events.", "oneOf": [ { - "description": "Request to create a delegation towards particular mixnode. Note that if such delegation already exists, it will get updated with the provided token amount.", + "description": "Request to create a delegation towards particular node. Note that if such delegation already exists, it will get updated with the provided token amount.", "type": "object", "required": [ "delegate" @@ -6628,7 +8142,7 @@ "type": "object", "required": [ "amount", - "mix_id", + "node_id", "owner" ], "properties": { @@ -6640,8 +8154,8 @@ } ] }, - "mix_id": { - "description": "The id of the mixnode used for the delegation.", + "node_id": { + "description": "The id of the node used for the delegation.", "type": "integer", "format": "uint32", "minimum": 0.0 @@ -6672,7 +8186,7 @@ "additionalProperties": false }, { - "description": "Request to remove delegation from particular mixnode.", + "description": "Request to remove delegation from particular node.", "type": "object", "required": [ "undelegate" @@ -6681,12 +8195,12 @@ "undelegate": { "type": "object", "required": [ - "mix_id", + "node_id", "owner" ], "properties": { - "mix_id": { - "description": "The id of the mixnode used for the delegation.", + "node_id": { + "description": "The id of the node used for the delegation.", "type": "integer", "format": "uint32", "minimum": 0.0 @@ -6720,10 +8234,44 @@ "description": "Request to pledge more tokens (by the node operator) towards its node.", "type": "object", "required": [ - "pledge_more" + "nym_node_pledge_more" ], "properties": { - "pledge_more": { + "nym_node_pledge_more": { + "type": "object", + "required": [ + "amount", + "node_id" + ], + "properties": { + "amount": { + "description": "The amount of additional tokens to use in the pledge.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "node_id": { + "description": "The id of the nym node that will have its pledge updated.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Request to pledge more tokens (by the node operator) towards its node.", + "type": "object", + "required": [ + "mixnode_pledge_more" + ], + "properties": { + "mixnode_pledge_more": { "type": "object", "required": [ "amount", @@ -6731,7 +8279,7 @@ ], "properties": { "amount": { - "description": "The amount of additional tokens to use by the pledge.", + "description": "The amount of additional tokens to use in the pledge.", "allOf": [ { "$ref": "#/definitions/Coin" @@ -6754,10 +8302,44 @@ "description": "Request to decrease amount of pledged tokens (by the node operator) from its node.", "type": "object", "required": [ - "decrease_pledge" + "nym_node_decrease_pledge" ], "properties": { - "decrease_pledge": { + "nym_node_decrease_pledge": { + "type": "object", + "required": [ + "decrease_by", + "node_id" + ], + "properties": { + "decrease_by": { + "description": "The amount of tokens that should be removed from the pledge.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "node_id": { + "description": "The id of the nym node that will have its pledge updated.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Request to decrease amount of pledged tokens (by the node operator) from its node.", + "type": "object", + "required": [ + "mixnode_decrease_pledge" + ], + "properties": { + "mixnode_decrease_pledge": { "type": "object", "required": [ "decrease_by", @@ -6810,20 +8392,20 @@ "additionalProperties": false }, { - "description": "Request to update the current size of the active set.", + "description": "Request to unbond a nym node and completely remove it from the network.", "type": "object", "required": [ - "update_active_set_size" + "unbond_nym_node" ], "properties": { - "update_active_set_size": { + "unbond_nym_node": { "type": "object", "required": [ - "new_size" + "node_id" ], "properties": { - "new_size": { - "description": "The new desired size of the active set.", + "node_id": { + "description": "The id of the node that will get unbonded.", "type": "integer", "format": "uint32", "minimum": 0.0 @@ -6833,6 +8415,28 @@ } }, "additionalProperties": false + }, + { + "description": "Request to update the current active set.", + "type": "object", + "required": [ + "update_active_set" + ], + "properties": { + "update_active_set": { + "type": "object", + "required": [ + "update" + ], + "properties": { + "update": { + "$ref": "#/definitions/ActiveSetUpdate" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false } ] }, @@ -6876,6 +8480,36 @@ }, "additionalProperties": false, "definitions": { + "ActiveSetUpdate": { + "description": "Specification on how the active set should be updated.", + "type": "object", + "required": [ + "entry_gateways", + "exit_gateways", + "mixnodes" + ], + "properties": { + "entry_gateways": { + "description": "The expected number of nodes assigned entry gateway role (i.e. [`Role::EntryGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "exit_gateways": { + "description": "The expected number of nodes assigned exit gateway role (i.e. [`Role::ExitGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "mixnodes": { + "description": "The expected number of nodes assigned the 'mixnode' role, i.e. total of [`Role::Layer1`], [`Role::Layer2`] and [`Role::Layer3`].", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, "Addr": { "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", "type": "string" @@ -6949,7 +8583,7 @@ "description": "Enum encompassing all possible epoch events.", "oneOf": [ { - "description": "Request to create a delegation towards particular mixnode. Note that if such delegation already exists, it will get updated with the provided token amount.", + "description": "Request to create a delegation towards particular node. Note that if such delegation already exists, it will get updated with the provided token amount.", "type": "object", "required": [ "delegate" @@ -6959,7 +8593,7 @@ "type": "object", "required": [ "amount", - "mix_id", + "node_id", "owner" ], "properties": { @@ -6971,8 +8605,8 @@ } ] }, - "mix_id": { - "description": "The id of the mixnode used for the delegation.", + "node_id": { + "description": "The id of the node used for the delegation.", "type": "integer", "format": "uint32", "minimum": 0.0 @@ -7003,7 +8637,7 @@ "additionalProperties": false }, { - "description": "Request to remove delegation from particular mixnode.", + "description": "Request to remove delegation from particular node.", "type": "object", "required": [ "undelegate" @@ -7012,12 +8646,12 @@ "undelegate": { "type": "object", "required": [ - "mix_id", + "node_id", "owner" ], "properties": { - "mix_id": { - "description": "The id of the mixnode used for the delegation.", + "node_id": { + "description": "The id of the node used for the delegation.", "type": "integer", "format": "uint32", "minimum": 0.0 @@ -7051,10 +8685,44 @@ "description": "Request to pledge more tokens (by the node operator) towards its node.", "type": "object", "required": [ - "pledge_more" + "nym_node_pledge_more" ], "properties": { - "pledge_more": { + "nym_node_pledge_more": { + "type": "object", + "required": [ + "amount", + "node_id" + ], + "properties": { + "amount": { + "description": "The amount of additional tokens to use in the pledge.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "node_id": { + "description": "The id of the nym node that will have its pledge updated.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Request to pledge more tokens (by the node operator) towards its node.", + "type": "object", + "required": [ + "mixnode_pledge_more" + ], + "properties": { + "mixnode_pledge_more": { "type": "object", "required": [ "amount", @@ -7062,7 +8730,7 @@ ], "properties": { "amount": { - "description": "The amount of additional tokens to use by the pledge.", + "description": "The amount of additional tokens to use in the pledge.", "allOf": [ { "$ref": "#/definitions/Coin" @@ -7085,10 +8753,44 @@ "description": "Request to decrease amount of pledged tokens (by the node operator) from its node.", "type": "object", "required": [ - "decrease_pledge" + "nym_node_decrease_pledge" ], "properties": { - "decrease_pledge": { + "nym_node_decrease_pledge": { + "type": "object", + "required": [ + "decrease_by", + "node_id" + ], + "properties": { + "decrease_by": { + "description": "The amount of tokens that should be removed from the pledge.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "node_id": { + "description": "The id of the nym node that will have its pledge updated.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Request to decrease amount of pledged tokens (by the node operator) from its node.", + "type": "object", + "required": [ + "mixnode_decrease_pledge" + ], + "properties": { + "mixnode_decrease_pledge": { "type": "object", "required": [ "decrease_by", @@ -7141,20 +8843,20 @@ "additionalProperties": false }, { - "description": "Request to update the current size of the active set.", + "description": "Request to unbond a nym node and completely remove it from the network.", "type": "object", "required": [ - "update_active_set_size" + "unbond_nym_node" ], "properties": { - "update_active_set_size": { + "unbond_nym_node": { "type": "object", "required": [ - "new_size" + "node_id" ], "properties": { - "new_size": { - "description": "The new desired size of the active set.", + "node_id": { + "description": "The id of the node that will get unbonded.", "type": "integer", "format": "uint32", "minimum": 0.0 @@ -7164,6 +8866,28 @@ } }, "additionalProperties": false + }, + { + "description": "Request to update the current active set.", + "type": "object", + "required": [ + "update_active_set" + ], + "properties": { + "update_active_set": { + "type": "object", + "required": [ + "update" + ], + "properties": { + "update": { + "$ref": "#/definitions/ActiveSetUpdate" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false } ] }, @@ -7255,14 +8979,16 @@ } ] }, - "rewarded_set_size": { - "description": "Defines the new size of the rewarded set.", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 + "rewarded_set_params": { + "description": "Defines the parameters of the rewarded set.", + "anyOf": [ + { + "$ref": "#/definitions/RewardedSetParams" + }, + { + "type": "null" + } + ] }, "staking_supply": { "description": "Defines the new value of the staking supply.", @@ -7300,7 +9026,7 @@ }, "additionalProperties": false }, - "MixNodeCostParams": { + "NodeCostParams": { "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", "type": "object", "required": [ @@ -7309,7 +9035,7 @@ ], "properties": { "interval_operating_cost": { - "description": "Operating cost of the associated mixnode per the entire interval.", + "description": "Operating cost of the associated node per the entire interval.", "allOf": [ { "$ref": "#/definitions/Coin" @@ -7317,7 +9043,7 @@ ] }, "profit_margin_percent": { - "description": "The profit margin of the associated mixnode, i.e. the desired percent of the reward to be distributed to the operator.", + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", "allOf": [ { "$ref": "#/definitions/Percent" @@ -7379,7 +9105,7 @@ "description": "The new updated cost function of this mixnode.", "allOf": [ { - "$ref": "#/definitions/MixNodeCostParams" + "$ref": "#/definitions/NodeCostParams" } ] } @@ -7389,6 +9115,40 @@ }, "additionalProperties": false }, + { + "description": "Request to update cost parameters of given nym node.", + "type": "object", + "required": [ + "change_nym_node_cost_params" + ], + "properties": { + "change_nym_node_cost_params": { + "type": "object", + "required": [ + "new_costs", + "node_id" + ], + "properties": { + "new_costs": { + "description": "The new updated cost function of this nym node.", + "allOf": [ + { + "$ref": "#/definitions/NodeCostParams" + } + ] + }, + "node_id": { + "description": "The id of the nym node that will have its cost parameters updated.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "description": "Request to update the underlying rewarding parameters used by the system", "type": "object", @@ -7458,6 +9218,42 @@ } ] }, + "RewardedSetParams": { + "type": "object", + "required": [ + "entry_gateways", + "exit_gateways", + "mixnodes", + "standby" + ], + "properties": { + "entry_gateways": { + "description": "The expected number of nodes assigned entry gateway role (i.e. [`Role::EntryGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "exit_gateways": { + "description": "The expected number of nodes assigned exit gateway role (i.e. [`Role::ExitGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "mixnodes": { + "description": "The expected number of nodes assigned the 'mixnode' role, i.e. total of [`Role::Layer1`], [`Role::Layer2`] and [`Role::Layer3`].", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "standby": { + "description": "Number of nodes in the 'standby' set. (i.e. [`Role::Standby`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, "Uint128": { "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", "type": "string" @@ -7554,14 +9350,16 @@ } ] }, - "rewarded_set_size": { - "description": "Defines the new size of the rewarded set.", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 + "rewarded_set_params": { + "description": "Defines the parameters of the rewarded set.", + "anyOf": [ + { + "$ref": "#/definitions/RewardedSetParams" + }, + { + "type": "null" + } + ] }, "staking_supply": { "description": "Defines the new value of the staking supply.", @@ -7599,7 +9397,7 @@ }, "additionalProperties": false }, - "MixNodeCostParams": { + "NodeCostParams": { "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", "type": "object", "required": [ @@ -7608,7 +9406,7 @@ ], "properties": { "interval_operating_cost": { - "description": "Operating cost of the associated mixnode per the entire interval.", + "description": "Operating cost of the associated node per the entire interval.", "allOf": [ { "$ref": "#/definitions/Coin" @@ -7616,7 +9414,7 @@ ] }, "profit_margin_percent": { - "description": "The profit margin of the associated mixnode, i.e. the desired percent of the reward to be distributed to the operator.", + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", "allOf": [ { "$ref": "#/definitions/Percent" @@ -7703,7 +9501,7 @@ "description": "The new updated cost function of this mixnode.", "allOf": [ { - "$ref": "#/definitions/MixNodeCostParams" + "$ref": "#/definitions/NodeCostParams" } ] } @@ -7713,6 +9511,40 @@ }, "additionalProperties": false }, + { + "description": "Request to update cost parameters of given nym node.", + "type": "object", + "required": [ + "change_nym_node_cost_params" + ], + "properties": { + "change_nym_node_cost_params": { + "type": "object", + "required": [ + "new_costs", + "node_id" + ], + "properties": { + "new_costs": { + "description": "The new updated cost function of this nym node.", + "allOf": [ + { + "$ref": "#/definitions/NodeCostParams" + } + ] + }, + "node_id": { + "description": "The id of the nym node that will have its cost parameters updated.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "description": "Request to update the underlying rewarding parameters used by the system", "type": "object", @@ -7782,19 +9614,56 @@ } ] }, + "RewardedSetParams": { + "type": "object", + "required": [ + "entry_gateways", + "exit_gateways", + "mixnodes", + "standby" + ], + "properties": { + "entry_gateways": { + "description": "The expected number of nodes assigned entry gateway role (i.e. [`Role::EntryGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "exit_gateways": { + "description": "The expected number of nodes assigned exit gateway role (i.e. [`Role::ExitGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "mixnodes": { + "description": "The expected number of nodes assigned the 'mixnode' role, i.e. total of [`Role::Layer1`], [`Role::Layer2`] and [`Role::Layer3`].", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "standby": { + "description": "Number of nodes in the 'standby' set. (i.e. [`Role::Standby`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, "Uint128": { "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", "type": "string" } } }, - "get_pending_mix_node_operator_reward": { + "get_pending_node_operator_reward": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "PendingRewardResponse", "description": "Response containing information about accrued rewards.", "type": "object", "required": [ - "mixnode_still_fully_bonded" + "mixnode_still_fully_bonded", + "node_still_fully_bonded" ], "properties": { "amount_earned": { @@ -7832,6 +9701,10 @@ }, "mixnode_still_fully_bonded": { "description": "The associated mixnode is still fully bonded, meaning it is neither unbonded nor in the process of unbonding that would have finished at the epoch transition.", + "deprecated": true, + "type": "boolean" + }, + "node_still_fully_bonded": { "type": "boolean" } }, @@ -7868,7 +9741,8 @@ "description": "Response containing information about accrued rewards.", "type": "object", "required": [ - "mixnode_still_fully_bonded" + "mixnode_still_fully_bonded", + "node_still_fully_bonded" ], "properties": { "amount_earned": { @@ -7906,6 +9780,10 @@ }, "mixnode_still_fully_bonded": { "description": "The associated mixnode is still fully bonded, meaning it is neither unbonded nor in the process of unbonding that would have finished at the epoch transition.", + "deprecated": true, + "type": "boolean" + }, + "node_still_fully_bonded": { "type": "boolean" } }, @@ -7936,64 +9814,163 @@ } } }, - "get_rewarded_set": { + "get_preassigned_gateway_ids": { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "PagedRewardedSetResponse", - "description": "Response containing paged list of all mixnodes in the rewarded set.", + "title": "PreassignedGatewayIdsResponse", "type": "object", "required": [ - "nodes" + "ids" ], "properties": { - "nodes": { - "description": "Nodes in the current rewarded set.", + "ids": { "type": "array", "items": { - "type": "array", - "items": [ - { - "type": "integer", - "format": "uint32", - "minimum": 0.0 - }, - { - "$ref": "#/definitions/RewardedSetNodeStatus" - } - ], - "maxItems": 2, - "minItems": 2 + "$ref": "#/definitions/PreassignedId" } }, "start_next_after": { "description": "Field indicating paging information for the following queries if the caller wishes to get further entries.", "type": [ - "integer", + "string", "null" - ], - "format": "uint32", - "minimum": 0.0 + ] } }, "additionalProperties": false, "definitions": { - "RewardedSetNodeStatus": { - "description": "Current state of given node in the rewarded set.", - "oneOf": [ - { - "description": "Node that is currently active, i.e. is expected to be used by clients for mixing packets.", - "type": "string", - "enum": [ - "active" + "PreassignedId": { + "type": "object", + "required": [ + "identity", + "node_id" + ], + "properties": { + "identity": { + "description": "The identity key (base58-encoded ed25519 public key) of the gateway.", + "type": "string" + }, + "node_id": { + "description": "The id pre-assigned to this gateway", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } + }, + "get_rewarded_set_metadata": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "RolesMetadataResponse", + "type": "object", + "required": [ + "metadata" + ], + "properties": { + "metadata": { + "$ref": "#/definitions/RewardedSetMetadata" + } + }, + "additionalProperties": false, + "definitions": { + "RewardedSetMetadata": { + "description": "Metadata associated with the rewarded set.", + "type": "object", + "required": [ + "entry_gateway_metadata", + "epoch_id", + "exit_gateway_metadata", + "fully_assigned", + "layer1_metadata", + "layer2_metadata", + "layer3_metadata", + "standby_metadata" + ], + "properties": { + "entry_gateway_metadata": { + "description": "Metadata for the 'EntryGateway' role", + "allOf": [ + { + "$ref": "#/definitions/RoleMetadata" + } ] }, - { - "description": "Node that is currently in standby, i.e. it's present in the rewarded set but is not active.", - "type": "string", - "enum": [ - "standby" + "epoch_id": { + "description": "Epoch that this data corresponds to.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "exit_gateway_metadata": { + "description": "Metadata for the 'ExitGateway' role", + "allOf": [ + { + "$ref": "#/definitions/RoleMetadata" + } + ] + }, + "fully_assigned": { + "description": "Indicates whether all roles got assigned to the set for this epoch.", + "type": "boolean" + }, + "layer1_metadata": { + "description": "Metadata for the 'Layer1' role", + "allOf": [ + { + "$ref": "#/definitions/RoleMetadata" + } + ] + }, + "layer2_metadata": { + "description": "Metadata for the 'Layer2' role", + "allOf": [ + { + "$ref": "#/definitions/RoleMetadata" + } + ] + }, + "layer3_metadata": { + "description": "Metadata for the 'Layer3' role", + "allOf": [ + { + "$ref": "#/definitions/RoleMetadata" + } + ] + }, + "standby_metadata": { + "description": "Metadata for the 'Standby' role", + "allOf": [ + { + "$ref": "#/definitions/RoleMetadata" + } ] } - ] + }, + "additionalProperties": false + }, + "RoleMetadata": { + "description": "Metadata associated with particular node role.", + "type": "object", + "required": [ + "highest_id", + "num_nodes" + ], + "properties": { + "highest_id": { + "description": "Highest, also latest, node-id of a node assigned this role.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "num_nodes": { + "description": "Number of nodes assigned this particular role.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false } } }, @@ -8003,17 +9980,10 @@ "description": "Parameters used for reward calculation.", "type": "object", "required": [ - "active_set_size", "interval", - "rewarded_set_size" + "rewarded_set" ], "properties": { - "active_set_size": { - "description": "The expected number of mixnodes in the active set.", - "type": "integer", - "format": "uint32", - "minimum": 0.0 - }, "interval": { "description": "Parameters that should remain unchanged throughout an interval.", "allOf": [ @@ -8022,11 +9992,8 @@ } ] }, - "rewarded_set_size": { - "description": "The expected number of mixnodes in the rewarded set (i.e. active + standby).", - "type": "integer", - "format": "uint32", - "minimum": 0.0 + "rewarded_set": { + "$ref": "#/definitions/RewardedSetParams" } }, "additionalProperties": false, @@ -8123,6 +10090,42 @@ "$ref": "#/definitions/Decimal" } ] + }, + "RewardedSetParams": { + "type": "object", + "required": [ + "entry_gateways", + "exit_gateways", + "mixnodes", + "standby" + ], + "properties": { + "entry_gateways": { + "description": "The expected number of nodes assigned entry gateway role (i.e. [`Role::EntryGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "exit_gateways": { + "description": "The expected number of nodes assigned exit gateway role (i.e. [`Role::ExitGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "mixnodes": { + "description": "The expected number of nodes assigned the 'mixnode' role, i.e. total of [`Role::Layer1`], [`Role::Layer2`] and [`Role::Layer3`].", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "standby": { + "description": "Number of nodes in the 'standby' set. (i.e. [`Role::Standby`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false } } }, @@ -8131,6 +10134,32 @@ "title": "String", "type": "string" }, + "get_role_assignment": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "EpochAssignmentResponse", + "type": "object", + "required": [ + "epoch_id", + "nodes" + ], + "properties": { + "epoch_id": { + "description": "Epoch that this data corresponds to.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "nodes": { + "type": "array", + "items": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + } + }, + "additionalProperties": false + }, "get_signing_nonce": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "uint32", @@ -8140,7 +10169,7 @@ }, "get_stake_saturation": { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "StakeSaturationResponse", + "title": "MixStakeSaturationResponse", "description": "Response containing the current state of the stake saturation of a mixnode with the provided id.", "type": "object", "required": [ @@ -8263,8 +10292,7 @@ "description": "Contract parameters that could be adjusted in a transaction by the contract admin.", "type": "object", "required": [ - "minimum_gateway_pledge", - "minimum_mixnode_pledge" + "minimum_pledge" ], "properties": { "interval_operating_cost": { @@ -8279,15 +10307,7 @@ } ] }, - "minimum_gateway_pledge": { - "description": "Minimum amount a gateway must pledge to get into the system.", - "allOf": [ - { - "$ref": "#/definitions/Coin" - } - ] - }, - "minimum_mixnode_delegation": { + "minimum_delegation": { "description": "Minimum amount a delegator must stake in orders for his delegation to get accepted.", "anyOf": [ { @@ -8298,8 +10318,8 @@ } ] }, - "minimum_mixnode_pledge": { - "description": "Minimum amount a mixnode must pledge to get into the system.", + "minimum_pledge": { + "description": "Minimum amount a node must pledge to get into the system.", "allOf": [ { "$ref": "#/definitions/Coin" @@ -8377,8 +10397,7 @@ "description": "Contract parameters that could be adjusted in a transaction by the contract admin.", "type": "object", "required": [ - "minimum_gateway_pledge", - "minimum_mixnode_pledge" + "minimum_pledge" ], "properties": { "interval_operating_cost": { @@ -8393,15 +10412,7 @@ } ] }, - "minimum_gateway_pledge": { - "description": "Minimum amount a gateway must pledge to get into the system.", - "allOf": [ - { - "$ref": "#/definitions/Coin" - } - ] - }, - "minimum_mixnode_delegation": { + "minimum_delegation": { "description": "Minimum amount a delegator must stake in orders for his delegation to get accepted.", "anyOf": [ { @@ -8412,8 +10423,8 @@ } ] }, - "minimum_mixnode_pledge": { - "description": "Minimum amount a mixnode must pledge to get into the system.", + "minimum_pledge": { + "description": "Minimum amount a node must pledge to get into the system.", "allOf": [ { "$ref": "#/definitions/Coin" @@ -8857,6 +10868,291 @@ "additionalProperties": false } } + }, + "get_unbonded_nym_node": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "UnbondedNodeResponse", + "description": "Response containing basic information of an unbonded nym-node with the provided id.", + "type": "object", + "required": [ + "node_id" + ], + "properties": { + "details": { + "description": "If there existed a nym-node with the provided id, this field contains its basic information.", + "anyOf": [ + { + "$ref": "#/definitions/UnbondedNymNode" + }, + { + "type": "null" + } + ] + }, + "node_id": { + "description": "Id of the requested nym-node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "UnbondedNymNode": { + "description": "Basic information of a node that used to be part of the nym network but has already unbonded.", + "type": "object", + "required": [ + "identity_key", + "node_id", + "owner", + "unbonding_height" + ], + "properties": { + "identity_key": { + "description": "Base58-encoded ed25519 EdDSA public key.", + "type": "string" + }, + "node_id": { + "description": "NodeId assigned to this node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "owner": { + "description": "Address of the owner of this nym node.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "unbonding_height": { + "description": "Block height at which this nym node has unbonded.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } + }, + "get_unbonded_nym_nodes_by_identity_key_paged": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PagedUnbondedNymNodesResponse", + "description": "Response containing paged list of all nym-nodes that have ever unbonded.", + "type": "object", + "required": [ + "nodes" + ], + "properties": { + "nodes": { + "description": "Basic information of the node such as the owner or the identity key.", + "type": "array", + "items": { + "$ref": "#/definitions/UnbondedNymNode" + } + }, + "start_next_after": { + "description": "Field indicating paging information for the following queries if the caller wishes to get further entries.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "UnbondedNymNode": { + "description": "Basic information of a node that used to be part of the nym network but has already unbonded.", + "type": "object", + "required": [ + "identity_key", + "node_id", + "owner", + "unbonding_height" + ], + "properties": { + "identity_key": { + "description": "Base58-encoded ed25519 EdDSA public key.", + "type": "string" + }, + "node_id": { + "description": "NodeId assigned to this node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "owner": { + "description": "Address of the owner of this nym node.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "unbonding_height": { + "description": "Block height at which this nym node has unbonded.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } + }, + "get_unbonded_nym_nodes_by_owner_paged": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PagedUnbondedNymNodesResponse", + "description": "Response containing paged list of all nym-nodes that have ever unbonded.", + "type": "object", + "required": [ + "nodes" + ], + "properties": { + "nodes": { + "description": "Basic information of the node such as the owner or the identity key.", + "type": "array", + "items": { + "$ref": "#/definitions/UnbondedNymNode" + } + }, + "start_next_after": { + "description": "Field indicating paging information for the following queries if the caller wishes to get further entries.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "UnbondedNymNode": { + "description": "Basic information of a node that used to be part of the nym network but has already unbonded.", + "type": "object", + "required": [ + "identity_key", + "node_id", + "owner", + "unbonding_height" + ], + "properties": { + "identity_key": { + "description": "Base58-encoded ed25519 EdDSA public key.", + "type": "string" + }, + "node_id": { + "description": "NodeId assigned to this node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "owner": { + "description": "Address of the owner of this nym node.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "unbonding_height": { + "description": "Block height at which this nym node has unbonded.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } + }, + "get_unbonded_nym_nodes_paged": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PagedUnbondedNymNodesResponse", + "description": "Response containing paged list of all nym-nodes that have ever unbonded.", + "type": "object", + "required": [ + "nodes" + ], + "properties": { + "nodes": { + "description": "Basic information of the node such as the owner or the identity key.", + "type": "array", + "items": { + "$ref": "#/definitions/UnbondedNymNode" + } + }, + "start_next_after": { + "description": "Field indicating paging information for the following queries if the caller wishes to get further entries.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "UnbondedNymNode": { + "description": "Basic information of a node that used to be part of the nym network but has already unbonded.", + "type": "object", + "required": [ + "identity_key", + "node_id", + "owner", + "unbonding_height" + ], + "properties": { + "identity_key": { + "description": "Base58-encoded ed25519 EdDSA public key.", + "type": "string" + }, + "node_id": { + "description": "NodeId assigned to this node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "owner": { + "description": "Address of the owner of this nym node.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "unbonding_height": { + "description": "Block height at which this nym node has unbonded.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } } } } diff --git a/contracts/mixnet/schema/raw/execute.json b/contracts/mixnet/schema/raw/execute.json index c5c773e76d..a71339ac52 100644 --- a/contracts/mixnet/schema/raw/execute.json +++ b/contracts/mixnet/schema/raw/execute.json @@ -24,228 +24,6 @@ }, "additionalProperties": false }, - { - "type": "object", - "required": [ - "assign_node_layer" - ], - "properties": { - "assign_node_layer": { - "type": "object", - "required": [ - "layer", - "mix_id" - ], - "properties": { - "layer": { - "$ref": "#/definitions/Layer" - }, - "mix_id": { - "type": "integer", - "format": "uint32", - "minimum": 0.0 - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Only owner of the node can crate the family with node as head", - "type": "object", - "required": [ - "create_family" - ], - "properties": { - "create_family": { - "type": "object", - "required": [ - "label" - ], - "properties": { - "label": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Family head needs to sign the joining node IdentityKey", - "type": "object", - "required": [ - "join_family" - ], - "properties": { - "join_family": { - "type": "object", - "required": [ - "family_head", - "join_permit" - ], - "properties": { - "family_head": { - "$ref": "#/definitions/FamilyHead" - }, - "join_permit": { - "$ref": "#/definitions/MessageSignature" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "leave_family" - ], - "properties": { - "leave_family": { - "type": "object", - "required": [ - "family_head" - ], - "properties": { - "family_head": { - "$ref": "#/definitions/FamilyHead" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "kick_family_member" - ], - "properties": { - "kick_family_member": { - "type": "object", - "required": [ - "member" - ], - "properties": { - "member": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "create_family_on_behalf" - ], - "properties": { - "create_family_on_behalf": { - "type": "object", - "required": [ - "label", - "owner_address" - ], - "properties": { - "label": { - "type": "string" - }, - "owner_address": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Family head needs to sign the joining node IdentityKey, MixNode needs to provide its signature proving that it wants to join the family", - "type": "object", - "required": [ - "join_family_on_behalf" - ], - "properties": { - "join_family_on_behalf": { - "type": "object", - "required": [ - "family_head", - "join_permit", - "member_address" - ], - "properties": { - "family_head": { - "$ref": "#/definitions/FamilyHead" - }, - "join_permit": { - "$ref": "#/definitions/MessageSignature" - }, - "member_address": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "leave_family_on_behalf" - ], - "properties": { - "leave_family_on_behalf": { - "type": "object", - "required": [ - "family_head", - "member_address" - ], - "properties": { - "family_head": { - "$ref": "#/definitions/FamilyHead" - }, - "member_address": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "kick_family_member_on_behalf" - ], - "properties": { - "kick_family_member_on_behalf": { - "type": "object", - "required": [ - "head_address", - "member" - ], - "properties": { - "head_address": { - "type": "string" - }, - "member": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, { "type": "object", "required": [ @@ -291,23 +69,21 @@ { "type": "object", "required": [ - "update_active_set_size" + "update_active_set_distribution" ], "properties": { - "update_active_set_size": { + "update_active_set_distribution": { "type": "object", "required": [ - "active_set_size", - "force_immediately" + "force_immediately", + "update" ], "properties": { - "active_set_size": { - "type": "integer", - "format": "uint32", - "minimum": 0.0 - }, "force_immediately": { "type": "boolean" + }, + "update": { + "$ref": "#/definitions/ActiveSetUpdate" } }, "additionalProperties": false @@ -386,36 +162,6 @@ }, "additionalProperties": false }, - { - "type": "object", - "required": [ - "advance_current_epoch" - ], - "properties": { - "advance_current_epoch": { - "type": "object", - "required": [ - "expected_active_set_size", - "new_rewarded_set" - ], - "properties": { - "expected_active_set_size": { - "type": "integer", - "format": "uint32", - "minimum": 0.0 - }, - "new_rewarded_set": { - "type": "array", - "items": { - "$ref": "#/definitions/LayerAssignment" - } - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, { "type": "object", "required": [ @@ -439,6 +185,27 @@ }, "additionalProperties": false }, + { + "type": "object", + "required": [ + "assign_roles" + ], + "properties": { + "assign_roles": { + "type": "object", + "required": [ + "assignment" + ], + "properties": { + "assignment": { + "$ref": "#/definitions/RoleAssignment" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "type": "object", "required": [ @@ -454,7 +221,7 @@ ], "properties": { "cost_params": { - "$ref": "#/definitions/MixNodeCostParams" + "$ref": "#/definitions/NodeCostParams" }, "mix_node": { "$ref": "#/definitions/MixNode" @@ -484,7 +251,7 @@ ], "properties": { "cost_params": { - "$ref": "#/definitions/MixNodeCostParams" + "$ref": "#/definitions/NodeCostParams" }, "mix_node": { "$ref": "#/definitions/MixNode" @@ -618,17 +385,17 @@ { "type": "object", "required": [ - "update_mixnode_cost_params" + "update_cost_params" ], "properties": { - "update_mixnode_cost_params": { + "update_cost_params": { "type": "object", "required": [ "new_costs" ], "properties": { "new_costs": { - "$ref": "#/definitions/MixNodeCostParams" + "$ref": "#/definitions/NodeCostParams" } }, "additionalProperties": false @@ -650,7 +417,7 @@ ], "properties": { "new_costs": { - "$ref": "#/definitions/MixNodeCostParams" + "$ref": "#/definitions/NodeCostParams" }, "owner": { "type": "string" @@ -707,6 +474,19 @@ }, "additionalProperties": false }, + { + "type": "object", + "required": [ + "migrate_mixnode" + ], + "properties": { + "migrate_mixnode": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "type": "object", "required": [ @@ -844,16 +624,104 @@ { "type": "object", "required": [ - "delegate_to_mixnode" + "migrate_gateway" ], "properties": { - "delegate_to_mixnode": { + "migrate_gateway": { + "type": "object", + "properties": { + "cost_params": { + "anyOf": [ + { + "$ref": "#/definitions/NodeCostParams" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "bond_nym_node" + ], + "properties": { + "bond_nym_node": { "type": "object", "required": [ - "mix_id" + "cost_params", + "node", + "owner_signature" ], "properties": { - "mix_id": { + "cost_params": { + "$ref": "#/definitions/NodeCostParams" + }, + "node": { + "$ref": "#/definitions/NymNode" + }, + "owner_signature": { + "$ref": "#/definitions/MessageSignature" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "unbond_nym_node" + ], + "properties": { + "unbond_nym_node": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "update_node_config" + ], + "properties": { + "update_node_config": { + "type": "object", + "required": [ + "update" + ], + "properties": { + "update": { + "$ref": "#/definitions/NodeConfigUpdate" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "delegate" + ], + "properties": { + "delegate": { + "type": "object", + "required": [ + "node_id" + ], + "properties": { + "node_id": { "type": "integer", "format": "uint32", "minimum": 0.0 @@ -894,16 +762,16 @@ { "type": "object", "required": [ - "undelegate_from_mixnode" + "undelegate" ], "properties": { - "undelegate_from_mixnode": { + "undelegate": { "type": "object", "required": [ - "mix_id" + "node_id" ], "properties": { - "mix_id": { + "node_id": { "type": "integer", "format": "uint32", "minimum": 0.0 @@ -944,23 +812,23 @@ { "type": "object", "required": [ - "reward_mixnode" + "reward_node" ], "properties": { - "reward_mixnode": { + "reward_node": { "type": "object", "required": [ - "mix_id", - "performance" + "node_id", + "params" ], "properties": { - "mix_id": { + "node_id": { "type": "integer", "format": "uint32", "minimum": 0.0 }, - "performance": { - "$ref": "#/definitions/Percent" + "params": { + "$ref": "#/definitions/NodeRewardingParameters" } }, "additionalProperties": false @@ -1011,10 +879,10 @@ "withdraw_delegator_reward": { "type": "object", "required": [ - "mix_id" + "node_id" ], "properties": { - "mix_id": { + "node_id": { "type": "integer", "format": "uint32", "minimum": 0.0 @@ -1087,9 +955,81 @@ } }, "additionalProperties": false + }, + { + "type": "object", + "required": [ + "testing_unchecked_bond_legacy_mixnode" + ], + "properties": { + "testing_unchecked_bond_legacy_mixnode": { + "type": "object", + "required": [ + "node" + ], + "properties": { + "node": { + "$ref": "#/definitions/MixNode" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "testing_unchecked_bond_legacy_gateway" + ], + "properties": { + "testing_unchecked_bond_legacy_gateway": { + "type": "object", + "required": [ + "node" + ], + "properties": { + "node": { + "$ref": "#/definitions/Gateway" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false } ], "definitions": { + "ActiveSetUpdate": { + "description": "Specification on how the active set should be updated.", + "type": "object", + "required": [ + "entry_gateways", + "exit_gateways", + "mixnodes" + ], + "properties": { + "entry_gateways": { + "description": "The expected number of nodes assigned entry gateway role (i.e. [`Role::EntryGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "exit_gateways": { + "description": "The expected number of nodes assigned exit gateway role (i.e. [`Role::ExitGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "mixnodes": { + "description": "The expected number of nodes assigned the 'mixnode' role, i.e. total of [`Role::Layer1`], [`Role::Layer2`] and [`Role::Layer3`].", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, "Coin": { "type": "object", "required": [ @@ -1109,8 +1049,7 @@ "description": "Contract parameters that could be adjusted in a transaction by the contract admin.", "type": "object", "required": [ - "minimum_gateway_pledge", - "minimum_mixnode_pledge" + "minimum_pledge" ], "properties": { "interval_operating_cost": { @@ -1125,15 +1064,7 @@ } ] }, - "minimum_gateway_pledge": { - "description": "Minimum amount a gateway must pledge to get into the system.", - "allOf": [ - { - "$ref": "#/definitions/Coin" - } - ] - }, - "minimum_mixnode_delegation": { + "minimum_delegation": { "description": "Minimum amount a delegator must stake in orders for his delegation to get accepted.", "anyOf": [ { @@ -1144,8 +1075,8 @@ } ] }, - "minimum_mixnode_pledge": { - "description": "Minimum amount a mixnode must pledge to get into the system.", + "minimum_pledge": { + "description": "Minimum amount a node must pledge to get into the system.", "allOf": [ { "$ref": "#/definitions/Coin" @@ -1171,10 +1102,6 @@ "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", "type": "string" }, - "FamilyHead": { - "description": "Head of particular family as identified by its identity key (i.e. public component of its ed25519 keypair stringified into base58).", - "type": "string" - }, "Gateway": { "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", "type": "object", @@ -1292,14 +1219,16 @@ } ] }, - "rewarded_set_size": { - "description": "Defines the new size of the rewarded set.", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 + "rewarded_set_params": { + "description": "Defines the parameters of the rewarded set.", + "anyOf": [ + { + "$ref": "#/definitions/RewardedSetParams" + }, + { + "type": "null" + } + ] }, "staking_supply": { "description": "Defines the new value of the staking supply.", @@ -1337,39 +1266,6 @@ }, "additionalProperties": false }, - "Layer": { - "type": "string", - "enum": [ - "One", - "Two", - "Three" - ] - }, - "LayerAssignment": { - "description": "Specifies layer assignment for the given mixnode.", - "type": "object", - "required": [ - "layer", - "mix_id" - ], - "properties": { - "layer": { - "description": "The layer to which it's going to be assigned", - "allOf": [ - { - "$ref": "#/definitions/Layer" - } - ] - }, - "mix_id": { - "description": "The id of the mixnode.", - "type": "integer", - "format": "uint32", - "minimum": 0.0 - } - }, - "additionalProperties": false - }, "MessageSignature": { "type": "array", "items": { @@ -1462,7 +1358,31 @@ }, "additionalProperties": false }, - "MixNodeCostParams": { + "NodeConfigUpdate": { + "type": "object", + "properties": { + "custom_http_port": { + "type": [ + "integer", + "null" + ], + "format": "uint16", + "minimum": 0.0 + }, + "host": { + "type": [ + "string", + "null" + ] + }, + "restore_default_http_port": { + "default": false, + "type": "boolean" + } + }, + "additionalProperties": false + }, + "NodeCostParams": { "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", "type": "object", "required": [ @@ -1471,7 +1391,7 @@ ], "properties": { "interval_operating_cost": { - "description": "Operating cost of the associated mixnode per the entire interval.", + "description": "Operating cost of the associated node per the entire interval.", "allOf": [ { "$ref": "#/definitions/Coin" @@ -1479,7 +1399,7 @@ ] }, "profit_margin_percent": { - "description": "The profit margin of the associated mixnode, i.e. the desired percent of the reward to be distributed to the operator.", + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", "allOf": [ { "$ref": "#/definitions/Percent" @@ -1489,6 +1409,61 @@ }, "additionalProperties": false }, + "NodeRewardingParameters": { + "description": "Parameters used for rewarding particular node.", + "type": "object", + "required": [ + "performance", + "work_factor" + ], + "properties": { + "performance": { + "description": "Performance of the particular node in the current epoch.", + "allOf": [ + { + "$ref": "#/definitions/Percent" + } + ] + }, + "work_factor": { + "description": "Amount of work performed by this node in the current epoch also known as 'omega' in the paper", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + }, + "additionalProperties": false + }, + "NymNode": { + "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", + "type": "object", + "required": [ + "host", + "identity_key" + ], + "properties": { + "custom_http_port": { + "description": "Allow specifying custom port for accessing the http, and thus self-described, api of this node for the capabilities discovery.", + "type": [ + "integer", + "null" + ], + "format": "uint16", + "minimum": 0.0 + }, + "host": { + "description": "Network address of this nym-node, for example 1.1.1.1 or foo.mixnode.com that is used to discover other capabilities of this node.", + "type": "string" + }, + "identity_key": { + "description": "Base58-encoded ed25519 EdDSA public key.", + "type": "string" + } + }, + "additionalProperties": false + }, "Percent": { "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", "allOf": [ @@ -1529,6 +1504,74 @@ }, "additionalProperties": false }, + "RewardedSetParams": { + "type": "object", + "required": [ + "entry_gateways", + "exit_gateways", + "mixnodes", + "standby" + ], + "properties": { + "entry_gateways": { + "description": "The expected number of nodes assigned entry gateway role (i.e. [`Role::EntryGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "exit_gateways": { + "description": "The expected number of nodes assigned exit gateway role (i.e. [`Role::ExitGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "mixnodes": { + "description": "The expected number of nodes assigned the 'mixnode' role, i.e. total of [`Role::Layer1`], [`Role::Layer2`] and [`Role::Layer3`].", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "standby": { + "description": "Number of nodes in the 'standby' set. (i.e. [`Role::Standby`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "Role": { + "type": "string", + "enum": [ + "eg", + "l1", + "l2", + "l3", + "xg", + "stb" + ] + }, + "RoleAssignment": { + "type": "object", + "required": [ + "nodes", + "role" + ], + "properties": { + "nodes": { + "type": "array", + "items": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "role": { + "$ref": "#/definitions/Role" + } + }, + "additionalProperties": false + }, "Uint128": { "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", "type": "string" diff --git a/contracts/mixnet/schema/raw/instantiate.json b/contracts/mixnet/schema/raw/instantiate.json index e025d9f11f..bb2dd70702 100644 --- a/contracts/mixnet/schema/raw/instantiate.json +++ b/contracts/mixnet/schema/raw/instantiate.json @@ -82,21 +82,15 @@ "InitialRewardingParams": { "type": "object", "required": [ - "active_set_size", "active_set_work_factor", "initial_reward_pool", "initial_staking_supply", "interval_pool_emission", - "rewarded_set_size", + "rewarded_set_params", "staking_supply_scale_factor", "sybil_resistance" ], "properties": { - "active_set_size": { - "type": "integer", - "format": "uint32", - "minimum": 0.0 - }, "active_set_work_factor": { "$ref": "#/definitions/Decimal" }, @@ -109,10 +103,8 @@ "interval_pool_emission": { "$ref": "#/definitions/Percent" }, - "rewarded_set_size": { - "type": "integer", - "format": "uint32", - "minimum": 0.0 + "rewarded_set_params": { + "$ref": "#/definitions/RewardedSetParams" }, "staking_supply_scale_factor": { "$ref": "#/definitions/Percent" @@ -163,6 +155,42 @@ }, "additionalProperties": false }, + "RewardedSetParams": { + "type": "object", + "required": [ + "entry_gateways", + "exit_gateways", + "mixnodes", + "standby" + ], + "properties": { + "entry_gateways": { + "description": "The expected number of nodes assigned entry gateway role (i.e. [`Role::EntryGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "exit_gateways": { + "description": "The expected number of nodes assigned exit gateway role (i.e. [`Role::ExitGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "mixnodes": { + "description": "The expected number of nodes assigned the 'mixnode' role, i.e. total of [`Role::Layer1`], [`Role::Layer2`] and [`Role::Layer3`].", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "standby": { + "description": "Number of nodes in the 'standby' set. (i.e. [`Role::Standby`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, "Uint128": { "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", "type": "string" diff --git a/contracts/mixnet/schema/raw/query.json b/contracts/mixnet/schema/raw/query.json index c938a56f64..90d0477a30 100644 --- a/contracts/mixnet/schema/raw/query.json +++ b/contracts/mixnet/schema/raw/query.json @@ -15,158 +15,6 @@ }, "additionalProperties": false }, - { - "description": "Gets the list of families registered in this contract.", - "type": "object", - "required": [ - "get_all_families_paged" - ], - "properties": { - "get_all_families_paged": { - "type": "object", - "properties": { - "limit": { - "description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - }, - "start_after": { - "description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.", - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Gets the list of all family members registered in this contract.", - "type": "object", - "required": [ - "get_all_members_paged" - ], - "properties": { - "get_all_members_paged": { - "type": "object", - "properties": { - "limit": { - "description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - }, - "start_after": { - "description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.", - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Attempts to lookup family information given the family head.", - "type": "object", - "required": [ - "get_family_by_head" - ], - "properties": { - "get_family_by_head": { - "type": "object", - "required": [ - "head" - ], - "properties": { - "head": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Attempts to lookup family information given the family label.", - "type": "object", - "required": [ - "get_family_by_label" - ], - "properties": { - "get_family_by_label": { - "type": "object", - "required": [ - "label" - ], - "properties": { - "label": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Attempts to retrieve family members given the family head.", - "type": "object", - "required": [ - "get_family_members_by_head" - ], - "properties": { - "get_family_members_by_head": { - "type": "object", - "required": [ - "head" - ], - "properties": { - "head": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Attempts to retrieve family members given the family label.", - "type": "object", - "required": [ - "get_family_members_by_label" - ], - "properties": { - "get_family_members_by_label": { - "type": "object", - "required": [ - "label" - ], - "properties": { - "label": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, { "description": "Gets build information of this contract, such as the commit hash used for the build or rustc version.", "type": "object", @@ -279,40 +127,6 @@ }, "additionalProperties": false }, - { - "description": "Gets the current list of mixnodes in the rewarded set.", - "type": "object", - "required": [ - "get_rewarded_set" - ], - "properties": { - "get_rewarded_set": { - "type": "object", - "properties": { - "limit": { - "description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - }, - "start_after": { - "description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, { "description": "Gets the basic list of all currently bonded mixnodes.", "type": "object", @@ -438,7 +252,7 @@ "minimum": 0.0 }, "owner": { - "description": "The address of the owner of the the mixnodes used for the query.", + "description": "The address of the owner of the mixnodes used for the query.", "type": "string" }, "start_after": { @@ -643,20 +457,6 @@ }, "additionalProperties": false }, - { - "description": "Gets the current layer configuration of the mix network.", - "type": "object", - "required": [ - "get_layer_distribution" - ], - "properties": { - "get_layer_distribution": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, { "description": "Gets the basic list of all currently bonded gateways.", "type": "object", @@ -736,16 +536,175 @@ "additionalProperties": false }, { - "description": "Gets all delegations associated with particular mixnode", + "description": "Get the `NodeId`s of all the legacy gateways that they will get assigned once migrated into NymNodes", "type": "object", "required": [ - "get_mixnode_delegations" + "get_preassigned_gateway_ids" ], "properties": { - "get_mixnode_delegations": { + "get_preassigned_gateway_ids": { + "type": "object", + "properties": { + "limit": { + "description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Gets the basic list of all currently bonded nymnodes.", + "type": "object", + "required": [ + "get_nym_node_bonds_paged" + ], + "properties": { + "get_nym_node_bonds_paged": { + "type": "object", + "properties": { + "limit": { + "description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Gets the detailed list of all currently bonded nymnodes.", + "type": "object", + "required": [ + "get_nym_nodes_detailed_paged" + ], + "properties": { + "get_nym_nodes_detailed_paged": { + "type": "object", + "properties": { + "limit": { + "description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Gets the basic information of an unbonded nym-node with the provided id.", + "type": "object", + "required": [ + "get_unbonded_nym_node" + ], + "properties": { + "get_unbonded_nym_node": { "type": "object", "required": [ - "mix_id" + "node_id" + ], + "properties": { + "node_id": { + "description": "Id of the node to query.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Gets the basic list of all unbonded nymnodes.", + "type": "object", + "required": [ + "get_unbonded_nym_nodes_paged" + ], + "properties": { + "get_unbonded_nym_nodes_paged": { + "type": "object", + "properties": { + "limit": { + "description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Gets the basic list of all unbonded nymnodes that belonged to a particular owner.", + "type": "object", + "required": [ + "get_unbonded_nym_nodes_by_owner_paged" + ], + "properties": { + "get_unbonded_nym_nodes_by_owner_paged": { + "type": "object", + "required": [ + "owner" ], "properties": { "limit": { @@ -757,7 +716,244 @@ "format": "uint32", "minimum": 0.0 }, - "mix_id": { + "owner": { + "description": "The address of the owner of the nym-node used for the query", + "type": "string" + }, + "start_after": { + "description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Gets the basic list of all unbonded nymnodes that used the particular identity key.", + "type": "object", + "required": [ + "get_unbonded_nym_nodes_by_identity_key_paged" + ], + "properties": { + "get_unbonded_nym_nodes_by_identity_key_paged": { + "type": "object", + "required": [ + "identity_key" + ], + "properties": { + "identity_key": { + "description": "The identity key (base58-encoded ed25519 public key) of the node used for the query.", + "type": "string" + }, + "limit": { + "description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Gets the detailed nymnode information belonging to the particular owner.", + "type": "object", + "required": [ + "get_owned_nym_node" + ], + "properties": { + "get_owned_nym_node": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "description": "Address of the node owner to use for the query.", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Gets the detailed nymnode information of a node with the provided id.", + "type": "object", + "required": [ + "get_nym_node_details" + ], + "properties": { + "get_nym_node_details": { + "type": "object", + "required": [ + "node_id" + ], + "properties": { + "node_id": { + "description": "Id of the node to query.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Gets the detailed nym-node information given its current identity key.", + "type": "object", + "required": [ + "get_nym_node_details_by_identity_key" + ], + "properties": { + "get_nym_node_details_by_identity_key": { + "type": "object", + "required": [ + "node_identity" + ], + "properties": { + "node_identity": { + "description": "The identity key (base58-encoded ed25519 public key) of the nym-node used for the query.", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Gets the rewarding information of a nym-node with the provided id.", + "type": "object", + "required": [ + "get_node_rewarding_details" + ], + "properties": { + "get_node_rewarding_details": { + "type": "object", + "required": [ + "node_id" + ], + "properties": { + "node_id": { + "description": "Id of the node to query.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Gets the stake saturation of a nym-node with the provided id.", + "type": "object", + "required": [ + "get_node_stake_saturation" + ], + "properties": { + "get_node_stake_saturation": { + "type": "object", + "required": [ + "node_id" + ], + "properties": { + "node_id": { + "description": "Id of the node to query.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "get_role_assignment" + ], + "properties": { + "get_role_assignment": { + "type": "object", + "required": [ + "role" + ], + "properties": { + "role": { + "$ref": "#/definitions/Role" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "get_rewarded_set_metadata" + ], + "properties": { + "get_rewarded_set_metadata": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Gets all delegations associated with particular node", + "type": "object", + "required": [ + "get_node_delegations" + ], + "properties": { + "get_node_delegations": { + "type": "object", + "required": [ + "node_id" + ], + "properties": { + "limit": { + "description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "node_id": { "description": "Id of the node to query.", "type": "integer", "format": "uint32", @@ -838,14 +1034,14 @@ "type": "object", "required": [ "delegator", - "mix_id" + "node_id" ], "properties": { "delegator": { "description": "The address of the owner of the delegation.", "type": "string" }, - "mix_id": { + "node_id": { "description": "Id of the node to query.", "type": "integer", "format": "uint32", @@ -935,16 +1131,16 @@ "description": "Gets the reward amount accrued by the particular mixnode that has not yet been claimed.", "type": "object", "required": [ - "get_pending_mix_node_operator_reward" + "get_pending_node_operator_reward" ], "properties": { - "get_pending_mix_node_operator_reward": { + "get_pending_node_operator_reward": { "type": "object", "required": [ - "mix_id" + "node_id" ], "properties": { - "mix_id": { + "node_id": { "description": "Id of the node to query.", "type": "integer", "format": "uint32", @@ -967,14 +1163,14 @@ "type": "object", "required": [ "address", - "mix_id" + "node_id" ], "properties": { "address": { "description": "Address of the delegator to use for the query.", "type": "string" }, - "mix_id": { + "node_id": { "description": "Id of the node to query.", "type": "integer", "format": "uint32", @@ -1004,7 +1200,7 @@ "type": "object", "required": [ "estimated_performance", - "mix_id" + "node_id" ], "properties": { "estimated_performance": { @@ -1015,7 +1211,18 @@ } ] }, - "mix_id": { + "estimated_work": { + "description": "The estimated work for the current epoch of the given node.", + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + }, + "node_id": { "description": "Id of the node to query.", "type": "integer", "format": "uint32", @@ -1039,7 +1246,7 @@ "required": [ "address", "estimated_performance", - "mix_id" + "node_id" ], "properties": { "address": { @@ -1054,18 +1261,22 @@ } ] }, - "mix_id": { + "estimated_work": { + "description": "The estimated work for the current epoch of the given node.", + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + }, + "node_id": { "description": "Id of the node to query.", "type": "integer", "format": "uint32", "minimum": 0.0 - }, - "proxy": { - "description": "Entity who made the delegation on behalf of the owner. If present, it's most likely the address of the vesting contract.", - "type": [ - "string", - "null" - ] } }, "additionalProperties": false @@ -1241,6 +1452,17 @@ "$ref": "#/definitions/Decimal" } ] + }, + "Role": { + "type": "string", + "enum": [ + "eg", + "l1", + "l2", + "l3", + "xg", + "stb" + ] } } } diff --git a/contracts/mixnet/schema/raw/response_to_get_all_delegations.json b/contracts/mixnet/schema/raw/response_to_get_all_delegations.json index a919220c46..71878b1827 100644 --- a/contracts/mixnet/schema/raw/response_to_get_all_delegations.json +++ b/contracts/mixnet/schema/raw/response_to_get_all_delegations.json @@ -66,7 +66,7 @@ "amount", "cumulative_reward_ratio", "height", - "mix_id", + "node_id", "owner" ], "properties": { @@ -92,8 +92,8 @@ "format": "uint64", "minimum": 0.0 }, - "mix_id": { - "description": "Id of the MixNode that this delegation was performed against.", + "node_id": { + "description": "Id of the Node that this delegation was performed against.", "type": "integer", "format": "uint32", "minimum": 0.0 diff --git a/contracts/mixnet/schema/raw/response_to_get_bonded_mixnode_details_by_identity.json b/contracts/mixnet/schema/raw/response_to_get_bonded_mixnode_details_by_identity.json index 59fc029331..8bde1a5732 100644 --- a/contracts/mixnet/schema/raw/response_to_get_bonded_mixnode_details_by_identity.json +++ b/contracts/mixnet/schema/raw/response_to_get_bonded_mixnode_details_by_identity.json @@ -48,14 +48,6 @@ "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", "type": "string" }, - "Layer": { - "type": "string", - "enum": [ - "One", - "Two", - "Three" - ] - }, "MixNode": { "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", "type": "object", @@ -112,7 +104,6 @@ "required": [ "bonding_height", "is_unbonding", - "layer", "mix_id", "mix_node", "original_pledge", @@ -129,14 +120,6 @@ "description": "Flag to indicate whether this node is in the process of unbonding, that will conclude upon the epoch finishing.", "type": "boolean" }, - "layer": { - "description": "Layer assigned to this mixnode.", - "allOf": [ - { - "$ref": "#/definitions/Layer" - } - ] - }, "mix_id": { "description": "Unique id assigned to the bonded mixnode.", "type": "integer", @@ -178,35 +161,7 @@ } ] } - }, - "additionalProperties": false - }, - "MixNodeCostParams": { - "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", - "type": "object", - "required": [ - "interval_operating_cost", - "profit_margin_percent" - ], - "properties": { - "interval_operating_cost": { - "description": "Operating cost of the associated mixnode per the entire interval.", - "allOf": [ - { - "$ref": "#/definitions/Coin" - } - ] - }, - "profit_margin_percent": { - "description": "The profit margin of the associated mixnode, i.e. the desired percent of the reward to be distributed to the operator.", - "allOf": [ - { - "$ref": "#/definitions/Percent" - } - ] - } - }, - "additionalProperties": false + } }, "MixNodeDetails": { "description": "Full details associated with given mixnode.", @@ -227,6 +182,7 @@ "pending_changes": { "description": "Adjustments to the mixnode that are ought to happen during future epoch transitions.", "default": { + "cost_params_change": null, "pledge_change": null }, "allOf": [ @@ -239,14 +195,41 @@ "description": "Details used for computation of rewarding related data.", "allOf": [ { - "$ref": "#/definitions/MixNodeRewarding" + "$ref": "#/definitions/NodeRewarding" } ] } }, "additionalProperties": false }, - "MixNodeRewarding": { + "NodeCostParams": { + "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", + "type": "object", + "required": [ + "interval_operating_cost", + "profit_margin_percent" + ], + "properties": { + "interval_operating_cost": { + "description": "Operating cost of the associated node per the entire interval.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "profit_margin_percent": { + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", + "allOf": [ + { + "$ref": "#/definitions/Percent" + } + ] + } + }, + "additionalProperties": false + }, + "NodeRewarding": { "type": "object", "required": [ "cost_params", @@ -262,7 +245,7 @@ "description": "Information provided by the operator that influence the cost function.", "allOf": [ { - "$ref": "#/definitions/MixNodeCostParams" + "$ref": "#/definitions/NodeCostParams" } ] }, @@ -302,7 +285,7 @@ "minimum": 0.0 }, "unit_delegation": { - "description": "Value of the theoretical \"unit delegation\" that has delegated to this mixnode at block 0.", + "description": "Value of the theoretical \"unit delegation\" that has delegated to this node at block 0.", "allOf": [ { "$ref": "#/definitions/Decimal" @@ -315,6 +298,15 @@ "PendingMixNodeChanges": { "type": "object", "properties": { + "cost_params_change": { + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, "pledge_change": { "type": [ "integer", diff --git a/contracts/mixnet/schema/raw/response_to_get_delegation_details.json b/contracts/mixnet/schema/raw/response_to_get_delegation_details.json index 8371493b30..f8807370a5 100644 --- a/contracts/mixnet/schema/raw/response_to_get_delegation_details.json +++ b/contracts/mixnet/schema/raw/response_to_get_delegation_details.json @@ -1,10 +1,11 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "MixNodeDelegationResponse", + "title": "NodeDelegationResponse", "description": "Response containing delegation details.", "type": "object", "required": [ - "mixnode_still_bonded" + "mixnode_still_bonded", + "node_still_bonded" ], "properties": { "delegation": { @@ -20,6 +21,10 @@ }, "mixnode_still_bonded": { "description": "Flag indicating whether the node towards which the delegation was made is still bonded in the network.", + "deprecated": true, + "type": "boolean" + }, + "node_still_bonded": { "type": "boolean" } }, @@ -55,7 +60,7 @@ "amount", "cumulative_reward_ratio", "height", - "mix_id", + "node_id", "owner" ], "properties": { @@ -81,8 +86,8 @@ "format": "uint64", "minimum": 0.0 }, - "mix_id": { - "description": "Id of the MixNode that this delegation was performed against.", + "node_id": { + "description": "Id of the Node that this delegation was performed against.", "type": "integer", "format": "uint32", "minimum": 0.0 diff --git a/contracts/mixnet/schema/raw/response_to_get_delegator_delegations.json b/contracts/mixnet/schema/raw/response_to_get_delegator_delegations.json index b6e7e7a6ee..e3556f1db2 100644 --- a/contracts/mixnet/schema/raw/response_to_get_delegator_delegations.json +++ b/contracts/mixnet/schema/raw/response_to_get_delegator_delegations.json @@ -66,7 +66,7 @@ "amount", "cumulative_reward_ratio", "height", - "mix_id", + "node_id", "owner" ], "properties": { @@ -92,8 +92,8 @@ "format": "uint64", "minimum": 0.0 }, - "mix_id": { - "description": "Id of the MixNode that this delegation was performed against.", + "node_id": { + "description": "Id of the Node that this delegation was performed against.", "type": "integer", "format": "uint32", "minimum": 0.0 diff --git a/contracts/mixnet/schema/raw/response_to_get_epoch_status.json b/contracts/mixnet/schema/raw/response_to_get_epoch_status.json index 1a85e633bf..cd4a2b0016 100644 --- a/contracts/mixnet/schema/raw/response_to_get_epoch_status.json +++ b/contracts/mixnet/schema/raw/response_to_get_epoch_status.json @@ -81,13 +81,39 @@ ] }, { - "description": "Represents the state of an epoch when all mixnodes have already been rewarded for their work in this epoch, all issued actions got resolved and the epoch should now be advanced whilst assigning new rewarded set.", - "type": "string", - "enum": [ - "advancing_epoch" - ] + "description": "Represents the state of an epoch when all nodes have already been rewarded for their work in this epoch, all issued actions got resolved and node roles should now be assigned before advancing into the next epoch.", + "type": "object", + "required": [ + "role_assignment" + ], + "properties": { + "role_assignment": { + "type": "object", + "required": [ + "next" + ], + "properties": { + "next": { + "$ref": "#/definitions/Role" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false } ] + }, + "Role": { + "type": "string", + "enum": [ + "eg", + "l1", + "l2", + "l3", + "xg", + "stb" + ] } } } diff --git a/contracts/mixnet/schema/raw/response_to_get_mix_node_bonds.json b/contracts/mixnet/schema/raw/response_to_get_mix_node_bonds.json index 61bdf6b3f1..ceb99ca8cb 100644 --- a/contracts/mixnet/schema/raw/response_to_get_mix_node_bonds.json +++ b/contracts/mixnet/schema/raw/response_to_get_mix_node_bonds.json @@ -52,14 +52,6 @@ } } }, - "Layer": { - "type": "string", - "enum": [ - "One", - "Two", - "Three" - ] - }, "MixNode": { "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", "type": "object", @@ -116,7 +108,6 @@ "required": [ "bonding_height", "is_unbonding", - "layer", "mix_id", "mix_node", "original_pledge", @@ -133,14 +124,6 @@ "description": "Flag to indicate whether this node is in the process of unbonding, that will conclude upon the epoch finishing.", "type": "boolean" }, - "layer": { - "description": "Layer assigned to this mixnode.", - "allOf": [ - { - "$ref": "#/definitions/Layer" - } - ] - }, "mix_id": { "description": "Unique id assigned to the bonded mixnode.", "type": "integer", @@ -182,8 +165,7 @@ } ] } - }, - "additionalProperties": false + } }, "Uint128": { "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", diff --git a/contracts/mixnet/schema/raw/response_to_get_mix_nodes_detailed.json b/contracts/mixnet/schema/raw/response_to_get_mix_nodes_detailed.json index bc95bb0839..4bcb8772ac 100644 --- a/contracts/mixnet/schema/raw/response_to_get_mix_nodes_detailed.json +++ b/contracts/mixnet/schema/raw/response_to_get_mix_nodes_detailed.json @@ -56,14 +56,6 @@ "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", "type": "string" }, - "Layer": { - "type": "string", - "enum": [ - "One", - "Two", - "Three" - ] - }, "MixNode": { "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", "type": "object", @@ -120,7 +112,6 @@ "required": [ "bonding_height", "is_unbonding", - "layer", "mix_id", "mix_node", "original_pledge", @@ -137,14 +128,6 @@ "description": "Flag to indicate whether this node is in the process of unbonding, that will conclude upon the epoch finishing.", "type": "boolean" }, - "layer": { - "description": "Layer assigned to this mixnode.", - "allOf": [ - { - "$ref": "#/definitions/Layer" - } - ] - }, "mix_id": { "description": "Unique id assigned to the bonded mixnode.", "type": "integer", @@ -186,35 +169,7 @@ } ] } - }, - "additionalProperties": false - }, - "MixNodeCostParams": { - "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", - "type": "object", - "required": [ - "interval_operating_cost", - "profit_margin_percent" - ], - "properties": { - "interval_operating_cost": { - "description": "Operating cost of the associated mixnode per the entire interval.", - "allOf": [ - { - "$ref": "#/definitions/Coin" - } - ] - }, - "profit_margin_percent": { - "description": "The profit margin of the associated mixnode, i.e. the desired percent of the reward to be distributed to the operator.", - "allOf": [ - { - "$ref": "#/definitions/Percent" - } - ] - } - }, - "additionalProperties": false + } }, "MixNodeDetails": { "description": "Full details associated with given mixnode.", @@ -235,6 +190,7 @@ "pending_changes": { "description": "Adjustments to the mixnode that are ought to happen during future epoch transitions.", "default": { + "cost_params_change": null, "pledge_change": null }, "allOf": [ @@ -247,14 +203,41 @@ "description": "Details used for computation of rewarding related data.", "allOf": [ { - "$ref": "#/definitions/MixNodeRewarding" + "$ref": "#/definitions/NodeRewarding" } ] } }, "additionalProperties": false }, - "MixNodeRewarding": { + "NodeCostParams": { + "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", + "type": "object", + "required": [ + "interval_operating_cost", + "profit_margin_percent" + ], + "properties": { + "interval_operating_cost": { + "description": "Operating cost of the associated node per the entire interval.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "profit_margin_percent": { + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", + "allOf": [ + { + "$ref": "#/definitions/Percent" + } + ] + } + }, + "additionalProperties": false + }, + "NodeRewarding": { "type": "object", "required": [ "cost_params", @@ -270,7 +253,7 @@ "description": "Information provided by the operator that influence the cost function.", "allOf": [ { - "$ref": "#/definitions/MixNodeCostParams" + "$ref": "#/definitions/NodeCostParams" } ] }, @@ -310,7 +293,7 @@ "minimum": 0.0 }, "unit_delegation": { - "description": "Value of the theoretical \"unit delegation\" that has delegated to this mixnode at block 0.", + "description": "Value of the theoretical \"unit delegation\" that has delegated to this node at block 0.", "allOf": [ { "$ref": "#/definitions/Decimal" @@ -323,6 +306,15 @@ "PendingMixNodeChanges": { "type": "object", "properties": { + "cost_params_change": { + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, "pledge_change": { "type": [ "integer", diff --git a/contracts/mixnet/schema/raw/response_to_get_mixnode_delegations.json b/contracts/mixnet/schema/raw/response_to_get_mixnode_delegations.json index 82d8c56b90..dccb346034 100644 --- a/contracts/mixnet/schema/raw/response_to_get_mixnode_delegations.json +++ b/contracts/mixnet/schema/raw/response_to_get_mixnode_delegations.json @@ -54,7 +54,7 @@ "amount", "cumulative_reward_ratio", "height", - "mix_id", + "node_id", "owner" ], "properties": { @@ -80,8 +80,8 @@ "format": "uint64", "minimum": 0.0 }, - "mix_id": { - "description": "Id of the MixNode that this delegation was performed against.", + "node_id": { + "description": "Id of the Node that this delegation was performed against.", "type": "integer", "format": "uint32", "minimum": 0.0 diff --git a/contracts/mixnet/schema/raw/response_to_get_mixnode_details.json b/contracts/mixnet/schema/raw/response_to_get_mixnode_details.json index 4d7bd42da4..68d3856e11 100644 --- a/contracts/mixnet/schema/raw/response_to_get_mixnode_details.json +++ b/contracts/mixnet/schema/raw/response_to_get_mixnode_details.json @@ -50,14 +50,6 @@ "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", "type": "string" }, - "Layer": { - "type": "string", - "enum": [ - "One", - "Two", - "Three" - ] - }, "MixNode": { "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", "type": "object", @@ -114,7 +106,6 @@ "required": [ "bonding_height", "is_unbonding", - "layer", "mix_id", "mix_node", "original_pledge", @@ -131,14 +122,6 @@ "description": "Flag to indicate whether this node is in the process of unbonding, that will conclude upon the epoch finishing.", "type": "boolean" }, - "layer": { - "description": "Layer assigned to this mixnode.", - "allOf": [ - { - "$ref": "#/definitions/Layer" - } - ] - }, "mix_id": { "description": "Unique id assigned to the bonded mixnode.", "type": "integer", @@ -180,35 +163,7 @@ } ] } - }, - "additionalProperties": false - }, - "MixNodeCostParams": { - "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", - "type": "object", - "required": [ - "interval_operating_cost", - "profit_margin_percent" - ], - "properties": { - "interval_operating_cost": { - "description": "Operating cost of the associated mixnode per the entire interval.", - "allOf": [ - { - "$ref": "#/definitions/Coin" - } - ] - }, - "profit_margin_percent": { - "description": "The profit margin of the associated mixnode, i.e. the desired percent of the reward to be distributed to the operator.", - "allOf": [ - { - "$ref": "#/definitions/Percent" - } - ] - } - }, - "additionalProperties": false + } }, "MixNodeDetails": { "description": "Full details associated with given mixnode.", @@ -229,6 +184,7 @@ "pending_changes": { "description": "Adjustments to the mixnode that are ought to happen during future epoch transitions.", "default": { + "cost_params_change": null, "pledge_change": null }, "allOf": [ @@ -241,14 +197,41 @@ "description": "Details used for computation of rewarding related data.", "allOf": [ { - "$ref": "#/definitions/MixNodeRewarding" + "$ref": "#/definitions/NodeRewarding" } ] } }, "additionalProperties": false }, - "MixNodeRewarding": { + "NodeCostParams": { + "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", + "type": "object", + "required": [ + "interval_operating_cost", + "profit_margin_percent" + ], + "properties": { + "interval_operating_cost": { + "description": "Operating cost of the associated node per the entire interval.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "profit_margin_percent": { + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", + "allOf": [ + { + "$ref": "#/definitions/Percent" + } + ] + } + }, + "additionalProperties": false + }, + "NodeRewarding": { "type": "object", "required": [ "cost_params", @@ -264,7 +247,7 @@ "description": "Information provided by the operator that influence the cost function.", "allOf": [ { - "$ref": "#/definitions/MixNodeCostParams" + "$ref": "#/definitions/NodeCostParams" } ] }, @@ -304,7 +287,7 @@ "minimum": 0.0 }, "unit_delegation": { - "description": "Value of the theoretical \"unit delegation\" that has delegated to this mixnode at block 0.", + "description": "Value of the theoretical \"unit delegation\" that has delegated to this node at block 0.", "allOf": [ { "$ref": "#/definitions/Decimal" @@ -317,6 +300,15 @@ "PendingMixNodeChanges": { "type": "object", "properties": { + "cost_params_change": { + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, "pledge_change": { "type": [ "integer", diff --git a/contracts/mixnet/schema/raw/response_to_get_mixnode_rewarding_details.json b/contracts/mixnet/schema/raw/response_to_get_mixnode_rewarding_details.json index 9b30dc4a75..dcb9cb67cd 100644 --- a/contracts/mixnet/schema/raw/response_to_get_mixnode_rewarding_details.json +++ b/contracts/mixnet/schema/raw/response_to_get_mixnode_rewarding_details.json @@ -17,7 +17,7 @@ "description": "If there exists a mixnode with the provided id, this field contains its rewarding information.", "anyOf": [ { - "$ref": "#/definitions/MixNodeRewarding" + "$ref": "#/definitions/NodeRewarding" }, { "type": "null" @@ -46,7 +46,7 @@ "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", "type": "string" }, - "MixNodeCostParams": { + "NodeCostParams": { "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", "type": "object", "required": [ @@ -55,7 +55,7 @@ ], "properties": { "interval_operating_cost": { - "description": "Operating cost of the associated mixnode per the entire interval.", + "description": "Operating cost of the associated node per the entire interval.", "allOf": [ { "$ref": "#/definitions/Coin" @@ -63,7 +63,7 @@ ] }, "profit_margin_percent": { - "description": "The profit margin of the associated mixnode, i.e. the desired percent of the reward to be distributed to the operator.", + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", "allOf": [ { "$ref": "#/definitions/Percent" @@ -73,7 +73,7 @@ }, "additionalProperties": false }, - "MixNodeRewarding": { + "NodeRewarding": { "type": "object", "required": [ "cost_params", @@ -89,7 +89,7 @@ "description": "Information provided by the operator that influence the cost function.", "allOf": [ { - "$ref": "#/definitions/MixNodeCostParams" + "$ref": "#/definitions/NodeCostParams" } ] }, @@ -129,7 +129,7 @@ "minimum": 0.0 }, "unit_delegation": { - "description": "Value of the theoretical \"unit delegation\" that has delegated to this mixnode at block 0.", + "description": "Value of the theoretical \"unit delegation\" that has delegated to this node at block 0.", "allOf": [ { "$ref": "#/definitions/Decimal" diff --git a/contracts/mixnet/schema/raw/response_to_get_node_delegations.json b/contracts/mixnet/schema/raw/response_to_get_node_delegations.json new file mode 100644 index 0000000000..c8ff4df221 --- /dev/null +++ b/contracts/mixnet/schema/raw/response_to_get_node_delegations.json @@ -0,0 +1,116 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PagedNodeDelegationsResponse", + "description": "Response containing paged list of all delegations made towards particular node.", + "type": "object", + "required": [ + "delegations" + ], + "properties": { + "delegations": { + "description": "Each individual delegation made.", + "type": "array", + "items": { + "$ref": "#/definitions/Delegation" + } + }, + "start_next_after": { + "description": "Field indicating paging information for the following queries if the caller wishes to get further entries.", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "Delegation": { + "description": "Information about tokens being delegated towards given mixnode in order to accrue rewards with their work.", + "type": "object", + "required": [ + "amount", + "cumulative_reward_ratio", + "height", + "node_id", + "owner" + ], + "properties": { + "amount": { + "description": "Original delegation amount. Note that it is never mutated as delegation accumulates rewards.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "cumulative_reward_ratio": { + "description": "Value of the \"unit delegation\" associated with the mixnode at the time of delegation.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "height": { + "description": "Block height where this delegation occurred.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "node_id": { + "description": "Id of the Node that this delegation was performed against.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "owner": { + "description": "Address of the owner of this delegation.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "proxy": { + "description": "Proxy address used to delegate the funds on behalf of another address", + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } +} diff --git a/contracts/mixnet/schema/raw/response_to_get_node_rewarding_details.json b/contracts/mixnet/schema/raw/response_to_get_node_rewarding_details.json new file mode 100644 index 0000000000..e9b3a6e545 --- /dev/null +++ b/contracts/mixnet/schema/raw/response_to_get_node_rewarding_details.json @@ -0,0 +1,155 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NodeRewardingDetailsResponse", + "description": "Response containing rewarding information of a node with the provided id.", + "type": "object", + "required": [ + "node_id" + ], + "properties": { + "node_id": { + "description": "Id of the requested node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "rewarding_details": { + "description": "If there exists a node with the provided id, this field contains its rewarding information.", + "anyOf": [ + { + "$ref": "#/definitions/NodeRewarding" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "NodeCostParams": { + "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", + "type": "object", + "required": [ + "interval_operating_cost", + "profit_margin_percent" + ], + "properties": { + "interval_operating_cost": { + "description": "Operating cost of the associated node per the entire interval.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "profit_margin_percent": { + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", + "allOf": [ + { + "$ref": "#/definitions/Percent" + } + ] + } + }, + "additionalProperties": false + }, + "NodeRewarding": { + "type": "object", + "required": [ + "cost_params", + "delegates", + "last_rewarded_epoch", + "operator", + "total_unit_reward", + "unique_delegations", + "unit_delegation" + ], + "properties": { + "cost_params": { + "description": "Information provided by the operator that influence the cost function.", + "allOf": [ + { + "$ref": "#/definitions/NodeCostParams" + } + ] + }, + "delegates": { + "description": "Total delegation and compounded reward earned by all node delegators.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "last_rewarded_epoch": { + "description": "Marks the epoch when this node was last rewarded so that we wouldn't accidentally attempt to reward it multiple times in the same epoch.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "operator": { + "description": "Total pledge and compounded reward earned by the node operator.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "total_unit_reward": { + "description": "Cumulative reward earned by the \"unit delegation\" since the block 0.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "unique_delegations": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "unit_delegation": { + "description": "Value of the theoretical \"unit delegation\" that has delegated to this node at block 0.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + }, + "additionalProperties": false + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } +} diff --git a/contracts/mixnet/schema/raw/response_to_get_node_stake_saturation.json b/contracts/mixnet/schema/raw/response_to_get_node_stake_saturation.json new file mode 100644 index 0000000000..3aa1cddc13 --- /dev/null +++ b/contracts/mixnet/schema/raw/response_to_get_node_stake_saturation.json @@ -0,0 +1,46 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "StakeSaturationResponse", + "description": "Response containing the current state of the stake saturation of a node with the provided id.", + "type": "object", + "required": [ + "node_id" + ], + "properties": { + "current_saturation": { + "description": "The current stake saturation of this node that is indirectly used in reward calculation formulas. Note that it can't be larger than 1.", + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + }, + "node_id": { + "description": "Id of the requested node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "uncapped_saturation": { + "description": "The current, absolute, stake saturation of this node. Note that as the name suggests it can be larger than 1. However, anything beyond that value has no effect on the total node reward.", + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + } + } +} diff --git a/contracts/mixnet/schema/raw/response_to_get_nym_node_bonds_paged.json b/contracts/mixnet/schema/raw/response_to_get_nym_node_bonds_paged.json new file mode 100644 index 0000000000..6be0563f7a --- /dev/null +++ b/contracts/mixnet/schema/raw/response_to_get_nym_node_bonds_paged.json @@ -0,0 +1,134 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PagedNymNodeBondsResponse", + "type": "object", + "required": [ + "nodes" + ], + "properties": { + "nodes": { + "description": "The nym node bond information present in the contract.", + "type": "array", + "items": { + "$ref": "#/definitions/NymNodeBond" + } + }, + "start_next_after": { + "description": "Field indicating paging information for the following queries if the caller wishes to get further entries.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "NymNode": { + "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", + "type": "object", + "required": [ + "host", + "identity_key" + ], + "properties": { + "custom_http_port": { + "description": "Allow specifying custom port for accessing the http, and thus self-described, api of this node for the capabilities discovery.", + "type": [ + "integer", + "null" + ], + "format": "uint16", + "minimum": 0.0 + }, + "host": { + "description": "Network address of this nym-node, for example 1.1.1.1 or foo.mixnode.com that is used to discover other capabilities of this node.", + "type": "string" + }, + "identity_key": { + "description": "Base58-encoded ed25519 EdDSA public key.", + "type": "string" + } + }, + "additionalProperties": false + }, + "NymNodeBond": { + "type": "object", + "required": [ + "bonding_height", + "is_unbonding", + "node", + "node_id", + "original_pledge", + "owner" + ], + "properties": { + "bonding_height": { + "description": "Block height at which this nym-node has been bonded.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "is_unbonding": { + "description": "Flag to indicate whether this node is in the process of unbonding, that will conclude upon the epoch finishing.", + "type": "boolean" + }, + "node": { + "description": "Information provided by the operator for the purposes of bonding.", + "allOf": [ + { + "$ref": "#/definitions/NymNode" + } + ] + }, + "node_id": { + "description": "Unique id assigned to the bonded node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "original_pledge": { + "description": "Original amount pledged by the operator of this node.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "owner": { + "description": "Address of the owner of this nym-node.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + } + }, + "additionalProperties": false + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } +} diff --git a/contracts/mixnet/schema/raw/response_to_get_nym_node_details.json b/contracts/mixnet/schema/raw/response_to_get_nym_node_details.json new file mode 100644 index 0000000000..4707dce857 --- /dev/null +++ b/contracts/mixnet/schema/raw/response_to_get_nym_node_details.json @@ -0,0 +1,299 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NodeDetailsResponse", + "description": "Response containing details of a node with the provided id.", + "type": "object", + "required": [ + "node_id" + ], + "properties": { + "details": { + "description": "If there exists a node with the provided id, this field contains its detailed information.", + "anyOf": [ + { + "$ref": "#/definitions/NymNodeDetails" + }, + { + "type": "null" + } + ] + }, + "node_id": { + "description": "Id of the requested node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "NodeCostParams": { + "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", + "type": "object", + "required": [ + "interval_operating_cost", + "profit_margin_percent" + ], + "properties": { + "interval_operating_cost": { + "description": "Operating cost of the associated node per the entire interval.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "profit_margin_percent": { + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", + "allOf": [ + { + "$ref": "#/definitions/Percent" + } + ] + } + }, + "additionalProperties": false + }, + "NodeRewarding": { + "type": "object", + "required": [ + "cost_params", + "delegates", + "last_rewarded_epoch", + "operator", + "total_unit_reward", + "unique_delegations", + "unit_delegation" + ], + "properties": { + "cost_params": { + "description": "Information provided by the operator that influence the cost function.", + "allOf": [ + { + "$ref": "#/definitions/NodeCostParams" + } + ] + }, + "delegates": { + "description": "Total delegation and compounded reward earned by all node delegators.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "last_rewarded_epoch": { + "description": "Marks the epoch when this node was last rewarded so that we wouldn't accidentally attempt to reward it multiple times in the same epoch.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "operator": { + "description": "Total pledge and compounded reward earned by the node operator.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "total_unit_reward": { + "description": "Cumulative reward earned by the \"unit delegation\" since the block 0.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "unique_delegations": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "unit_delegation": { + "description": "Value of the theoretical \"unit delegation\" that has delegated to this node at block 0.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + }, + "additionalProperties": false + }, + "NymNode": { + "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", + "type": "object", + "required": [ + "host", + "identity_key" + ], + "properties": { + "custom_http_port": { + "description": "Allow specifying custom port for accessing the http, and thus self-described, api of this node for the capabilities discovery.", + "type": [ + "integer", + "null" + ], + "format": "uint16", + "minimum": 0.0 + }, + "host": { + "description": "Network address of this nym-node, for example 1.1.1.1 or foo.mixnode.com that is used to discover other capabilities of this node.", + "type": "string" + }, + "identity_key": { + "description": "Base58-encoded ed25519 EdDSA public key.", + "type": "string" + } + }, + "additionalProperties": false + }, + "NymNodeBond": { + "type": "object", + "required": [ + "bonding_height", + "is_unbonding", + "node", + "node_id", + "original_pledge", + "owner" + ], + "properties": { + "bonding_height": { + "description": "Block height at which this nym-node has been bonded.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "is_unbonding": { + "description": "Flag to indicate whether this node is in the process of unbonding, that will conclude upon the epoch finishing.", + "type": "boolean" + }, + "node": { + "description": "Information provided by the operator for the purposes of bonding.", + "allOf": [ + { + "$ref": "#/definitions/NymNode" + } + ] + }, + "node_id": { + "description": "Unique id assigned to the bonded node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "original_pledge": { + "description": "Original amount pledged by the operator of this node.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "owner": { + "description": "Address of the owner of this nym-node.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + } + }, + "additionalProperties": false + }, + "NymNodeDetails": { + "description": "Full details associated with given node.", + "type": "object", + "required": [ + "bond_information", + "pending_changes", + "rewarding_details" + ], + "properties": { + "bond_information": { + "description": "Basic bond information of this node, such as owner address, original pledge, etc.", + "allOf": [ + { + "$ref": "#/definitions/NymNodeBond" + } + ] + }, + "pending_changes": { + "description": "Adjustments to the node that are scheduled to happen during future epoch/interval transitions.", + "allOf": [ + { + "$ref": "#/definitions/PendingNodeChanges" + } + ] + }, + "rewarding_details": { + "description": "Details used for computation of rewarding related data.", + "allOf": [ + { + "$ref": "#/definitions/NodeRewarding" + } + ] + } + }, + "additionalProperties": false + }, + "PendingNodeChanges": { + "type": "object", + "properties": { + "cost_params_change": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "pledge_change": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } +} diff --git a/contracts/mixnet/schema/raw/response_to_get_nym_node_details_by_identity_key.json b/contracts/mixnet/schema/raw/response_to_get_nym_node_details_by_identity_key.json new file mode 100644 index 0000000000..aea64e0746 --- /dev/null +++ b/contracts/mixnet/schema/raw/response_to_get_nym_node_details_by_identity_key.json @@ -0,0 +1,297 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NodeDetailsByIdentityResponse", + "description": "Response containing details of a bonded node with the provided identity key.", + "type": "object", + "required": [ + "identity_key" + ], + "properties": { + "details": { + "description": "If there exists a bonded node with the provided identity key, this field contains its detailed information.", + "anyOf": [ + { + "$ref": "#/definitions/NymNodeDetails" + }, + { + "type": "null" + } + ] + }, + "identity_key": { + "description": "The identity key (base58-encoded ed25519 public key) of the node.", + "type": "string" + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "NodeCostParams": { + "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", + "type": "object", + "required": [ + "interval_operating_cost", + "profit_margin_percent" + ], + "properties": { + "interval_operating_cost": { + "description": "Operating cost of the associated node per the entire interval.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "profit_margin_percent": { + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", + "allOf": [ + { + "$ref": "#/definitions/Percent" + } + ] + } + }, + "additionalProperties": false + }, + "NodeRewarding": { + "type": "object", + "required": [ + "cost_params", + "delegates", + "last_rewarded_epoch", + "operator", + "total_unit_reward", + "unique_delegations", + "unit_delegation" + ], + "properties": { + "cost_params": { + "description": "Information provided by the operator that influence the cost function.", + "allOf": [ + { + "$ref": "#/definitions/NodeCostParams" + } + ] + }, + "delegates": { + "description": "Total delegation and compounded reward earned by all node delegators.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "last_rewarded_epoch": { + "description": "Marks the epoch when this node was last rewarded so that we wouldn't accidentally attempt to reward it multiple times in the same epoch.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "operator": { + "description": "Total pledge and compounded reward earned by the node operator.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "total_unit_reward": { + "description": "Cumulative reward earned by the \"unit delegation\" since the block 0.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "unique_delegations": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "unit_delegation": { + "description": "Value of the theoretical \"unit delegation\" that has delegated to this node at block 0.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + }, + "additionalProperties": false + }, + "NymNode": { + "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", + "type": "object", + "required": [ + "host", + "identity_key" + ], + "properties": { + "custom_http_port": { + "description": "Allow specifying custom port for accessing the http, and thus self-described, api of this node for the capabilities discovery.", + "type": [ + "integer", + "null" + ], + "format": "uint16", + "minimum": 0.0 + }, + "host": { + "description": "Network address of this nym-node, for example 1.1.1.1 or foo.mixnode.com that is used to discover other capabilities of this node.", + "type": "string" + }, + "identity_key": { + "description": "Base58-encoded ed25519 EdDSA public key.", + "type": "string" + } + }, + "additionalProperties": false + }, + "NymNodeBond": { + "type": "object", + "required": [ + "bonding_height", + "is_unbonding", + "node", + "node_id", + "original_pledge", + "owner" + ], + "properties": { + "bonding_height": { + "description": "Block height at which this nym-node has been bonded.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "is_unbonding": { + "description": "Flag to indicate whether this node is in the process of unbonding, that will conclude upon the epoch finishing.", + "type": "boolean" + }, + "node": { + "description": "Information provided by the operator for the purposes of bonding.", + "allOf": [ + { + "$ref": "#/definitions/NymNode" + } + ] + }, + "node_id": { + "description": "Unique id assigned to the bonded node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "original_pledge": { + "description": "Original amount pledged by the operator of this node.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "owner": { + "description": "Address of the owner of this nym-node.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + } + }, + "additionalProperties": false + }, + "NymNodeDetails": { + "description": "Full details associated with given node.", + "type": "object", + "required": [ + "bond_information", + "pending_changes", + "rewarding_details" + ], + "properties": { + "bond_information": { + "description": "Basic bond information of this node, such as owner address, original pledge, etc.", + "allOf": [ + { + "$ref": "#/definitions/NymNodeBond" + } + ] + }, + "pending_changes": { + "description": "Adjustments to the node that are scheduled to happen during future epoch/interval transitions.", + "allOf": [ + { + "$ref": "#/definitions/PendingNodeChanges" + } + ] + }, + "rewarding_details": { + "description": "Details used for computation of rewarding related data.", + "allOf": [ + { + "$ref": "#/definitions/NodeRewarding" + } + ] + } + }, + "additionalProperties": false + }, + "PendingNodeChanges": { + "type": "object", + "properties": { + "cost_params_change": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "pledge_change": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } +} diff --git a/contracts/mixnet/schema/raw/response_to_get_nym_nodes_detailed_paged.json b/contracts/mixnet/schema/raw/response_to_get_nym_nodes_detailed_paged.json new file mode 100644 index 0000000000..0b96dd856c --- /dev/null +++ b/contracts/mixnet/schema/raw/response_to_get_nym_nodes_detailed_paged.json @@ -0,0 +1,297 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PagedNymNodeDetailsResponse", + "type": "object", + "required": [ + "nodes" + ], + "properties": { + "nodes": { + "description": "All nym-node details stored in the contract. Apart from the basic bond information it also contains details required for all future reward calculation as well as any pending changes requested by the operator.", + "type": "array", + "items": { + "$ref": "#/definitions/NymNodeDetails" + } + }, + "start_next_after": { + "description": "Field indicating paging information for the following queries if the caller wishes to get further entries.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "NodeCostParams": { + "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", + "type": "object", + "required": [ + "interval_operating_cost", + "profit_margin_percent" + ], + "properties": { + "interval_operating_cost": { + "description": "Operating cost of the associated node per the entire interval.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "profit_margin_percent": { + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", + "allOf": [ + { + "$ref": "#/definitions/Percent" + } + ] + } + }, + "additionalProperties": false + }, + "NodeRewarding": { + "type": "object", + "required": [ + "cost_params", + "delegates", + "last_rewarded_epoch", + "operator", + "total_unit_reward", + "unique_delegations", + "unit_delegation" + ], + "properties": { + "cost_params": { + "description": "Information provided by the operator that influence the cost function.", + "allOf": [ + { + "$ref": "#/definitions/NodeCostParams" + } + ] + }, + "delegates": { + "description": "Total delegation and compounded reward earned by all node delegators.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "last_rewarded_epoch": { + "description": "Marks the epoch when this node was last rewarded so that we wouldn't accidentally attempt to reward it multiple times in the same epoch.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "operator": { + "description": "Total pledge and compounded reward earned by the node operator.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "total_unit_reward": { + "description": "Cumulative reward earned by the \"unit delegation\" since the block 0.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "unique_delegations": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "unit_delegation": { + "description": "Value of the theoretical \"unit delegation\" that has delegated to this node at block 0.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + }, + "additionalProperties": false + }, + "NymNode": { + "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", + "type": "object", + "required": [ + "host", + "identity_key" + ], + "properties": { + "custom_http_port": { + "description": "Allow specifying custom port for accessing the http, and thus self-described, api of this node for the capabilities discovery.", + "type": [ + "integer", + "null" + ], + "format": "uint16", + "minimum": 0.0 + }, + "host": { + "description": "Network address of this nym-node, for example 1.1.1.1 or foo.mixnode.com that is used to discover other capabilities of this node.", + "type": "string" + }, + "identity_key": { + "description": "Base58-encoded ed25519 EdDSA public key.", + "type": "string" + } + }, + "additionalProperties": false + }, + "NymNodeBond": { + "type": "object", + "required": [ + "bonding_height", + "is_unbonding", + "node", + "node_id", + "original_pledge", + "owner" + ], + "properties": { + "bonding_height": { + "description": "Block height at which this nym-node has been bonded.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "is_unbonding": { + "description": "Flag to indicate whether this node is in the process of unbonding, that will conclude upon the epoch finishing.", + "type": "boolean" + }, + "node": { + "description": "Information provided by the operator for the purposes of bonding.", + "allOf": [ + { + "$ref": "#/definitions/NymNode" + } + ] + }, + "node_id": { + "description": "Unique id assigned to the bonded node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "original_pledge": { + "description": "Original amount pledged by the operator of this node.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "owner": { + "description": "Address of the owner of this nym-node.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + } + }, + "additionalProperties": false + }, + "NymNodeDetails": { + "description": "Full details associated with given node.", + "type": "object", + "required": [ + "bond_information", + "pending_changes", + "rewarding_details" + ], + "properties": { + "bond_information": { + "description": "Basic bond information of this node, such as owner address, original pledge, etc.", + "allOf": [ + { + "$ref": "#/definitions/NymNodeBond" + } + ] + }, + "pending_changes": { + "description": "Adjustments to the node that are scheduled to happen during future epoch/interval transitions.", + "allOf": [ + { + "$ref": "#/definitions/PendingNodeChanges" + } + ] + }, + "rewarding_details": { + "description": "Details used for computation of rewarding related data.", + "allOf": [ + { + "$ref": "#/definitions/NodeRewarding" + } + ] + } + }, + "additionalProperties": false + }, + "PendingNodeChanges": { + "type": "object", + "properties": { + "cost_params_change": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "pledge_change": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } +} diff --git a/contracts/mixnet/schema/raw/response_to_get_owned_mixnode.json b/contracts/mixnet/schema/raw/response_to_get_owned_mixnode.json index da895c4bf4..94867ce877 100644 --- a/contracts/mixnet/schema/raw/response_to_get_owned_mixnode.json +++ b/contracts/mixnet/schema/raw/response_to_get_owned_mixnode.json @@ -52,14 +52,6 @@ "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", "type": "string" }, - "Layer": { - "type": "string", - "enum": [ - "One", - "Two", - "Three" - ] - }, "MixNode": { "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", "type": "object", @@ -116,7 +108,6 @@ "required": [ "bonding_height", "is_unbonding", - "layer", "mix_id", "mix_node", "original_pledge", @@ -133,14 +124,6 @@ "description": "Flag to indicate whether this node is in the process of unbonding, that will conclude upon the epoch finishing.", "type": "boolean" }, - "layer": { - "description": "Layer assigned to this mixnode.", - "allOf": [ - { - "$ref": "#/definitions/Layer" - } - ] - }, "mix_id": { "description": "Unique id assigned to the bonded mixnode.", "type": "integer", @@ -182,35 +165,7 @@ } ] } - }, - "additionalProperties": false - }, - "MixNodeCostParams": { - "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", - "type": "object", - "required": [ - "interval_operating_cost", - "profit_margin_percent" - ], - "properties": { - "interval_operating_cost": { - "description": "Operating cost of the associated mixnode per the entire interval.", - "allOf": [ - { - "$ref": "#/definitions/Coin" - } - ] - }, - "profit_margin_percent": { - "description": "The profit margin of the associated mixnode, i.e. the desired percent of the reward to be distributed to the operator.", - "allOf": [ - { - "$ref": "#/definitions/Percent" - } - ] - } - }, - "additionalProperties": false + } }, "MixNodeDetails": { "description": "Full details associated with given mixnode.", @@ -231,6 +186,7 @@ "pending_changes": { "description": "Adjustments to the mixnode that are ought to happen during future epoch transitions.", "default": { + "cost_params_change": null, "pledge_change": null }, "allOf": [ @@ -243,14 +199,41 @@ "description": "Details used for computation of rewarding related data.", "allOf": [ { - "$ref": "#/definitions/MixNodeRewarding" + "$ref": "#/definitions/NodeRewarding" } ] } }, "additionalProperties": false }, - "MixNodeRewarding": { + "NodeCostParams": { + "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", + "type": "object", + "required": [ + "interval_operating_cost", + "profit_margin_percent" + ], + "properties": { + "interval_operating_cost": { + "description": "Operating cost of the associated node per the entire interval.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "profit_margin_percent": { + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", + "allOf": [ + { + "$ref": "#/definitions/Percent" + } + ] + } + }, + "additionalProperties": false + }, + "NodeRewarding": { "type": "object", "required": [ "cost_params", @@ -266,7 +249,7 @@ "description": "Information provided by the operator that influence the cost function.", "allOf": [ { - "$ref": "#/definitions/MixNodeCostParams" + "$ref": "#/definitions/NodeCostParams" } ] }, @@ -306,7 +289,7 @@ "minimum": 0.0 }, "unit_delegation": { - "description": "Value of the theoretical \"unit delegation\" that has delegated to this mixnode at block 0.", + "description": "Value of the theoretical \"unit delegation\" that has delegated to this node at block 0.", "allOf": [ { "$ref": "#/definitions/Decimal" @@ -319,6 +302,15 @@ "PendingMixNodeChanges": { "type": "object", "properties": { + "cost_params_change": { + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, "pledge_change": { "type": [ "integer", diff --git a/contracts/mixnet/schema/raw/response_to_get_owned_nym_node.json b/contracts/mixnet/schema/raw/response_to_get_owned_nym_node.json new file mode 100644 index 0000000000..ed1ccc194d --- /dev/null +++ b/contracts/mixnet/schema/raw/response_to_get_owned_nym_node.json @@ -0,0 +1,301 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NodeOwnershipResponse", + "description": "Response containing details of a node belonging to the particular owner.", + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "description": "Validated address of the node owner.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "details": { + "description": "If the provided address owns a nym-node, this field contains its detailed information.", + "anyOf": [ + { + "$ref": "#/definitions/NymNodeDetails" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "NodeCostParams": { + "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", + "type": "object", + "required": [ + "interval_operating_cost", + "profit_margin_percent" + ], + "properties": { + "interval_operating_cost": { + "description": "Operating cost of the associated node per the entire interval.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "profit_margin_percent": { + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", + "allOf": [ + { + "$ref": "#/definitions/Percent" + } + ] + } + }, + "additionalProperties": false + }, + "NodeRewarding": { + "type": "object", + "required": [ + "cost_params", + "delegates", + "last_rewarded_epoch", + "operator", + "total_unit_reward", + "unique_delegations", + "unit_delegation" + ], + "properties": { + "cost_params": { + "description": "Information provided by the operator that influence the cost function.", + "allOf": [ + { + "$ref": "#/definitions/NodeCostParams" + } + ] + }, + "delegates": { + "description": "Total delegation and compounded reward earned by all node delegators.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "last_rewarded_epoch": { + "description": "Marks the epoch when this node was last rewarded so that we wouldn't accidentally attempt to reward it multiple times in the same epoch.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "operator": { + "description": "Total pledge and compounded reward earned by the node operator.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "total_unit_reward": { + "description": "Cumulative reward earned by the \"unit delegation\" since the block 0.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "unique_delegations": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "unit_delegation": { + "description": "Value of the theoretical \"unit delegation\" that has delegated to this node at block 0.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + }, + "additionalProperties": false + }, + "NymNode": { + "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", + "type": "object", + "required": [ + "host", + "identity_key" + ], + "properties": { + "custom_http_port": { + "description": "Allow specifying custom port for accessing the http, and thus self-described, api of this node for the capabilities discovery.", + "type": [ + "integer", + "null" + ], + "format": "uint16", + "minimum": 0.0 + }, + "host": { + "description": "Network address of this nym-node, for example 1.1.1.1 or foo.mixnode.com that is used to discover other capabilities of this node.", + "type": "string" + }, + "identity_key": { + "description": "Base58-encoded ed25519 EdDSA public key.", + "type": "string" + } + }, + "additionalProperties": false + }, + "NymNodeBond": { + "type": "object", + "required": [ + "bonding_height", + "is_unbonding", + "node", + "node_id", + "original_pledge", + "owner" + ], + "properties": { + "bonding_height": { + "description": "Block height at which this nym-node has been bonded.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "is_unbonding": { + "description": "Flag to indicate whether this node is in the process of unbonding, that will conclude upon the epoch finishing.", + "type": "boolean" + }, + "node": { + "description": "Information provided by the operator for the purposes of bonding.", + "allOf": [ + { + "$ref": "#/definitions/NymNode" + } + ] + }, + "node_id": { + "description": "Unique id assigned to the bonded node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "original_pledge": { + "description": "Original amount pledged by the operator of this node.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "owner": { + "description": "Address of the owner of this nym-node.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + } + }, + "additionalProperties": false + }, + "NymNodeDetails": { + "description": "Full details associated with given node.", + "type": "object", + "required": [ + "bond_information", + "pending_changes", + "rewarding_details" + ], + "properties": { + "bond_information": { + "description": "Basic bond information of this node, such as owner address, original pledge, etc.", + "allOf": [ + { + "$ref": "#/definitions/NymNodeBond" + } + ] + }, + "pending_changes": { + "description": "Adjustments to the node that are scheduled to happen during future epoch/interval transitions.", + "allOf": [ + { + "$ref": "#/definitions/PendingNodeChanges" + } + ] + }, + "rewarding_details": { + "description": "Details used for computation of rewarding related data.", + "allOf": [ + { + "$ref": "#/definitions/NodeRewarding" + } + ] + } + }, + "additionalProperties": false + }, + "PendingNodeChanges": { + "type": "object", + "properties": { + "cost_params_change": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "pledge_change": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } +} diff --git a/contracts/mixnet/schema/raw/response_to_get_pending_delegator_reward.json b/contracts/mixnet/schema/raw/response_to_get_pending_delegator_reward.json index e885d5976c..4ae2297c32 100644 --- a/contracts/mixnet/schema/raw/response_to_get_pending_delegator_reward.json +++ b/contracts/mixnet/schema/raw/response_to_get_pending_delegator_reward.json @@ -4,7 +4,8 @@ "description": "Response containing information about accrued rewards.", "type": "object", "required": [ - "mixnode_still_fully_bonded" + "mixnode_still_fully_bonded", + "node_still_fully_bonded" ], "properties": { "amount_earned": { @@ -42,6 +43,10 @@ }, "mixnode_still_fully_bonded": { "description": "The associated mixnode is still fully bonded, meaning it is neither unbonded nor in the process of unbonding that would have finished at the epoch transition.", + "deprecated": true, + "type": "boolean" + }, + "node_still_fully_bonded": { "type": "boolean" } }, diff --git a/contracts/mixnet/schema/raw/response_to_get_pending_epoch_event.json b/contracts/mixnet/schema/raw/response_to_get_pending_epoch_event.json index 7588cad3e9..67b89db672 100644 --- a/contracts/mixnet/schema/raw/response_to_get_pending_epoch_event.json +++ b/contracts/mixnet/schema/raw/response_to_get_pending_epoch_event.json @@ -24,6 +24,36 @@ }, "additionalProperties": false, "definitions": { + "ActiveSetUpdate": { + "description": "Specification on how the active set should be updated.", + "type": "object", + "required": [ + "entry_gateways", + "exit_gateways", + "mixnodes" + ], + "properties": { + "entry_gateways": { + "description": "The expected number of nodes assigned entry gateway role (i.e. [`Role::EntryGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "exit_gateways": { + "description": "The expected number of nodes assigned exit gateway role (i.e. [`Role::ExitGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "mixnodes": { + "description": "The expected number of nodes assigned the 'mixnode' role, i.e. total of [`Role::Layer1`], [`Role::Layer2`] and [`Role::Layer3`].", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, "Addr": { "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", "type": "string" @@ -72,7 +102,7 @@ "description": "Enum encompassing all possible epoch events.", "oneOf": [ { - "description": "Request to create a delegation towards particular mixnode. Note that if such delegation already exists, it will get updated with the provided token amount.", + "description": "Request to create a delegation towards particular node. Note that if such delegation already exists, it will get updated with the provided token amount.", "type": "object", "required": [ "delegate" @@ -82,7 +112,7 @@ "type": "object", "required": [ "amount", - "mix_id", + "node_id", "owner" ], "properties": { @@ -94,8 +124,8 @@ } ] }, - "mix_id": { - "description": "The id of the mixnode used for the delegation.", + "node_id": { + "description": "The id of the node used for the delegation.", "type": "integer", "format": "uint32", "minimum": 0.0 @@ -126,7 +156,7 @@ "additionalProperties": false }, { - "description": "Request to remove delegation from particular mixnode.", + "description": "Request to remove delegation from particular node.", "type": "object", "required": [ "undelegate" @@ -135,12 +165,12 @@ "undelegate": { "type": "object", "required": [ - "mix_id", + "node_id", "owner" ], "properties": { - "mix_id": { - "description": "The id of the mixnode used for the delegation.", + "node_id": { + "description": "The id of the node used for the delegation.", "type": "integer", "format": "uint32", "minimum": 0.0 @@ -174,10 +204,44 @@ "description": "Request to pledge more tokens (by the node operator) towards its node.", "type": "object", "required": [ - "pledge_more" + "nym_node_pledge_more" ], "properties": { - "pledge_more": { + "nym_node_pledge_more": { + "type": "object", + "required": [ + "amount", + "node_id" + ], + "properties": { + "amount": { + "description": "The amount of additional tokens to use in the pledge.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "node_id": { + "description": "The id of the nym node that will have its pledge updated.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Request to pledge more tokens (by the node operator) towards its node.", + "type": "object", + "required": [ + "mixnode_pledge_more" + ], + "properties": { + "mixnode_pledge_more": { "type": "object", "required": [ "amount", @@ -185,7 +249,7 @@ ], "properties": { "amount": { - "description": "The amount of additional tokens to use by the pledge.", + "description": "The amount of additional tokens to use in the pledge.", "allOf": [ { "$ref": "#/definitions/Coin" @@ -208,10 +272,44 @@ "description": "Request to decrease amount of pledged tokens (by the node operator) from its node.", "type": "object", "required": [ - "decrease_pledge" + "nym_node_decrease_pledge" ], "properties": { - "decrease_pledge": { + "nym_node_decrease_pledge": { + "type": "object", + "required": [ + "decrease_by", + "node_id" + ], + "properties": { + "decrease_by": { + "description": "The amount of tokens that should be removed from the pledge.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "node_id": { + "description": "The id of the nym node that will have its pledge updated.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Request to decrease amount of pledged tokens (by the node operator) from its node.", + "type": "object", + "required": [ + "mixnode_decrease_pledge" + ], + "properties": { + "mixnode_decrease_pledge": { "type": "object", "required": [ "decrease_by", @@ -264,20 +362,20 @@ "additionalProperties": false }, { - "description": "Request to update the current size of the active set.", + "description": "Request to unbond a nym node and completely remove it from the network.", "type": "object", "required": [ - "update_active_set_size" + "unbond_nym_node" ], "properties": { - "update_active_set_size": { + "unbond_nym_node": { "type": "object", "required": [ - "new_size" + "node_id" ], "properties": { - "new_size": { - "description": "The new desired size of the active set.", + "node_id": { + "description": "The id of the node that will get unbonded.", "type": "integer", "format": "uint32", "minimum": 0.0 @@ -287,6 +385,28 @@ } }, "additionalProperties": false + }, + { + "description": "Request to update the current active set.", + "type": "object", + "required": [ + "update_active_set" + ], + "properties": { + "update_active_set": { + "type": "object", + "required": [ + "update" + ], + "properties": { + "update": { + "$ref": "#/definitions/ActiveSetUpdate" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false } ] }, diff --git a/contracts/mixnet/schema/raw/response_to_get_pending_epoch_events.json b/contracts/mixnet/schema/raw/response_to_get_pending_epoch_events.json index 09a4d03787..54a6a8e52f 100644 --- a/contracts/mixnet/schema/raw/response_to_get_pending_epoch_events.json +++ b/contracts/mixnet/schema/raw/response_to_get_pending_epoch_events.json @@ -32,6 +32,36 @@ }, "additionalProperties": false, "definitions": { + "ActiveSetUpdate": { + "description": "Specification on how the active set should be updated.", + "type": "object", + "required": [ + "entry_gateways", + "exit_gateways", + "mixnodes" + ], + "properties": { + "entry_gateways": { + "description": "The expected number of nodes assigned entry gateway role (i.e. [`Role::EntryGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "exit_gateways": { + "description": "The expected number of nodes assigned exit gateway role (i.e. [`Role::ExitGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "mixnodes": { + "description": "The expected number of nodes assigned the 'mixnode' role, i.e. total of [`Role::Layer1`], [`Role::Layer2`] and [`Role::Layer3`].", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, "Addr": { "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", "type": "string" @@ -105,7 +135,7 @@ "description": "Enum encompassing all possible epoch events.", "oneOf": [ { - "description": "Request to create a delegation towards particular mixnode. Note that if such delegation already exists, it will get updated with the provided token amount.", + "description": "Request to create a delegation towards particular node. Note that if such delegation already exists, it will get updated with the provided token amount.", "type": "object", "required": [ "delegate" @@ -115,7 +145,7 @@ "type": "object", "required": [ "amount", - "mix_id", + "node_id", "owner" ], "properties": { @@ -127,8 +157,8 @@ } ] }, - "mix_id": { - "description": "The id of the mixnode used for the delegation.", + "node_id": { + "description": "The id of the node used for the delegation.", "type": "integer", "format": "uint32", "minimum": 0.0 @@ -159,7 +189,7 @@ "additionalProperties": false }, { - "description": "Request to remove delegation from particular mixnode.", + "description": "Request to remove delegation from particular node.", "type": "object", "required": [ "undelegate" @@ -168,12 +198,12 @@ "undelegate": { "type": "object", "required": [ - "mix_id", + "node_id", "owner" ], "properties": { - "mix_id": { - "description": "The id of the mixnode used for the delegation.", + "node_id": { + "description": "The id of the node used for the delegation.", "type": "integer", "format": "uint32", "minimum": 0.0 @@ -207,10 +237,44 @@ "description": "Request to pledge more tokens (by the node operator) towards its node.", "type": "object", "required": [ - "pledge_more" + "nym_node_pledge_more" ], "properties": { - "pledge_more": { + "nym_node_pledge_more": { + "type": "object", + "required": [ + "amount", + "node_id" + ], + "properties": { + "amount": { + "description": "The amount of additional tokens to use in the pledge.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "node_id": { + "description": "The id of the nym node that will have its pledge updated.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Request to pledge more tokens (by the node operator) towards its node.", + "type": "object", + "required": [ + "mixnode_pledge_more" + ], + "properties": { + "mixnode_pledge_more": { "type": "object", "required": [ "amount", @@ -218,7 +282,7 @@ ], "properties": { "amount": { - "description": "The amount of additional tokens to use by the pledge.", + "description": "The amount of additional tokens to use in the pledge.", "allOf": [ { "$ref": "#/definitions/Coin" @@ -241,10 +305,44 @@ "description": "Request to decrease amount of pledged tokens (by the node operator) from its node.", "type": "object", "required": [ - "decrease_pledge" + "nym_node_decrease_pledge" ], "properties": { - "decrease_pledge": { + "nym_node_decrease_pledge": { + "type": "object", + "required": [ + "decrease_by", + "node_id" + ], + "properties": { + "decrease_by": { + "description": "The amount of tokens that should be removed from the pledge.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "node_id": { + "description": "The id of the nym node that will have its pledge updated.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Request to decrease amount of pledged tokens (by the node operator) from its node.", + "type": "object", + "required": [ + "mixnode_decrease_pledge" + ], + "properties": { + "mixnode_decrease_pledge": { "type": "object", "required": [ "decrease_by", @@ -297,20 +395,20 @@ "additionalProperties": false }, { - "description": "Request to update the current size of the active set.", + "description": "Request to unbond a nym node and completely remove it from the network.", "type": "object", "required": [ - "update_active_set_size" + "unbond_nym_node" ], "properties": { - "update_active_set_size": { + "unbond_nym_node": { "type": "object", "required": [ - "new_size" + "node_id" ], "properties": { - "new_size": { - "description": "The new desired size of the active set.", + "node_id": { + "description": "The id of the node that will get unbonded.", "type": "integer", "format": "uint32", "minimum": 0.0 @@ -320,6 +418,28 @@ } }, "additionalProperties": false + }, + { + "description": "Request to update the current active set.", + "type": "object", + "required": [ + "update_active_set" + ], + "properties": { + "update_active_set": { + "type": "object", + "required": [ + "update" + ], + "properties": { + "update": { + "$ref": "#/definitions/ActiveSetUpdate" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false } ] }, diff --git a/contracts/mixnet/schema/raw/response_to_get_pending_interval_event.json b/contracts/mixnet/schema/raw/response_to_get_pending_interval_event.json index a0877027a9..f6926bf188 100644 --- a/contracts/mixnet/schema/raw/response_to_get_pending_interval_event.json +++ b/contracts/mixnet/schema/raw/response_to_get_pending_interval_event.json @@ -80,14 +80,16 @@ } ] }, - "rewarded_set_size": { - "description": "Defines the new size of the rewarded set.", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 + "rewarded_set_params": { + "description": "Defines the parameters of the rewarded set.", + "anyOf": [ + { + "$ref": "#/definitions/RewardedSetParams" + }, + { + "type": "null" + } + ] }, "staking_supply": { "description": "Defines the new value of the staking supply.", @@ -125,7 +127,7 @@ }, "additionalProperties": false }, - "MixNodeCostParams": { + "NodeCostParams": { "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", "type": "object", "required": [ @@ -134,7 +136,7 @@ ], "properties": { "interval_operating_cost": { - "description": "Operating cost of the associated mixnode per the entire interval.", + "description": "Operating cost of the associated node per the entire interval.", "allOf": [ { "$ref": "#/definitions/Coin" @@ -142,7 +144,7 @@ ] }, "profit_margin_percent": { - "description": "The profit margin of the associated mixnode, i.e. the desired percent of the reward to be distributed to the operator.", + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", "allOf": [ { "$ref": "#/definitions/Percent" @@ -204,7 +206,7 @@ "description": "The new updated cost function of this mixnode.", "allOf": [ { - "$ref": "#/definitions/MixNodeCostParams" + "$ref": "#/definitions/NodeCostParams" } ] } @@ -214,6 +216,40 @@ }, "additionalProperties": false }, + { + "description": "Request to update cost parameters of given nym node.", + "type": "object", + "required": [ + "change_nym_node_cost_params" + ], + "properties": { + "change_nym_node_cost_params": { + "type": "object", + "required": [ + "new_costs", + "node_id" + ], + "properties": { + "new_costs": { + "description": "The new updated cost function of this nym node.", + "allOf": [ + { + "$ref": "#/definitions/NodeCostParams" + } + ] + }, + "node_id": { + "description": "The id of the nym node that will have its cost parameters updated.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "description": "Request to update the underlying rewarding parameters used by the system", "type": "object", @@ -283,6 +319,42 @@ } ] }, + "RewardedSetParams": { + "type": "object", + "required": [ + "entry_gateways", + "exit_gateways", + "mixnodes", + "standby" + ], + "properties": { + "entry_gateways": { + "description": "The expected number of nodes assigned entry gateway role (i.e. [`Role::EntryGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "exit_gateways": { + "description": "The expected number of nodes assigned exit gateway role (i.e. [`Role::ExitGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "mixnodes": { + "description": "The expected number of nodes assigned the 'mixnode' role, i.e. total of [`Role::Layer1`], [`Role::Layer2`] and [`Role::Layer3`].", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "standby": { + "description": "Number of nodes in the 'standby' set. (i.e. [`Role::Standby`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, "Uint128": { "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", "type": "string" diff --git a/contracts/mixnet/schema/raw/response_to_get_pending_interval_events.json b/contracts/mixnet/schema/raw/response_to_get_pending_interval_events.json index ccbdc22178..188e99023a 100644 --- a/contracts/mixnet/schema/raw/response_to_get_pending_interval_events.json +++ b/contracts/mixnet/schema/raw/response_to_get_pending_interval_events.json @@ -88,14 +88,16 @@ } ] }, - "rewarded_set_size": { - "description": "Defines the new size of the rewarded set.", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 + "rewarded_set_params": { + "description": "Defines the parameters of the rewarded set.", + "anyOf": [ + { + "$ref": "#/definitions/RewardedSetParams" + }, + { + "type": "null" + } + ] }, "staking_supply": { "description": "Defines the new value of the staking supply.", @@ -133,7 +135,7 @@ }, "additionalProperties": false }, - "MixNodeCostParams": { + "NodeCostParams": { "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", "type": "object", "required": [ @@ -142,7 +144,7 @@ ], "properties": { "interval_operating_cost": { - "description": "Operating cost of the associated mixnode per the entire interval.", + "description": "Operating cost of the associated node per the entire interval.", "allOf": [ { "$ref": "#/definitions/Coin" @@ -150,7 +152,7 @@ ] }, "profit_margin_percent": { - "description": "The profit margin of the associated mixnode, i.e. the desired percent of the reward to be distributed to the operator.", + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", "allOf": [ { "$ref": "#/definitions/Percent" @@ -237,7 +239,7 @@ "description": "The new updated cost function of this mixnode.", "allOf": [ { - "$ref": "#/definitions/MixNodeCostParams" + "$ref": "#/definitions/NodeCostParams" } ] } @@ -247,6 +249,40 @@ }, "additionalProperties": false }, + { + "description": "Request to update cost parameters of given nym node.", + "type": "object", + "required": [ + "change_nym_node_cost_params" + ], + "properties": { + "change_nym_node_cost_params": { + "type": "object", + "required": [ + "new_costs", + "node_id" + ], + "properties": { + "new_costs": { + "description": "The new updated cost function of this nym node.", + "allOf": [ + { + "$ref": "#/definitions/NodeCostParams" + } + ] + }, + "node_id": { + "description": "The id of the nym node that will have its cost parameters updated.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "description": "Request to update the underlying rewarding parameters used by the system", "type": "object", @@ -316,6 +352,42 @@ } ] }, + "RewardedSetParams": { + "type": "object", + "required": [ + "entry_gateways", + "exit_gateways", + "mixnodes", + "standby" + ], + "properties": { + "entry_gateways": { + "description": "The expected number of nodes assigned entry gateway role (i.e. [`Role::EntryGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "exit_gateways": { + "description": "The expected number of nodes assigned exit gateway role (i.e. [`Role::ExitGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "mixnodes": { + "description": "The expected number of nodes assigned the 'mixnode' role, i.e. total of [`Role::Layer1`], [`Role::Layer2`] and [`Role::Layer3`].", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "standby": { + "description": "Number of nodes in the 'standby' set. (i.e. [`Role::Standby`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, "Uint128": { "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", "type": "string" diff --git a/contracts/mixnet/schema/raw/response_to_get_pending_node_operator_reward.json b/contracts/mixnet/schema/raw/response_to_get_pending_node_operator_reward.json new file mode 100644 index 0000000000..4ae2297c32 --- /dev/null +++ b/contracts/mixnet/schema/raw/response_to_get_pending_node_operator_reward.json @@ -0,0 +1,79 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PendingRewardResponse", + "description": "Response containing information about accrued rewards.", + "type": "object", + "required": [ + "mixnode_still_fully_bonded", + "node_still_fully_bonded" + ], + "properties": { + "amount_earned": { + "description": "The amount of tokens that could be claimed.", + "anyOf": [ + { + "$ref": "#/definitions/Coin" + }, + { + "type": "null" + } + ] + }, + "amount_earned_detailed": { + "description": "The full pending rewards. Note that it's nearly identical to `amount_earned`, however, it contains few additional decimal points for more accurate reward calculation.", + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + }, + "amount_staked": { + "description": "The amount of tokens initially staked.", + "anyOf": [ + { + "$ref": "#/definitions/Coin" + }, + { + "type": "null" + } + ] + }, + "mixnode_still_fully_bonded": { + "description": "The associated mixnode is still fully bonded, meaning it is neither unbonded nor in the process of unbonding that would have finished at the epoch transition.", + "deprecated": true, + "type": "boolean" + }, + "node_still_fully_bonded": { + "type": "boolean" + } + }, + "additionalProperties": false, + "definitions": { + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } +} diff --git a/contracts/mixnet/schema/raw/response_to_get_pending_operator_reward.json b/contracts/mixnet/schema/raw/response_to_get_pending_operator_reward.json index e885d5976c..4ae2297c32 100644 --- a/contracts/mixnet/schema/raw/response_to_get_pending_operator_reward.json +++ b/contracts/mixnet/schema/raw/response_to_get_pending_operator_reward.json @@ -4,7 +4,8 @@ "description": "Response containing information about accrued rewards.", "type": "object", "required": [ - "mixnode_still_fully_bonded" + "mixnode_still_fully_bonded", + "node_still_fully_bonded" ], "properties": { "amount_earned": { @@ -42,6 +43,10 @@ }, "mixnode_still_fully_bonded": { "description": "The associated mixnode is still fully bonded, meaning it is neither unbonded nor in the process of unbonding that would have finished at the epoch transition.", + "deprecated": true, + "type": "boolean" + }, + "node_still_fully_bonded": { "type": "boolean" } }, diff --git a/contracts/mixnet/schema/raw/response_to_get_preassigned_gateway_ids.json b/contracts/mixnet/schema/raw/response_to_get_preassigned_gateway_ids.json new file mode 100644 index 0000000000..bfd6be0f84 --- /dev/null +++ b/contracts/mixnet/schema/raw/response_to_get_preassigned_gateway_ids.json @@ -0,0 +1,46 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PreassignedGatewayIdsResponse", + "type": "object", + "required": [ + "ids" + ], + "properties": { + "ids": { + "type": "array", + "items": { + "$ref": "#/definitions/PreassignedId" + } + }, + "start_next_after": { + "description": "Field indicating paging information for the following queries if the caller wishes to get further entries.", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false, + "definitions": { + "PreassignedId": { + "type": "object", + "required": [ + "identity", + "node_id" + ], + "properties": { + "identity": { + "description": "The identity key (base58-encoded ed25519 public key) of the gateway.", + "type": "string" + }, + "node_id": { + "description": "The id pre-assigned to this gateway", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } +} diff --git a/contracts/mixnet/schema/raw/response_to_get_rewarded_set_metadata.json b/contracts/mixnet/schema/raw/response_to_get_rewarded_set_metadata.json new file mode 100644 index 0000000000..54bdbf9f6c --- /dev/null +++ b/contracts/mixnet/schema/raw/response_to_get_rewarded_set_metadata.json @@ -0,0 +1,114 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "RolesMetadataResponse", + "type": "object", + "required": [ + "metadata" + ], + "properties": { + "metadata": { + "$ref": "#/definitions/RewardedSetMetadata" + } + }, + "additionalProperties": false, + "definitions": { + "RewardedSetMetadata": { + "description": "Metadata associated with the rewarded set.", + "type": "object", + "required": [ + "entry_gateway_metadata", + "epoch_id", + "exit_gateway_metadata", + "fully_assigned", + "layer1_metadata", + "layer2_metadata", + "layer3_metadata", + "standby_metadata" + ], + "properties": { + "entry_gateway_metadata": { + "description": "Metadata for the 'EntryGateway' role", + "allOf": [ + { + "$ref": "#/definitions/RoleMetadata" + } + ] + }, + "epoch_id": { + "description": "Epoch that this data corresponds to.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "exit_gateway_metadata": { + "description": "Metadata for the 'ExitGateway' role", + "allOf": [ + { + "$ref": "#/definitions/RoleMetadata" + } + ] + }, + "fully_assigned": { + "description": "Indicates whether all roles got assigned to the set for this epoch.", + "type": "boolean" + }, + "layer1_metadata": { + "description": "Metadata for the 'Layer1' role", + "allOf": [ + { + "$ref": "#/definitions/RoleMetadata" + } + ] + }, + "layer2_metadata": { + "description": "Metadata for the 'Layer2' role", + "allOf": [ + { + "$ref": "#/definitions/RoleMetadata" + } + ] + }, + "layer3_metadata": { + "description": "Metadata for the 'Layer3' role", + "allOf": [ + { + "$ref": "#/definitions/RoleMetadata" + } + ] + }, + "standby_metadata": { + "description": "Metadata for the 'Standby' role", + "allOf": [ + { + "$ref": "#/definitions/RoleMetadata" + } + ] + } + }, + "additionalProperties": false + }, + "RoleMetadata": { + "description": "Metadata associated with particular node role.", + "type": "object", + "required": [ + "highest_id", + "num_nodes" + ], + "properties": { + "highest_id": { + "description": "Highest, also latest, node-id of a node assigned this role.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "num_nodes": { + "description": "Number of nodes assigned this particular role.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } +} diff --git a/contracts/mixnet/schema/raw/response_to_get_rewarding_params.json b/contracts/mixnet/schema/raw/response_to_get_rewarding_params.json index 8840bc31f2..d99be0119b 100644 --- a/contracts/mixnet/schema/raw/response_to_get_rewarding_params.json +++ b/contracts/mixnet/schema/raw/response_to_get_rewarding_params.json @@ -4,17 +4,10 @@ "description": "Parameters used for reward calculation.", "type": "object", "required": [ - "active_set_size", "interval", - "rewarded_set_size" + "rewarded_set" ], "properties": { - "active_set_size": { - "description": "The expected number of mixnodes in the active set.", - "type": "integer", - "format": "uint32", - "minimum": 0.0 - }, "interval": { "description": "Parameters that should remain unchanged throughout an interval.", "allOf": [ @@ -23,11 +16,8 @@ } ] }, - "rewarded_set_size": { - "description": "The expected number of mixnodes in the rewarded set (i.e. active + standby).", - "type": "integer", - "format": "uint32", - "minimum": 0.0 + "rewarded_set": { + "$ref": "#/definitions/RewardedSetParams" } }, "additionalProperties": false, @@ -124,6 +114,42 @@ "$ref": "#/definitions/Decimal" } ] + }, + "RewardedSetParams": { + "type": "object", + "required": [ + "entry_gateways", + "exit_gateways", + "mixnodes", + "standby" + ], + "properties": { + "entry_gateways": { + "description": "The expected number of nodes assigned entry gateway role (i.e. [`Role::EntryGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "exit_gateways": { + "description": "The expected number of nodes assigned exit gateway role (i.e. [`Role::ExitGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "mixnodes": { + "description": "The expected number of nodes assigned the 'mixnode' role, i.e. total of [`Role::Layer1`], [`Role::Layer2`] and [`Role::Layer3`].", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "standby": { + "description": "Number of nodes in the 'standby' set. (i.e. [`Role::Standby`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false } } } diff --git a/contracts/mixnet/schema/raw/response_to_get_role_assignment.json b/contracts/mixnet/schema/raw/response_to_get_role_assignment.json new file mode 100644 index 0000000000..9d7ff6c4e3 --- /dev/null +++ b/contracts/mixnet/schema/raw/response_to_get_role_assignment.json @@ -0,0 +1,26 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "EpochAssignmentResponse", + "type": "object", + "required": [ + "epoch_id", + "nodes" + ], + "properties": { + "epoch_id": { + "description": "Epoch that this data corresponds to.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "nodes": { + "type": "array", + "items": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + } + }, + "additionalProperties": false +} diff --git a/contracts/mixnet/schema/raw/response_to_get_stake_saturation.json b/contracts/mixnet/schema/raw/response_to_get_stake_saturation.json index 4a86c03934..c6a38d46a2 100644 --- a/contracts/mixnet/schema/raw/response_to_get_stake_saturation.json +++ b/contracts/mixnet/schema/raw/response_to_get_stake_saturation.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "StakeSaturationResponse", + "title": "MixStakeSaturationResponse", "description": "Response containing the current state of the stake saturation of a mixnode with the provided id.", "type": "object", "required": [ diff --git a/contracts/mixnet/schema/raw/response_to_get_state.json b/contracts/mixnet/schema/raw/response_to_get_state.json index 9103e87b72..df4584dde6 100644 --- a/contracts/mixnet/schema/raw/response_to_get_state.json +++ b/contracts/mixnet/schema/raw/response_to_get_state.json @@ -77,8 +77,7 @@ "description": "Contract parameters that could be adjusted in a transaction by the contract admin.", "type": "object", "required": [ - "minimum_gateway_pledge", - "minimum_mixnode_pledge" + "minimum_pledge" ], "properties": { "interval_operating_cost": { @@ -93,15 +92,7 @@ } ] }, - "minimum_gateway_pledge": { - "description": "Minimum amount a gateway must pledge to get into the system.", - "allOf": [ - { - "$ref": "#/definitions/Coin" - } - ] - }, - "minimum_mixnode_delegation": { + "minimum_delegation": { "description": "Minimum amount a delegator must stake in orders for his delegation to get accepted.", "anyOf": [ { @@ -112,8 +103,8 @@ } ] }, - "minimum_mixnode_pledge": { - "description": "Minimum amount a mixnode must pledge to get into the system.", + "minimum_pledge": { + "description": "Minimum amount a node must pledge to get into the system.", "allOf": [ { "$ref": "#/definitions/Coin" diff --git a/contracts/mixnet/schema/raw/response_to_get_state_params.json b/contracts/mixnet/schema/raw/response_to_get_state_params.json index 52d0191167..7ef87647d8 100644 --- a/contracts/mixnet/schema/raw/response_to_get_state_params.json +++ b/contracts/mixnet/schema/raw/response_to_get_state_params.json @@ -4,8 +4,7 @@ "description": "Contract parameters that could be adjusted in a transaction by the contract admin.", "type": "object", "required": [ - "minimum_gateway_pledge", - "minimum_mixnode_pledge" + "minimum_pledge" ], "properties": { "interval_operating_cost": { @@ -20,15 +19,7 @@ } ] }, - "minimum_gateway_pledge": { - "description": "Minimum amount a gateway must pledge to get into the system.", - "allOf": [ - { - "$ref": "#/definitions/Coin" - } - ] - }, - "minimum_mixnode_delegation": { + "minimum_delegation": { "description": "Minimum amount a delegator must stake in orders for his delegation to get accepted.", "anyOf": [ { @@ -39,8 +30,8 @@ } ] }, - "minimum_mixnode_pledge": { - "description": "Minimum amount a mixnode must pledge to get into the system.", + "minimum_pledge": { + "description": "Minimum amount a node must pledge to get into the system.", "allOf": [ { "$ref": "#/definitions/Coin" diff --git a/contracts/mixnet/schema/raw/response_to_get_unbonded_nym_node.json b/contracts/mixnet/schema/raw/response_to_get_unbonded_nym_node.json new file mode 100644 index 0000000000..dde90a69e3 --- /dev/null +++ b/contracts/mixnet/schema/raw/response_to_get_unbonded_nym_node.json @@ -0,0 +1,72 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "UnbondedNodeResponse", + "description": "Response containing basic information of an unbonded nym-node with the provided id.", + "type": "object", + "required": [ + "node_id" + ], + "properties": { + "details": { + "description": "If there existed a nym-node with the provided id, this field contains its basic information.", + "anyOf": [ + { + "$ref": "#/definitions/UnbondedNymNode" + }, + { + "type": "null" + } + ] + }, + "node_id": { + "description": "Id of the requested nym-node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "UnbondedNymNode": { + "description": "Basic information of a node that used to be part of the nym network but has already unbonded.", + "type": "object", + "required": [ + "identity_key", + "node_id", + "owner", + "unbonding_height" + ], + "properties": { + "identity_key": { + "description": "Base58-encoded ed25519 EdDSA public key.", + "type": "string" + }, + "node_id": { + "description": "NodeId assigned to this node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "owner": { + "description": "Address of the owner of this nym node.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "unbonding_height": { + "description": "Block height at which this nym node has unbonded.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } +} diff --git a/contracts/mixnet/schema/raw/response_to_get_unbonded_nym_nodes_by_identity_key_paged.json b/contracts/mixnet/schema/raw/response_to_get_unbonded_nym_nodes_by_identity_key_paged.json new file mode 100644 index 0000000000..6d6b347b21 --- /dev/null +++ b/contracts/mixnet/schema/raw/response_to_get_unbonded_nym_nodes_by_identity_key_paged.json @@ -0,0 +1,71 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PagedUnbondedNymNodesResponse", + "description": "Response containing paged list of all nym-nodes that have ever unbonded.", + "type": "object", + "required": [ + "nodes" + ], + "properties": { + "nodes": { + "description": "Basic information of the node such as the owner or the identity key.", + "type": "array", + "items": { + "$ref": "#/definitions/UnbondedNymNode" + } + }, + "start_next_after": { + "description": "Field indicating paging information for the following queries if the caller wishes to get further entries.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "UnbondedNymNode": { + "description": "Basic information of a node that used to be part of the nym network but has already unbonded.", + "type": "object", + "required": [ + "identity_key", + "node_id", + "owner", + "unbonding_height" + ], + "properties": { + "identity_key": { + "description": "Base58-encoded ed25519 EdDSA public key.", + "type": "string" + }, + "node_id": { + "description": "NodeId assigned to this node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "owner": { + "description": "Address of the owner of this nym node.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "unbonding_height": { + "description": "Block height at which this nym node has unbonded.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } +} diff --git a/contracts/mixnet/schema/raw/response_to_get_unbonded_nym_nodes_by_owner_paged.json b/contracts/mixnet/schema/raw/response_to_get_unbonded_nym_nodes_by_owner_paged.json new file mode 100644 index 0000000000..6d6b347b21 --- /dev/null +++ b/contracts/mixnet/schema/raw/response_to_get_unbonded_nym_nodes_by_owner_paged.json @@ -0,0 +1,71 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PagedUnbondedNymNodesResponse", + "description": "Response containing paged list of all nym-nodes that have ever unbonded.", + "type": "object", + "required": [ + "nodes" + ], + "properties": { + "nodes": { + "description": "Basic information of the node such as the owner or the identity key.", + "type": "array", + "items": { + "$ref": "#/definitions/UnbondedNymNode" + } + }, + "start_next_after": { + "description": "Field indicating paging information for the following queries if the caller wishes to get further entries.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "UnbondedNymNode": { + "description": "Basic information of a node that used to be part of the nym network but has already unbonded.", + "type": "object", + "required": [ + "identity_key", + "node_id", + "owner", + "unbonding_height" + ], + "properties": { + "identity_key": { + "description": "Base58-encoded ed25519 EdDSA public key.", + "type": "string" + }, + "node_id": { + "description": "NodeId assigned to this node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "owner": { + "description": "Address of the owner of this nym node.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "unbonding_height": { + "description": "Block height at which this nym node has unbonded.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } +} diff --git a/contracts/mixnet/schema/raw/response_to_get_unbonded_nym_nodes_paged.json b/contracts/mixnet/schema/raw/response_to_get_unbonded_nym_nodes_paged.json new file mode 100644 index 0000000000..6d6b347b21 --- /dev/null +++ b/contracts/mixnet/schema/raw/response_to_get_unbonded_nym_nodes_paged.json @@ -0,0 +1,71 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PagedUnbondedNymNodesResponse", + "description": "Response containing paged list of all nym-nodes that have ever unbonded.", + "type": "object", + "required": [ + "nodes" + ], + "properties": { + "nodes": { + "description": "Basic information of the node such as the owner or the identity key.", + "type": "array", + "items": { + "$ref": "#/definitions/UnbondedNymNode" + } + }, + "start_next_after": { + "description": "Field indicating paging information for the following queries if the caller wishes to get further entries.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "UnbondedNymNode": { + "description": "Basic information of a node that used to be part of the nym network but has already unbonded.", + "type": "object", + "required": [ + "identity_key", + "node_id", + "owner", + "unbonding_height" + ], + "properties": { + "identity_key": { + "description": "Base58-encoded ed25519 EdDSA public key.", + "type": "string" + }, + "node_id": { + "description": "NodeId assigned to this node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "owner": { + "description": "Address of the owner of this nym node.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "unbonding_height": { + "description": "Block height at which this nym node has unbonded.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } +} diff --git a/contracts/mixnet/src/compat/helpers.rs b/contracts/mixnet/src/compat/helpers.rs new file mode 100644 index 0000000000..0f57650eaf --- /dev/null +++ b/contracts/mixnet/src/compat/helpers.rs @@ -0,0 +1,144 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::mixnet_contract_settings::storage as mixnet_params_storage; +use crate::mixnodes::storage as mixnode_storage; +use crate::nodes::storage as nymnodes_storage; +use crate::support::helpers::ensure_epoch_in_progress_state; +use cosmwasm_std::{Coin, StdResult, Storage}; +use mixnet_contract_common::error::MixnetContractError; +use mixnet_contract_common::helpers::{NodeBond, NodeDetails, PendingChanges}; +use mixnet_contract_common::NodeId; + +pub fn ensure_can_withdraw_rewards(node_details: &D) -> Result<(), MixnetContractError> +where + D: NodeDetails, +{ + // we can only withdraw rewards for a bonded node (i.e. not in the process of unbonding) + // otherwise we know there are no rewards to withdraw + node_details.bond_info().ensure_bonded()?; + + Ok(()) +} + +pub fn ensure_can_modify_cost_params( + storage: &dyn Storage, + node_details: &D, +) -> Result<(), MixnetContractError> +where + D: NodeDetails, +{ + // changing cost params is only allowed if the epoch is currently not in the process of being advanced + ensure_epoch_in_progress_state(storage)?; + + // we can only change cost params for a bonded node (i.e. not in the process of unbonding) + node_details.bond_info().ensure_bonded()?; + + Ok(()) +} + +fn ensure_can_modify_pledge( + storage: &dyn Storage, + node_details: &D, +) -> Result<(), MixnetContractError> +where + D: NodeDetails, +{ + // changing pledge is only allowed if the epoch is currently not in the process of being advanced + ensure_epoch_in_progress_state(storage)?; + + // we can only change pledge for a bonded node (i.e. not in the process of unbonding) + node_details.bond_info().ensure_bonded()?; + + // the node can't have any pending pledge changes + node_details + .pending_changes() + .ensure_no_pending_pledge_changes()?; + + Ok(()) +} + +// remove duplicate code and make sure the same checks are performed everywhere +// (so nothing is accidentally missing) +pub fn ensure_can_increase_pledge( + storage: &dyn Storage, + node_details: &D, +) -> Result<(), MixnetContractError> +where + D: NodeDetails, +{ + ensure_can_modify_pledge(storage, node_details) +} + +// remove duplicate code and make sure the same checks are performed everywhere +// (so nothing is accidentally missing) +pub fn ensure_can_decrease_pledge( + storage: &dyn Storage, + node_details: &D, + decrease_by: &Coin, +) -> Result<(), MixnetContractError> +where + D: NodeDetails, +{ + ensure_can_modify_pledge(storage, node_details)?; + + let minimum_pledge = mixnet_params_storage::minimum_node_pledge(storage)?; + + // check that the denomination is correct + if decrease_by.denom != minimum_pledge.denom { + return Err(MixnetContractError::WrongDenom { + received: decrease_by.denom.clone(), + expected: minimum_pledge.denom, + }); + } + + // also check if the request contains non-zero amount + // (otherwise it's a no-op and we should we waste gas when resolving events?) + if decrease_by.amount.is_zero() { + return Err(MixnetContractError::ZeroCoinAmount); + } + + // decreasing pledge can't result in the new pledge being lower than the minimum amount + let new_pledge_amount = node_details + .bond_info() + .original_pledge() + .amount + .saturating_sub(decrease_by.amount); + if new_pledge_amount < minimum_pledge.amount { + return Err(MixnetContractError::InvalidPledgeReduction { + current: node_details.bond_info().original_pledge().amount, + decrease_by: decrease_by.amount, + minimum: minimum_pledge.amount, + denom: minimum_pledge.denom, + }); + } + + Ok(()) +} + +pub fn get_bond( + storage: &dyn Storage, + node_id: NodeId, +) -> Result, MixnetContractError> { + if let Ok(mix_bond) = mixnode_storage::mixnode_bonds().load(storage, node_id) { + Ok(Box::new(mix_bond)) + } else { + let node_bond = nymnodes_storage::nym_nodes() + .load(storage, node_id) + .map_err(|_| MixnetContractError::NymNodeBondNotFound { node_id })?; + Ok(Box::new(node_bond)) + } +} + +pub fn may_get_bond( + storage: &dyn Storage, + node_id: NodeId, +) -> StdResult>> { + if let Some(mix_bond) = mixnode_storage::mixnode_bonds().may_load(storage, node_id)? { + Ok(Some(Box::new(mix_bond))) + } else if let Some(node_bond) = nymnodes_storage::nym_nodes().may_load(storage, node_id)? { + Ok(Some(Box::new(node_bond))) + } else { + Ok(None) + } +} diff --git a/contracts/mixnet/src/compat/mod.rs b/contracts/mixnet/src/compat/mod.rs new file mode 100644 index 0000000000..94d181c9dc --- /dev/null +++ b/contracts/mixnet/src/compat/mod.rs @@ -0,0 +1,7 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub(crate) mod helpers; +pub(crate) mod queries; +mod storage_traits; +pub(crate) mod transactions; diff --git a/contracts/mixnet/src/compat/queries.rs b/contracts/mixnet/src/compat/queries.rs new file mode 100644 index 0000000000..908c7122bd --- /dev/null +++ b/contracts/mixnet/src/compat/queries.rs @@ -0,0 +1,76 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +// // will return details of either nym-node, legacy mixnode or legacy gateway +// pub fn query_owned_node() { +// todo!() +// } + +pub(crate) mod rewards { + use crate::mixnodes::helpers::{get_mixnode_details_by_id, get_mixnode_details_by_owner}; + use crate::nodes::helpers::{get_node_details_by_id, get_node_details_by_owner}; + use cosmwasm_std::{Addr, Deps, StdResult}; + use mixnet_contract_common::{NodeId, PendingRewardResponse}; + + #[allow(deprecated)] + pub(crate) fn pending_operator_reward( + deps: Deps<'_>, + operator: Addr, + ) -> StdResult { + // check if owns mixnode or nymnode and query accordingly + if let Some(nym_node_details) = get_node_details_by_owner(deps.storage, operator.clone())? { + Ok(PendingRewardResponse { + amount_staked: Some(nym_node_details.original_pledge().clone()), + amount_earned: Some(nym_node_details.pending_operator_reward()), + amount_earned_detailed: Some(nym_node_details.pending_detailed_operator_reward()?), + mixnode_still_fully_bonded: !nym_node_details.is_unbonding(), + node_still_fully_bonded: !nym_node_details.is_unbonding(), + }) + } else if let Some(legacy_mixnode_details) = + get_mixnode_details_by_owner(deps.storage, operator)? + { + Ok(PendingRewardResponse { + amount_staked: Some(legacy_mixnode_details.original_pledge().clone()), + amount_earned: Some(legacy_mixnode_details.pending_operator_reward()), + amount_earned_detailed: Some( + legacy_mixnode_details.pending_detailed_operator_reward()?, + ), + mixnode_still_fully_bonded: !legacy_mixnode_details.is_unbonding(), + node_still_fully_bonded: !legacy_mixnode_details.is_unbonding(), + }) + } else { + Ok(PendingRewardResponse::default()) + } + } + + #[allow(deprecated)] + pub(crate) fn pending_operator_reward_by_id( + deps: Deps<'_>, + node_id: NodeId, + ) -> StdResult { + // check if owns mixnode or nymnode and query accordingly + if let Some(nym_node_details) = get_node_details_by_id(deps.storage, node_id)? { + Ok(PendingRewardResponse { + amount_staked: Some(nym_node_details.original_pledge().clone()), + amount_earned: Some(nym_node_details.pending_operator_reward()), + amount_earned_detailed: Some(nym_node_details.pending_detailed_operator_reward()?), + mixnode_still_fully_bonded: !nym_node_details.is_unbonding(), + node_still_fully_bonded: !nym_node_details.is_unbonding(), + }) + } else if let Some(legacy_mixnode_details) = + get_mixnode_details_by_id(deps.storage, node_id)? + { + Ok(PendingRewardResponse { + amount_staked: Some(legacy_mixnode_details.original_pledge().clone()), + amount_earned: Some(legacy_mixnode_details.pending_operator_reward()), + amount_earned_detailed: Some( + legacy_mixnode_details.pending_detailed_operator_reward()?, + ), + mixnode_still_fully_bonded: !legacy_mixnode_details.is_unbonding(), + node_still_fully_bonded: !legacy_mixnode_details.is_unbonding(), + }) + } else { + Ok(PendingRewardResponse::default()) + } + } +} diff --git a/contracts/mixnet/src/compat/storage_traits.rs b/contracts/mixnet/src/compat/storage_traits.rs new file mode 100644 index 0000000000..5cb369ff51 --- /dev/null +++ b/contracts/mixnet/src/compat/storage_traits.rs @@ -0,0 +1,48 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +// use crate::mixnodes::storage as mixnodes_storage; +// use crate::nodes::storage as nymnodes_storage; +// use crate::rewards::storage as rewards_storage; +// use cosmwasm_std::Storage; +// use cw_storage_plus::Map; +// use mixnet_contract_common::error::MixnetContractError; +// use mixnet_contract_common::mixnode::PendingMixNodeChanges; +// use mixnet_contract_common::{NodeId, PendingNodeChanges}; +// use serde::de::DeserializeOwned; +// use serde::{Deserialize, Serialize}; +// +// // I've created this trait to ensure everything is always stored in the right storage bucket, +// // because I fear I might have accidentally missed something during the transition period +// // of having BOTH mixnodes and nym-nodes +// pub(crate) trait NodeDetailsStorage { +// // +// } +// +// pub(crate) trait NodeBondStorage { +// // +// } +// +// pub(crate) trait PendingChangesStorage: Sized + Serialize + DeserializeOwned { +// const STORAGE_MAP: Map<'static, NodeId, Self>; +// +// fn save(&self, storage: &mut dyn Storage, node_id: NodeId) -> Result<(), MixnetContractError> { +// Ok(Self::STORAGE_MAP.save(storage, node_id, self)?) +// } +// +// fn load(storage: &dyn Storage, node_id: NodeId) -> Result { +// Ok(Self::STORAGE_MAP.load(storage, node_id)?) +// } +// } +// +// pub(crate) trait RewardingStorage { +// // +// } +// +// impl PendingChangesStorage for PendingNodeChanges { +// const STORAGE_MAP: Map<'static, NodeId, Self> = nymnodes_storage::PENDING_NYMNODE_CHANGES; +// } +// +// impl PendingChangesStorage for PendingMixNodeChanges { +// const STORAGE_MAP: Map<'static, NodeId, Self> = mixnodes_storage::PENDING_MIXNODE_CHANGES; +// } diff --git a/contracts/mixnet/src/compat/transactions.rs b/contracts/mixnet/src/compat/transactions.rs new file mode 100644 index 0000000000..dbedab1325 --- /dev/null +++ b/contracts/mixnet/src/compat/transactions.rs @@ -0,0 +1,95 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::mixnodes::helpers::get_mixnode_details_by_owner; +use crate::mixnodes::transactions::{ + try_decrease_mixnode_pledge, try_increase_mixnode_pledge, try_update_mixnode_cost_params, +}; +use crate::nodes::helpers::get_node_details_by_owner; +use crate::nodes::transactions::{ + try_decrease_nym_node_pledge, try_increase_nym_node_pledge, try_update_nym_node_cost_params, +}; +use crate::rewards::transactions::{ + try_withdraw_mixnode_operator_reward, try_withdraw_nym_node_operator_reward, +}; +use crate::support::helpers::{ + ensure_operating_cost_within_range, ensure_profit_margin_within_range, +}; +use cosmwasm_std::{Coin, DepsMut, Env, MessageInfo, Response}; +use mixnet_contract_common::error::MixnetContractError; +use mixnet_contract_common::NodeCostParams; + +pub(crate) fn try_increase_pledge( + deps: DepsMut<'_>, + env: Env, + info: MessageInfo, +) -> Result { + // check if owns mixnode or nymnode and change accordingly + if let Some(nym_node_details) = get_node_details_by_owner(deps.storage, info.sender.clone())? { + try_increase_nym_node_pledge(deps, env, info.funds, nym_node_details) + } else if let Some(legacy_mixnode_details) = + get_mixnode_details_by_owner(deps.storage, info.sender.clone())? + { + try_increase_mixnode_pledge(deps, env, info.funds, legacy_mixnode_details) + } else { + Err(MixnetContractError::NoAssociatedNodeBond { owner: info.sender }) + } +} + +pub fn try_decrease_pledge( + deps: DepsMut<'_>, + env: Env, + info: MessageInfo, + decrease_by: Coin, +) -> Result { + // check if owns mixnode or nymnode and change accordingly + if let Some(nym_node_details) = get_node_details_by_owner(deps.storage, info.sender.clone())? { + try_decrease_nym_node_pledge(deps, env, decrease_by, nym_node_details) + } else if let Some(legacy_mixnode_details) = + get_mixnode_details_by_owner(deps.storage, info.sender.clone())? + { + try_decrease_mixnode_pledge(deps, env, decrease_by, legacy_mixnode_details) + } else { + Err(MixnetContractError::NoAssociatedNodeBond { owner: info.sender }) + } +} + +pub(crate) fn try_update_cost_params( + deps: DepsMut<'_>, + env: Env, + info: MessageInfo, + new_costs: NodeCostParams, +) -> Result { + // ensure the profit margin is within the defined range + ensure_profit_margin_within_range(deps.storage, new_costs.profit_margin_percent)?; + + // ensure the operating cost is within the defined range + ensure_operating_cost_within_range(deps.storage, &new_costs.interval_operating_cost)?; + + // check if owns mixnode or nymnode and change accordingly + if let Some(nym_node_details) = get_node_details_by_owner(deps.storage, info.sender.clone())? { + try_update_nym_node_cost_params(deps, env, new_costs, nym_node_details) + } else if let Some(legacy_mixnode_details) = + get_mixnode_details_by_owner(deps.storage, info.sender.clone())? + { + try_update_mixnode_cost_params(deps, env, new_costs, legacy_mixnode_details) + } else { + Err(MixnetContractError::NoAssociatedNodeBond { owner: info.sender }) + } +} + +pub(crate) fn try_withdraw_operator_reward( + deps: DepsMut<'_>, + info: MessageInfo, +) -> Result { + // check if owns mixnode or nymnode and change accordingly + if let Some(nym_node_details) = get_node_details_by_owner(deps.storage, info.sender.clone())? { + try_withdraw_nym_node_operator_reward(deps, nym_node_details) + } else if let Some(legacy_mixnode_details) = + get_mixnode_details_by_owner(deps.storage, info.sender.clone())? + { + try_withdraw_mixnode_operator_reward(deps, legacy_mixnode_details) + } else { + Err(MixnetContractError::NoAssociatedNodeBond { owner: info.sender }) + } +} diff --git a/contracts/mixnet/src/constants.rs b/contracts/mixnet/src/constants.rs index 0ea867c00c..3d2d165113 100644 --- a/contracts/mixnet/src/constants.rs +++ b/contracts/mixnet/src/constants.rs @@ -1,42 +1,54 @@ // Copyright 2022-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use cosmwasm_std::Uint128; +use cosmwasm_std::{Coin, Uint128}; +use mixnet_contract_common::{ + NodeCostParams, DEFAULT_INTERVAL_OPERATING_COST_AMOUNT, DEFAULT_PROFIT_MARGIN_PERCENT, +}; +use nym_contracts_common::Percent; -/// Constant specifying minimum of coin amount required to bond a gateway -pub const INITIAL_GATEWAY_PLEDGE_AMOUNT: Uint128 = Uint128::new(100_000_000); +/// Constant specifying minimum of coin amount required to bond a node +pub const INITIAL_PLEDGE_AMOUNT: Uint128 = Uint128::new(100_000_000); -/// Constant specifying minimum of coin amount required to bond a mixnode -pub const INITIAL_MIXNODE_PLEDGE_AMOUNT: Uint128 = Uint128::new(100_000_000); +pub fn default_node_costs>(rewarding_denom: S) -> NodeCostParams { + // safety: our hardcoded PM value is a valid percent + #[allow(clippy::unwrap_used)] + NodeCostParams { + profit_margin_percent: Percent::from_percentage_value(DEFAULT_PROFIT_MARGIN_PERCENT) + .unwrap(), + interval_operating_cost: Coin::new(DEFAULT_INTERVAL_OPERATING_COST_AMOUNT, rewarding_denom), + } +} // retrieval limits // TODO: those would need to be empirically verified whether they're not way too small or way too high -pub const GATEWAY_BOND_DEFAULT_RETRIEVAL_LIMIT: u32 = 100; -pub const GATEWAY_BOND_MAX_RETRIEVAL_LIMIT: u32 = 150; +pub const GATEWAY_BOND_DEFAULT_RETRIEVAL_LIMIT: u32 = 50; +pub const GATEWAY_BOND_MAX_RETRIEVAL_LIMIT: u32 = 100; -pub const MIXNODE_BOND_DEFAULT_RETRIEVAL_LIMIT: u32 = 100; -pub const MIXNODE_BOND_MAX_RETRIEVAL_LIMIT: u32 = 150; +pub const MIXNODE_BOND_DEFAULT_RETRIEVAL_LIMIT: u32 = 50; +pub const MIXNODE_BOND_MAX_RETRIEVAL_LIMIT: u32 = 100; -pub const MIXNODE_DETAILS_DEFAULT_RETRIEVAL_LIMIT: u32 = 75; -pub const MIXNODE_DETAILS_MAX_RETRIEVAL_LIMIT: u32 = 100; +pub const NYM_NODE_BOND_DEFAULT_RETRIEVAL_LIMIT: u32 = 50; +pub const NYM_NODE_BOND_MAX_RETRIEVAL_LIMIT: u32 = 100; -pub const UNBONDED_MIXNODES_DEFAULT_RETRIEVAL_LIMIT: u32 = 250; -pub const UNBONDED_MIXNODES_MAX_RETRIEVAL_LIMIT: u32 = 300; +pub const MIXNODE_DETAILS_DEFAULT_RETRIEVAL_LIMIT: u32 = 50; +pub const MIXNODE_DETAILS_MAX_RETRIEVAL_LIMIT: u32 = 75; +pub const NYM_NODE_DETAILS_DEFAULT_RETRIEVAL_LIMIT: u32 = 50; +pub const NYM_NODE_DETAILS_MAX_RETRIEVAL_LIMIT: u32 = 75; -pub const DELEGATION_PAGE_DEFAULT_RETRIEVAL_LIMIT: u32 = 250; -pub const DELEGATION_PAGE_MAX_RETRIEVAL_LIMIT: u32 = 300; +pub const UNBONDED_MIXNODES_DEFAULT_RETRIEVAL_LIMIT: u32 = 100; +pub const UNBONDED_MIXNODES_MAX_RETRIEVAL_LIMIT: u32 = 200; +pub const UNBONDED_NYM_NODES_DEFAULT_RETRIEVAL_LIMIT: u32 = 100; +pub const UNBONDED_NYM_NODES_MAX_RETRIEVAL_LIMIT: u32 = 200; -pub const EPOCH_EVENTS_DEFAULT_RETRIEVAL_LIMIT: u32 = 200; -pub const EPOCH_EVENTS_MAX_RETRIEVAL_LIMIT: u32 = 250; +pub const DELEGATION_PAGE_DEFAULT_RETRIEVAL_LIMIT: u32 = 100; +pub const DELEGATION_PAGE_MAX_RETRIEVAL_LIMIT: u32 = 500; -pub const INTERVAL_EVENTS_DEFAULT_RETRIEVAL_LIMIT: u32 = 200; -pub const INTERVAL_EVENTS_MAX_RETRIEVAL_LIMIT: u32 = 250; +pub const EPOCH_EVENTS_DEFAULT_RETRIEVAL_LIMIT: u32 = 50; +pub const EPOCH_EVENTS_MAX_RETRIEVAL_LIMIT: u32 = 100; -pub const REWARDED_SET_DEFAULT_RETRIEVAL_LIMIT: u32 = 500; -pub const REWARDED_SET_MAX_RETRIEVAL_LIMIT: u32 = 1000; - -pub const FAMILIES_DEFAULT_RETRIEVAL_LIMIT: u32 = 10; -pub const FAMILIES_MAX_RETRIEVAL_LIMIT: u32 = 20; +pub const INTERVAL_EVENTS_DEFAULT_RETRIEVAL_LIMIT: u32 = 50; +pub const INTERVAL_EVENTS_MAX_RETRIEVAL_LIMIT: u32 = 100; // storage keys pub const DELEGATION_PK_NAMESPACE: &str = "dl"; @@ -46,7 +58,6 @@ pub const DELEGATION_MIXNODE_IDX_NAMESPACE: &str = "dlm"; pub const GATEWAYS_PK_NAMESPACE: &str = "gt"; pub const GATEWAYS_OWNER_IDX_NAMESPACE: &str = "gto"; -pub const REWARDED_SET_KEY: &str = "rs"; pub const CURRENT_EPOCH_STATUS_KEY: &str = "ces"; pub const CURRENT_INTERVAL_KEY: &str = "ci"; pub const EPOCH_EVENT_ID_COUNTER_KEY: &str = "eic"; @@ -60,7 +71,10 @@ pub const LAST_INTERVAL_EVENT_ID_KEY: &str = "lie"; pub const ADMIN_STORAGE_KEY: &str = "admin"; pub const CONTRACT_STATE_KEY: &str = "state"; -pub const LAYER_DISTRIBUTION_KEY: &str = "layers"; +pub const NYMNODE_ROLES_ASSIGNMENT_NAMESPACE: &str = "roles"; +pub const NYMNODE_REWARDED_SET_METADATA_NAMESPACE: &str = "roles_metadata"; +pub const NYMNODE_ACTIVE_ROLE_ASSIGNMENT_KEY: &str = "active_roles"; + pub const NODE_ID_COUNTER_KEY: &str = "nic"; pub const PENDING_MIXNODE_CHANGES_NAMESPACE: &str = "pmc"; pub const MIXNODES_PK_NAMESPACE: &str = "mnn"; @@ -68,16 +82,26 @@ pub const MIXNODES_OWNER_IDX_NAMESPACE: &str = "mno"; pub const MIXNODES_IDENTITY_IDX_NAMESPACE: &str = "mni"; pub const MIXNODES_SPHINX_IDX_NAMESPACE: &str = "mns"; +pub const PENDING_NYMNODE_CHANGES_NAMESPACE: &str = "pnc"; +pub const NYMNODE_PK_NAMESPACE: &str = "nn"; +pub const NYMNODE_OWNER_IDX_NAMESPACE: &str = "nno"; +pub const NYMNODE_IDENTITY_IDX_NAMESPACE: &str = "nni"; + pub const UNBONDED_MIXNODES_PK_NAMESPACE: &str = "ubm"; pub const UNBONDED_MIXNODES_OWNER_IDX_NAMESPACE: &str = "umo"; pub const UNBONDED_MIXNODES_IDENTITY_IDX_NAMESPACE: &str = "umi"; +pub const UNBONDED_NYMNODE_PK_NAMESPACE: &str = "ubnn"; +pub const UNBONDED_NYMNODE_OWNER_IDX_NAMESPACE: &str = "ubno"; +pub const UNBONDED_NYMNODE_IDENTITY_IDX_NAMESPACE: &str = "ubni"; + +pub const CUMULATIVE_EPOCH_WORK_KEY: &str = "cumulative_epoch_work"; pub const REWARDING_PARAMS_KEY: &str = "rparams"; pub const PENDING_REWARD_POOL_KEY: &str = "prp"; pub const MIXNODES_REWARDING_PK_NAMESPACE: &str = "mnr"; - -pub const FAMILIES_INDEX_NAMESPACE: &str = "faml2"; -pub const FAMILIES_MAP_NAMESPACE: &str = "fam2"; -pub const MEMBERS_MAP_NAMESPACE: &str = "memb2"; +pub const NYMNODE_REWARDING_PK_NAMESPACE: &str = MIXNODES_REWARDING_PK_NAMESPACE; pub const SIGNING_NONCES_NAMESPACE: &str = "sn"; + +// temporary storage keys created for the transition period: +pub const LEGACY_GATEWAY_ID_NAMESPACE: &str = "lgidr"; diff --git a/contracts/mixnet/src/contract.rs b/contracts/mixnet/src/contract.rs index cda8264afa..36ffe22623 100644 --- a/contracts/mixnet/src/contract.rs +++ b/contracts/mixnet/src/contract.rs @@ -1,20 +1,23 @@ // Copyright 2021-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::constants::{INITIAL_GATEWAY_PLEDGE_AMOUNT, INITIAL_MIXNODE_PLEDGE_AMOUNT}; +use crate::constants::INITIAL_PLEDGE_AMOUNT; use crate::interval::storage as interval_storage; use crate::mixnet_contract_settings::storage as mixnet_params_storage; -use crate::mixnodes::storage as mixnode_storage; -use crate::rewards::storage as rewards_storage; +use crate::nodes::storage as nymnodes_storage; +use crate::queued_migrations::migrate_to_nym_nodes_usage; +use crate::rewards::storage::RewardingStorage; +use crate::support::tests::legacy; use cosmwasm_std::{ - entry_point, to_binary, Addr, Coin, Deps, DepsMut, Env, MessageInfo, QueryResponse, Response, + coin, entry_point, to_binary, Addr, Coin, Deps, DepsMut, Env, MessageInfo, QueryResponse, + Response, }; use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::{ ContractState, ContractStateParams, ExecuteMsg, InstantiateMsg, Interval, MigrateMsg, - OperatingCostRange, ProfitMarginRange, QueryMsg, + NodeCostParams, OperatingCostRange, ProfitMarginRange, QueryMsg, }; -use nym_contracts_common::set_build_information; +use nym_contracts_common::{set_build_information, Percent}; // version info for migration info const CONTRACT_NAME: &str = "crate:nym-mixnet-contract"; @@ -36,14 +39,10 @@ fn default_initial_state( vesting_contract_address, rewarding_denom: rewarding_denom.clone(), params: ContractStateParams { - minimum_mixnode_delegation: None, - minimum_mixnode_pledge: Coin { + minimum_delegation: None, + minimum_pledge: Coin { denom: rewarding_denom.clone(), - amount: INITIAL_MIXNODE_PLEDGE_AMOUNT, - }, - minimum_gateway_pledge: Coin { - denom: rewarding_denom, - amount: INITIAL_GATEWAY_PLEDGE_AMOUNT, + amount: INITIAL_PLEDGE_AMOUNT, }, profit_margin, interval_operating_cost, @@ -93,8 +92,8 @@ pub fn instantiate( rewarding_validator_address, )?; mixnet_params_storage::initialise_storage(deps.branch(), state, info.sender)?; - mixnode_storage::initialise_storage(deps.storage)?; - rewards_storage::initialise_storage(deps.storage, reward_params)?; + RewardingStorage::new().initialise(deps.storage, reward_params)?; + nymnodes_storage::initialise_storage(deps.storage)?; cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; set_build_information!(deps.storage)?; @@ -110,29 +109,12 @@ pub fn execute( msg: ExecuteMsg, ) -> Result { match msg { + // state/sys-params-related ExecuteMsg::UpdateAdmin { admin } => { crate::mixnet_contract_settings::transactions::try_update_contract_admin( deps, info, admin, ) } - ExecuteMsg::AssignNodeLayer { mix_id, layer } => { - crate::mixnodes::transactions::assign_mixnode_layer(deps, info, mix_id, layer) - } - // families - ExecuteMsg::CreateFamily { label } => { - crate::families::transactions::try_create_family(deps, info, label) - } - ExecuteMsg::JoinFamily { - join_permit, - family_head, - } => crate::families::transactions::try_join_family(deps, info, join_permit, family_head), - ExecuteMsg::LeaveFamily { family_head } => { - crate::families::transactions::try_leave_family(deps, info, family_head) - } - ExecuteMsg::KickFamilyMember { member } => { - crate::families::transactions::try_head_kick_member(deps, info, member) - } - // state/sys-params-related ExecuteMsg::UpdateRewardingValidatorAddress { address } => { crate::mixnet_contract_settings::transactions::try_update_rewarding_validator_address( deps, info, address, @@ -145,14 +127,14 @@ pub fn execute( updated_parameters, ) } - ExecuteMsg::UpdateActiveSetSize { - active_set_size, + ExecuteMsg::UpdateActiveSetDistribution { + update, force_immediately, - } => crate::rewards::transactions::try_update_active_set_size( + } => crate::rewards::transactions::try_update_active_set_distribution( deps, env, info, - active_set_size, + update, force_immediately, ), ExecuteMsg::UpdateRewardingParams { @@ -180,17 +162,9 @@ pub fn execute( ExecuteMsg::BeginEpochTransition {} => { crate::interval::transactions::try_begin_epoch_transition(deps, env, info) } - ExecuteMsg::AdvanceCurrentEpoch { - new_rewarded_set, - // families_in_layer, - expected_active_set_size, - } => crate::interval::transactions::try_advance_epoch( - deps, - env, - info, - new_rewarded_set, - expected_active_set_size, - ), + ExecuteMsg::AssignRoles { assignment } => { + crate::interval::transactions::try_assign_roles(deps, env, info, assignment) + } ExecuteMsg::ReconcileEpochEvents { limit } => { crate::interval::transactions::try_reconcile_epoch_events(deps, env, info, limit) } @@ -208,23 +182,15 @@ pub fn execute( cost_params, owner_signature, ), - ExecuteMsg::PledgeMore {} => { - crate::mixnodes::transactions::try_increase_pledge(deps, env, info) - } - ExecuteMsg::DecreasePledge { decrease_by } => { - crate::mixnodes::transactions::try_decrease_pledge(deps, env, info, decrease_by) - } ExecuteMsg::UnbondMixnode {} => { crate::mixnodes::transactions::try_remove_mixnode(deps, env, info) } - ExecuteMsg::UpdateMixnodeCostParams { new_costs } => { - crate::mixnodes::transactions::try_update_mixnode_cost_params( - deps, env, info, new_costs, - ) - } ExecuteMsg::UpdateMixnodeConfig { new_config } => { crate::mixnodes::transactions::try_update_mixnode_config(deps, info, new_config) } + ExecuteMsg::MigrateMixnode {} => { + crate::mixnodes::transactions::try_migrate_to_nymnode(deps, info) + } // gateway-related: ExecuteMsg::BondGateway { @@ -243,27 +209,60 @@ pub fn execute( ExecuteMsg::UpdateGatewayConfig { new_config } => { crate::gateways::transactions::try_update_gateway_config(deps, info, new_config) } + ExecuteMsg::MigrateGateway { cost_params } => { + crate::gateways::transactions::try_migrate_to_nymnode(deps, info, cost_params) + } + + // nym-node related: + ExecuteMsg::BondNymNode { + node, + cost_params, + owner_signature, + } => crate::nodes::transactions::try_add_nym_node( + deps, + env, + info, + node, + cost_params, + owner_signature, + ), + ExecuteMsg::UnbondNymNode {} => { + crate::nodes::transactions::try_remove_nym_node(deps, env, info) + } + ExecuteMsg::UpdateNodeConfig { update } => { + crate::nodes::transactions::try_update_node_config(deps, info, update) + } + + // nym-node/mixnode-related: + ExecuteMsg::PledgeMore {} => { + crate::compat::transactions::try_increase_pledge(deps, env, info) + } + ExecuteMsg::DecreasePledge { decrease_by } => { + crate::compat::transactions::try_decrease_pledge(deps, env, info, decrease_by) + } + ExecuteMsg::UpdateCostParams { new_costs } => { + crate::compat::transactions::try_update_cost_params(deps, env, info, new_costs) + } // delegation-related: - ExecuteMsg::DelegateToMixnode { mix_id } => { - crate::delegations::transactions::try_delegate_to_mixnode(deps, env, info, mix_id) + ExecuteMsg::Delegate { node_id } => { + crate::delegations::transactions::try_delegate_to_node(deps, env, info, node_id) } - ExecuteMsg::UndelegateFromMixnode { mix_id } => { - crate::delegations::transactions::try_remove_delegation_from_mixnode( - deps, env, info, mix_id, + ExecuteMsg::Undelegate { node_id } => { + crate::delegations::transactions::try_remove_delegation_from_node( + deps, env, info, node_id, ) } // reward-related - ExecuteMsg::RewardMixnode { - mix_id, - performance, - } => crate::rewards::transactions::try_reward_mixnode(deps, env, info, mix_id, performance), + ExecuteMsg::RewardNode { node_id, params } => { + crate::rewards::transactions::try_reward_node(deps, env, info, node_id, params) + } ExecuteMsg::WithdrawOperatorReward {} => { - crate::rewards::transactions::try_withdraw_operator_reward(deps, info) + crate::compat::transactions::try_withdraw_operator_reward(deps, info) } - ExecuteMsg::WithdrawDelegatorReward { mix_id } => { + ExecuteMsg::WithdrawDelegatorReward { node_id: mix_id } => { crate::rewards::transactions::try_withdraw_delegator_reward(deps, info, mix_id) } @@ -276,11 +275,7 @@ pub fn execute( } // legacy vesting - ExecuteMsg::CreateFamilyOnBehalf { .. } - | ExecuteMsg::JoinFamilyOnBehalf { .. } - | ExecuteMsg::LeaveFamilyOnBehalf { .. } - | ExecuteMsg::KickFamilyMemberOnBehalf { .. } - | ExecuteMsg::BondMixnodeOnBehalf { .. } + ExecuteMsg::BondMixnodeOnBehalf { .. } | ExecuteMsg::PledgeMoreOnBehalf { .. } | ExecuteMsg::DecreasePledgeOnBehalf { .. } | ExecuteMsg::UnbondMixnodeOnBehalf { .. } @@ -301,6 +296,24 @@ pub fn execute( ExecuteMsg::TestingResolveAllPendingEvents { limit } => { crate::testing::transactions::try_resolve_all_pending_events(deps, env, limit) } + ExecuteMsg::TestingUncheckedBondLegacyMixnode { node } => { + legacy::save_new_mixnode( + deps.storage, + env, + node, + NodeCostParams { + profit_margin_percent: Percent::from_percentage_value(20).unwrap(), + interval_operating_cost: coin(40_000_000, "unym"), + }, + info.sender, + info.funds[0].clone(), + )?; + Ok(Response::default()) + } + ExecuteMsg::TestingUncheckedBondLegacyGateway { node } => { + legacy::save_new_gateway(deps.storage, env, node, info.sender, info.funds[0].clone())?; + Ok(Response::default()) + } } } @@ -311,24 +324,6 @@ pub fn query( msg: QueryMsg, ) -> Result { let query_res = match msg { - QueryMsg::GetAllFamiliesPaged { limit, start_after } => to_binary( - &crate::families::queries::get_all_families_paged(deps.storage, start_after, limit)?, - ), - QueryMsg::GetAllMembersPaged { limit, start_after } => to_binary( - &crate::families::queries::get_all_members_paged(deps.storage, start_after, limit)?, - ), - QueryMsg::GetFamilyByHead { head } => to_binary( - &crate::families::queries::get_family_by_head(&head, deps.storage)?, - ), - QueryMsg::GetFamilyByLabel { label } => to_binary( - &crate::families::queries::get_family_by_label(label, deps.storage)?, - ), - QueryMsg::GetFamilyMembersByHead { head } => to_binary( - &crate::families::queries::get_family_members_by_head(&head, deps.storage)?, - ), - QueryMsg::GetFamilyMembersByLabel { label } => to_binary( - &crate::families::queries::get_family_members_by_label(label, deps.storage)?, - ), QueryMsg::GetContractVersion {} => { to_binary(&crate::mixnet_contract_settings::queries::query_contract_version()) } @@ -354,9 +349,6 @@ pub fn query( QueryMsg::GetCurrentIntervalDetails {} => to_binary( &crate::interval::queries::query_current_interval_details(deps, env)?, ), - QueryMsg::GetRewardedSet { limit, start_after } => to_binary( - &crate::interval::queries::query_rewarded_set_paged(deps, start_after, limit)?, - ), // mixnode-related: QueryMsg::GetMixNodeBonds { start_after, limit } => to_binary( @@ -410,9 +402,6 @@ pub fn query( QueryMsg::GetBondedMixnodeDetailsByIdentity { mix_identity } => to_binary( &crate::mixnodes::queries::query_mixnode_details_by_identity(deps, mix_identity)?, ), - QueryMsg::GetLayerDistribution {} => { - to_binary(&crate::mixnodes::queries::query_layer_distribution(deps)?) - } // gateway-related: QueryMsg::GetGateways { limit, start_after } => to_binary( @@ -424,20 +413,80 @@ pub fn query( QueryMsg::GetOwnedGateway { address } => to_binary( &crate::gateways::queries::query_owned_gateway(deps, address)?, ), + QueryMsg::GetPreassignedGatewayIds { limit, start_after } => to_binary( + &crate::gateways::queries::query_preassigned_ids_paged(deps, start_after, limit)?, + ), - // delegation-related: - QueryMsg::GetMixnodeDelegations { - mix_id, - start_after, + // nym-node-related: + QueryMsg::GetNymNodeBondsPaged { start_after, limit } => to_binary( + &crate::nodes::queries::query_nymnode_bonds_paged(deps, start_after, limit)?, + ), + QueryMsg::GetNymNodesDetailedPaged { limit, start_after } => to_binary( + &crate::nodes::queries::query_nymnodes_details_paged(deps, start_after, limit)?, + ), + QueryMsg::GetUnbondedNymNode { node_id } => to_binary( + &crate::nodes::queries::query_unbonded_nymnode(deps, node_id)?, + ), + QueryMsg::GetUnbondedNymNodesPaged { limit, start_after } => to_binary( + &crate::nodes::queries::query_unbonded_nymnodes_paged(deps, limit, start_after)?, + ), + QueryMsg::GetUnbondedNymNodesByOwnerPaged { + owner, limit, + start_after, } => to_binary( - &crate::delegations::queries::query_mixnode_delegations_paged( + &crate::nodes::queries::query_unbonded_nymnodes_by_owner_paged( deps, - mix_id, - start_after, + owner, limit, + start_after, )?, ), + QueryMsg::GetUnbondedNymNodesByIdentityKeyPaged { + identity_key, + limit, + start_after, + } => to_binary( + &crate::nodes::queries::query_unbonded_nymnodes_by_identity_paged( + deps, + identity_key, + limit, + start_after, + )?, + ), + QueryMsg::GetOwnedNymNode { address } => { + to_binary(&crate::nodes::queries::query_owned_nymnode(deps, address)?) + } + QueryMsg::GetNymNodeDetails { node_id } => to_binary( + &crate::nodes::queries::query_nymnode_details(deps, node_id)?, + ), + QueryMsg::GetNymNodeDetailsByIdentityKey { node_identity } => to_binary( + &crate::nodes::queries::query_nymnode_details_by_identity(deps, node_identity)?, + ), + QueryMsg::GetNodeRewardingDetails { node_id } => to_binary( + &crate::nodes::queries::query_nymnode_rewarding_details(deps, node_id)?, + ), + QueryMsg::GetNodeStakeSaturation { node_id } => to_binary( + &crate::nodes::queries::query_stake_saturation(deps, node_id)?, + ), + QueryMsg::GetRoleAssignment { role } => { + to_binary(&crate::nodes::queries::query_epoch_assignment(deps, role)?) + } + QueryMsg::GetRewardedSetMetadata {} => { + to_binary(&crate::nodes::queries::query_rewarded_set_metadata(deps)?) + } + + // delegation-related: + QueryMsg::GetNodeDelegations { + node_id, + start_after, + limit, + } => to_binary(&crate::delegations::queries::query_node_delegations_paged( + deps, + node_id, + start_after, + limit, + )?), QueryMsg::GetDelegatorDelegations { delegator, start_after, @@ -451,11 +500,11 @@ pub fn query( )?, ), QueryMsg::GetDelegationDetails { - mix_id, + node_id, delegator, proxy, - } => to_binary(&crate::delegations::queries::query_mixnode_delegation( - deps, mix_id, delegator, proxy, + } => to_binary(&crate::delegations::queries::query_node_delegation( + deps, node_id, delegator, proxy, )?), QueryMsg::GetAllDelegations { start_after, limit } => to_binary( &crate::delegations::queries::query_all_delegations_paged(deps, start_after, limit)?, @@ -465,38 +514,40 @@ pub fn query( QueryMsg::GetPendingOperatorReward { address } => to_binary( &crate::rewards::queries::query_pending_operator_reward(deps, address)?, ), - QueryMsg::GetPendingMixNodeOperatorReward { mix_id } => to_binary( - &crate::rewards::queries::query_pending_mixnode_operator_reward(deps, mix_id)?, + QueryMsg::GetPendingNodeOperatorReward { node_id } => to_binary( + &crate::rewards::queries::query_pending_mixnode_operator_reward(deps, node_id)?, ), QueryMsg::GetPendingDelegatorReward { address, - mix_id, + node_id, proxy, } => to_binary(&crate::rewards::queries::query_pending_delegator_reward( - deps, address, mix_id, proxy, + deps, address, node_id, proxy, )?), QueryMsg::GetEstimatedCurrentEpochOperatorReward { - mix_id, + node_id, estimated_performance, + estimated_work, } => to_binary( &crate::rewards::queries::query_estimated_current_epoch_operator_reward( deps, - mix_id, + node_id, estimated_performance, + estimated_work, )?, ), QueryMsg::GetEstimatedCurrentEpochDelegatorReward { address, - mix_id, - proxy, + node_id, estimated_performance, + estimated_work, } => to_binary( &crate::rewards::queries::query_estimated_current_epoch_delegator_reward( deps, address, - mix_id, - proxy, + node_id, estimated_performance, + estimated_work, )?, ), @@ -538,13 +589,19 @@ pub fn query( #[entry_point] pub fn migrate( - deps: DepsMut<'_>, + mut deps: DepsMut<'_>, _env: Env, msg: MigrateMsg, ) -> Result { set_build_information!(deps.storage)?; cw2::ensure_from_older_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + // remove all family-related things + crate::queued_migrations::families_purge(deps.branch())?; + + // prepare the ground for using nym-nodes rather than standalone mixnodes/gateways + migrate_to_nym_nodes_usage(deps.branch(), &msg)?; + // due to circular dependency on contract addresses (i.e. mixnet contract requiring vesting contract address // and vesting contract requiring the mixnet contract address), if we ever want to deploy any new fresh // environment, one of the contracts will HAVE TO go through a migration @@ -561,9 +618,12 @@ pub fn migrate( #[cfg(test)] mod tests { use super::*; + use crate::rewards::storage as rewards_storage; use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; use cosmwasm_std::{Decimal, Uint128}; - use mixnet_contract_common::reward_params::{IntervalRewardParams, RewardingParams}; + use mixnet_contract_common::reward_params::{ + IntervalRewardParams, RewardedSetParams, RewardingParams, + }; use mixnet_contract_common::{InitialRewardingParams, Percent}; use std::time::Duration; @@ -585,8 +645,12 @@ mod tests { sybil_resistance: Percent::from_percentage_value(23).unwrap(), active_set_work_factor: Decimal::from_atomics(10u32, 0).unwrap(), interval_pool_emission: Percent::from_percentage_value(1).unwrap(), - rewarded_set_size: 543, - active_set_size: 123, + rewarded_set_params: RewardedSetParams { + entry_gateways: 123, + exit_gateways: 70, + mixnodes: 120, + standby: 0, + }, }, profit_margin: ProfitMarginRange { minimum: "0.05".parse().unwrap(), @@ -609,14 +673,10 @@ mod tests { vesting_contract_address: Addr::unchecked("bar456"), rewarding_denom: "uatom".into(), params: ContractStateParams { - minimum_mixnode_delegation: None, - minimum_mixnode_pledge: Coin { + minimum_delegation: None, + minimum_pledge: Coin { denom: "uatom".into(), - amount: INITIAL_MIXNODE_PLEDGE_AMOUNT, - }, - minimum_gateway_pledge: Coin { - denom: "uatom".into(), - amount: INITIAL_GATEWAY_PLEDGE_AMOUNT, + amount: INITIAL_PLEDGE_AMOUNT, }, profit_margin: ProfitMarginRange { minimum: Percent::from_percentage_value(5).unwrap(), @@ -631,7 +691,7 @@ mod tests { let expected_epoch_reward_budget = Decimal::from_ratio(100_000_000_000_000u128, 1234u32) * Decimal::percent(1); - let expected_stake_saturation_point = Decimal::from_ratio(123_456_000_000_000u128, 543u32); + let expected_stake_saturation_point = Decimal::from_ratio(123_456_000_000_000u128, 313u32); let expected_rewarding_params = RewardingParams { interval: IntervalRewardParams { @@ -644,8 +704,12 @@ mod tests { active_set_work_factor: Decimal::from_atomics(10u32, 0).unwrap(), interval_pool_emission: Percent::from_percentage_value(1).unwrap(), }, - rewarded_set_size: 543, - active_set_size: 123, + rewarded_set: RewardedSetParams { + entry_gateways: 123, + exit_gateways: 70, + mixnodes: 120, + standby: 0, + }, }; let state = mixnet_params_storage::CONTRACT_STATE diff --git a/contracts/mixnet/src/delegations/helpers.rs b/contracts/mixnet/src/delegations/helpers.rs index 3991711a06..e82496f1a9 100644 --- a/contracts/mixnet/src/delegations/helpers.rs +++ b/contracts/mixnet/src/delegations/helpers.rs @@ -5,17 +5,17 @@ use crate::delegations::storage; use crate::rewards::storage as rewards_storage; use cosmwasm_std::{Coin, Storage}; use mixnet_contract_common::error::MixnetContractError; -use mixnet_contract_common::mixnode::MixNodeRewarding; +use mixnet_contract_common::mixnode::NodeRewarding; use mixnet_contract_common::Delegation; pub(crate) fn undelegate( store: &mut dyn Storage, delegation: Delegation, - mut mix_rewarding: MixNodeRewarding, + mut mix_rewarding: NodeRewarding, ) -> Result { let tokens = mix_rewarding.undelegate(&delegation)?; - rewards_storage::MIXNODE_REWARDING.save(store, delegation.mix_id, &mix_rewarding)?; + rewards_storage::MIXNODE_REWARDING.save(store, delegation.node_id, &mix_rewarding)?; storage::delegations().replace(store, delegation.storage_key(), None, Some(&delegation))?; Ok(tokens) @@ -32,16 +32,19 @@ mod tests { fn undelegation_updates_mix_rewarding_storage_and_deletes_delegation() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", Some(Uint128::new(100_000_000_000))); + let mix_id = + test.add_rewarded_set_nymnode_id("mix-owner", Some(Uint128::new(100_000_000_000))); let delegator = "delegator"; let og_amount = Uint128::new(200_000_000); test.add_immediate_delegation(delegator, og_amount, mix_id); test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![mix_id]); - let dist1 = test.reward_with_distribution_with_state_bypass(mix_id, performance(100.0)); + let dist1 = + test.legacy_reward_with_distribution_with_state_bypass(mix_id, performance(100.0)); test.skip_to_next_epoch_end(); - let dist2 = test.reward_with_distribution_with_state_bypass(mix_id, performance(100.0)); + let dist2 = + test.legacy_reward_with_distribution_with_state_bypass(mix_id, performance(100.0)); let mix_rewarding = test.mix_rewarding(mix_id); let delegation = test.delegation(mix_id, delegator, &None); diff --git a/contracts/mixnet/src/delegations/queries.rs b/contracts/mixnet/src/delegations/queries.rs index 01e7b65648..fb46e8056f 100644 --- a/contracts/mixnet/src/delegations/queries.rs +++ b/contracts/mixnet/src/delegations/queries.rs @@ -2,38 +2,40 @@ // SPDX-License-Identifier: Apache-2.0 use super::storage; +use crate::compat; use crate::constants::{ DELEGATION_PAGE_DEFAULT_RETRIEVAL_LIMIT, DELEGATION_PAGE_MAX_RETRIEVAL_LIMIT, }; -use crate::mixnodes::storage as mixnodes_storage; use cosmwasm_std::Deps; use cosmwasm_std::Order; use cosmwasm_std::StdResult; use cw_storage_plus::Bound; -use mixnet_contract_common::delegation::{MixNodeDelegationResponse, OwnerProxySubKey}; +use mixnet_contract_common::delegation::{NodeDelegationResponse, OwnerProxySubKey}; use mixnet_contract_common::{ - delegation, Delegation, MixId, PagedAllDelegationsResponse, PagedDelegatorDelegationsResponse, - PagedMixNodeDelegationsResponse, + delegation, Delegation, NodeId, PagedAllDelegationsResponse, PagedDelegatorDelegationsResponse, + PagedNodeDelegationsResponse, }; -pub(crate) fn query_mixnode_delegations_paged( +pub(crate) fn query_node_delegations_paged( deps: Deps<'_>, - mix_id: MixId, + node_id: NodeId, start_after: Option, limit: Option, -) -> StdResult { +) -> StdResult { let limit = limit .unwrap_or(DELEGATION_PAGE_DEFAULT_RETRIEVAL_LIMIT) .min(DELEGATION_PAGE_MAX_RETRIEVAL_LIMIT) as usize; let start = start_after.map(|subkey| { - Bound::exclusive(Delegation::generate_storage_key_with_subkey(mix_id, subkey)) + Bound::exclusive(Delegation::generate_storage_key_with_subkey( + node_id, subkey, + )) }); let delegations = storage::delegations() .idx .mixnode - .prefix(mix_id) + .prefix(node_id) .range(deps.storage, start, None, Order::Ascending) .take(limit) .map(|record| record.map(|r| r.1)) @@ -41,7 +43,7 @@ pub(crate) fn query_mixnode_delegations_paged( let start_next_after = delegations.last().map(|del| del.proxy_storage_key()); - Ok(PagedMixNodeDelegationsResponse::new( + Ok(PagedNodeDelegationsResponse::new( delegations, start_next_after, )) @@ -50,7 +52,7 @@ pub(crate) fn query_mixnode_delegations_paged( pub(crate) fn query_delegator_delegations_paged( deps: Deps<'_>, delegation_owner: String, - start_after: Option<(MixId, OwnerProxySubKey)>, + start_after: Option<(NodeId, OwnerProxySubKey)>, limit: Option, ) -> StdResult { let validated_owner = deps.api.addr_validate(&delegation_owner)?; @@ -74,7 +76,7 @@ pub(crate) fn query_delegator_delegations_paged( let start_next_after = delegations .last() - .map(|del| (del.mix_id, del.proxy_storage_key())); + .map(|del| (del.node_id, del.proxy_storage_key())); Ok(PagedDelegatorDelegationsResponse::new( delegations, @@ -83,30 +85,26 @@ pub(crate) fn query_delegator_delegations_paged( } // queries for delegation value of given address for particular node -pub(crate) fn query_mixnode_delegation( +pub(crate) fn query_node_delegation( deps: Deps<'_>, - mix_id: MixId, + node_id: NodeId, delegation_owner: String, proxy: Option, -) -> StdResult { +) -> StdResult { let validated_owner = deps.api.addr_validate(&delegation_owner)?; let validated_proxy = proxy .map(|proxy| deps.api.addr_validate(&proxy)) .transpose()?; let storage_key = - Delegation::generate_storage_key(mix_id, &validated_owner, validated_proxy.as_ref()); + Delegation::generate_storage_key(node_id, &validated_owner, validated_proxy.as_ref()); let delegation = storage::delegations().may_load(deps.storage, storage_key)?; - let mixnode_still_bonded = mixnodes_storage::mixnode_bonds() - .may_load(deps.storage, mix_id)? - .map(|bond| !bond.is_unbonding) + let node_still_bonded = compat::helpers::may_get_bond(deps.storage, node_id)? + .map(|bond| !bond.is_unbonding()) .unwrap_or_default(); - Ok(MixNodeDelegationResponse::new( - delegation, - mixnode_still_bonded, - )) + Ok(NodeDelegationResponse::new(delegation, node_still_bonded)) } pub(crate) fn query_all_delegations_paged( @@ -141,7 +139,7 @@ mod tests { fn add_dummy_mixes_with_delegations(test: &mut TestSetup, delegators: usize, mixes: usize) { for i in 0..mixes { - let mix_id = test.add_dummy_mixnode(&format!("mix-owner{}", i), None); + let mix_id = test.add_legacy_mixnode(&format!("mix-owner{}", i), None); for delegator in 0..delegators { let name = &format!("delegator{}", delegator); test.add_immediate_delegation(name, 100_000_000u32, mix_id) @@ -157,7 +155,7 @@ mod tests { #[test] fn obeys_limits() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_legacy_mixnode("mix-owner", None); let env = test.env(); test_helpers::add_dummy_delegations(test.deps_mut(), env, mix_id, 200); @@ -165,20 +163,20 @@ mod tests { let limit = 2; let page1 = - query_mixnode_delegations_paged(test.deps(), mix_id, None, Some(limit)).unwrap(); + query_node_delegations_paged(test.deps(), mix_id, None, Some(limit)).unwrap(); assert_eq!(limit, page1.delegations.len() as u32); } #[test] fn has_default_limit() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_legacy_mixnode("mix-owner", None); let env = test.env(); test_helpers::add_dummy_delegations(test.deps_mut(), env, mix_id, 500); // query without explicitly setting a limit - let page1 = query_mixnode_delegations_paged(test.deps(), mix_id, None, None).unwrap(); + let page1 = query_node_delegations_paged(test.deps(), mix_id, None, None).unwrap(); assert_eq!( DELEGATION_PAGE_DEFAULT_RETRIEVAL_LIMIT, @@ -189,7 +187,7 @@ mod tests { #[test] fn has_max_limit() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_legacy_mixnode("mix-owner", None); let env = test.env(); test_helpers::add_dummy_delegations(test.deps_mut(), env, mix_id, 5000); @@ -197,8 +195,7 @@ mod tests { // query with a crazily high limit in an attempt to use too many resources let crazy_limit = 10000; let page1 = - query_mixnode_delegations_paged(test.deps(), mix_id, None, Some(crazy_limit)) - .unwrap(); + query_node_delegations_paged(test.deps(), mix_id, None, Some(crazy_limit)).unwrap(); assert_eq!( DELEGATION_PAGE_MAX_RETRIEVAL_LIMIT, @@ -210,12 +207,12 @@ mod tests { fn pagination_works() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_legacy_mixnode("mix-owner", None); test.add_immediate_delegation("addr1", 1000u32, mix_id); let per_page = 2; let page1 = - query_mixnode_delegations_paged(test.deps(), mix_id, None, Some(per_page)).unwrap(); + query_node_delegations_paged(test.deps(), mix_id, None, Some(per_page)).unwrap(); // page should have 1 result on it assert_eq!(1, page1.delegations.len()); @@ -225,20 +222,20 @@ mod tests { // page1 should have 2 results on it let page1 = - query_mixnode_delegations_paged(test.deps(), mix_id, None, Some(per_page)).unwrap(); + query_node_delegations_paged(test.deps(), mix_id, None, Some(per_page)).unwrap(); assert_eq!(2, page1.delegations.len()); test.add_immediate_delegation("addr3", 1000u32, mix_id); // page1 still has the same 2 results let another_page1 = - query_mixnode_delegations_paged(test.deps(), mix_id, None, Some(per_page)).unwrap(); + query_node_delegations_paged(test.deps(), mix_id, None, Some(per_page)).unwrap(); assert_eq!(2, another_page1.delegations.len()); assert_eq!(page1, another_page1); // retrieving the next page should start after the last key on this page let start_after = page1.start_next_after.unwrap(); - let page2 = query_mixnode_delegations_paged( + let page2 = query_node_delegations_paged( test.deps(), mix_id, Some(start_after.clone()), @@ -251,7 +248,7 @@ mod tests { // save another one test.add_immediate_delegation("addr4", 1000u32, mix_id); - let page2 = query_mixnode_delegations_paged( + let page2 = query_node_delegations_paged( test.deps(), mix_id, Some(start_after), @@ -266,10 +263,10 @@ mod tests { #[test] fn all_retrieved_delegations_are_towards_specified_mixnode() { let mut test = TestSetup::new(); - let mix_id1 = test.add_dummy_mixnode("mix-owner1", None); - let mix_id2 = test.add_dummy_mixnode("mix-owner2", None); - let mix_id3 = test.add_dummy_mixnode("mix-owner3", None); - let mix_id4 = test.add_dummy_mixnode("mix-owner4", None); + let mix_id1 = test.add_legacy_mixnode("mix-owner1", None); + let mix_id2 = test.add_legacy_mixnode("mix-owner2", None); + let mix_id3 = test.add_legacy_mixnode("mix-owner3", None); + let mix_id4 = test.add_legacy_mixnode("mix-owner4", None); let env = test.env(); // add other "out of order" delegations manually @@ -282,21 +279,21 @@ mod tests { test_helpers::add_dummy_delegations(test.deps_mut(), env, mix_id4, 10); test.add_immediate_delegation("random-delegator4", 1000u32, mix_id2); - let res1 = query_mixnode_delegations_paged(test.deps(), mix_id1, None, None).unwrap(); + let res1 = query_node_delegations_paged(test.deps(), mix_id1, None, None).unwrap(); assert_eq!(res1.delegations.len(), 10); - assert!(res1.delegations.into_iter().all(|d| d.mix_id == mix_id1)); + assert!(res1.delegations.into_iter().all(|d| d.node_id == mix_id1)); - let res2 = query_mixnode_delegations_paged(test.deps(), mix_id2, None, None).unwrap(); + let res2 = query_node_delegations_paged(test.deps(), mix_id2, None, None).unwrap(); assert_eq!(res2.delegations.len(), 14); - assert!(res2.delegations.into_iter().all(|d| d.mix_id == mix_id2)); + assert!(res2.delegations.into_iter().all(|d| d.node_id == mix_id2)); - let res3 = query_mixnode_delegations_paged(test.deps(), mix_id3, None, None).unwrap(); + let res3 = query_node_delegations_paged(test.deps(), mix_id3, None, None).unwrap(); assert_eq!(res3.delegations.len(), 10); - assert!(res3.delegations.into_iter().all(|d| d.mix_id == mix_id3)); + assert!(res3.delegations.into_iter().all(|d| d.node_id == mix_id3)); - let res4 = query_mixnode_delegations_paged(test.deps(), mix_id4, None, None).unwrap(); + let res4 = query_node_delegations_paged(test.deps(), mix_id4, None, None).unwrap(); assert_eq!(res4.delegations.len(), 10); - assert!(res4.delegations.into_iter().all(|d| d.mix_id == mix_id4)); + assert!(res4.delegations.into_iter().all(|d| d.node_id == mix_id4)); } } @@ -365,11 +362,11 @@ mod tests { let mut test = TestSetup::new(); // note that mix_ids are monotonically increasing - let mix_id1 = test.add_dummy_mixnode("mix-owner1", None); - let mix_id2 = test.add_dummy_mixnode("mix-owner2", None); - let mix_id3 = test.add_dummy_mixnode("mix-owner3", None); - let mix_id4 = test.add_dummy_mixnode("mix-owner4", None); - let mix_id5 = test.add_dummy_mixnode("mix-owner5", None); + let mix_id1 = test.add_legacy_mixnode("mix-owner1", None); + let mix_id2 = test.add_legacy_mixnode("mix-owner2", None); + let mix_id3 = test.add_legacy_mixnode("mix-owner3", None); + let mix_id4 = test.add_legacy_mixnode("mix-owner4", None); + let mix_id5 = test.add_legacy_mixnode("mix-owner5", None); // add few delegations from unrelated delegators for mix_id in [mix_id1, mix_id2, mix_id3, mix_id4, mix_id5] { @@ -526,8 +523,8 @@ mod tests { // note that mix_ids are monotonically increasing and are the first chunk of all // delegation storage keys, - let mix_id1 = test.add_dummy_mixnode("mix-owner1", None); - let mix_id2 = test.add_dummy_mixnode("mix-owner2", None); + let mix_id1 = test.add_legacy_mixnode("mix-owner1", None); + let mix_id2 = test.add_legacy_mixnode("mix-owner2", None); let delegator1 = "delegator1"; let delegator2 = "delegator2"; @@ -541,7 +538,7 @@ mod tests { assert_eq!(1, page1.delegations.len()); assert!( page1.delegations[0].owner.as_str() == delegator1 - && page1.delegations[0].mix_id == mix_id1 + && page1.delegations[0].node_id == mix_id1 ); test.add_immediate_delegation(delegator1, 1000u32, mix_id2); @@ -552,11 +549,11 @@ mod tests { assert_eq!(2, page1.delegations.len()); assert!( page1.delegations[0].owner.as_str() == delegator1 - && page1.delegations[0].mix_id == mix_id1 + && page1.delegations[0].node_id == mix_id1 ); assert!( page1.delegations[1].owner.as_str() == delegator1 - && page1.delegations[1].mix_id == mix_id2 + && page1.delegations[1].node_id == mix_id2 ); test.add_immediate_delegation(delegator2, 1000u32, mix_id1); @@ -567,11 +564,11 @@ mod tests { assert_eq!(2, another_page1.delegations.len()); assert!( another_page1.delegations[0].owner.as_str() == delegator1 - && another_page1.delegations[0].mix_id == mix_id1 + && another_page1.delegations[0].node_id == mix_id1 ); assert!( another_page1.delegations[1].owner.as_str() == delegator2 - && another_page1.delegations[1].mix_id == mix_id1 + && another_page1.delegations[1].node_id == mix_id1 ); // retrieving the next page should start after the last key on this page @@ -583,7 +580,7 @@ mod tests { assert_eq!(1, page2.delegations.len()); assert!( page2.delegations[0].owner.as_str() == delegator1 - && page2.delegations[0].mix_id == mix_id2 + && page2.delegations[0].node_id == mix_id2 ); // save another one @@ -596,72 +593,145 @@ mod tests { assert_eq!(2, page2.delegations.len()); assert!( page2.delegations[0].owner.as_str() == delegator1 - && page2.delegations[0].mix_id == mix_id2 + && page2.delegations[0].node_id == mix_id2 ); assert!( page2.delegations[1].owner.as_str() == delegator2 - && page2.delegations[1].mix_id == mix_id2 + && page2.delegations[1].node_id == mix_id2 ); } } #[cfg(test)] - mod querying_for_specific_mixnode_delegation { + mod querying_for_specific_node_delegation { use super::*; - #[test] - fn when_delegation_doesnt_exist() { - let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); - let owner = "owner"; + #[cfg(test)] + mod legacy_mixnodes { + use super::*; - let res = query_mixnode_delegation(test.deps(), mix_id, owner.into(), None).unwrap(); - assert!(res.delegation.is_none()); - assert!(res.mixnode_still_bonded); + #[allow(deprecated)] + #[test] + fn when_delegation_doesnt_exist() { + let mut test = TestSetup::new(); + let mix_id = test.add_legacy_mixnode("mix-owner", None); + let owner = "owner"; + + let res = query_node_delegation(test.deps(), mix_id, owner.into(), None).unwrap(); + assert!(res.delegation.is_none()); + assert!(res.mixnode_still_bonded); + assert!(res.node_still_bonded); + } + + #[allow(deprecated)] + #[test] + fn when_delegation_exists_but_mixnode_has_unbonded() { + let mut test = TestSetup::new(); + let mix_id = test.add_legacy_mixnode("mix-owner", None); + let owner = "owner"; + + test.add_immediate_delegation(owner, 1000u32, mix_id); + test.immediately_unbond_mixnode(mix_id); + + let res = query_node_delegation(test.deps(), mix_id, owner.into(), None).unwrap(); + assert_eq!(res.delegation.as_ref().unwrap().owner.as_str(), owner); + assert_eq!(res.delegation.as_ref().unwrap().amount.amount.u128(), 1000); + assert!(!res.mixnode_still_bonded); + assert!(!res.node_still_bonded); + } + + #[allow(deprecated)] + #[test] + fn when_delegation_exists_but_mixnode_is_unbonding() { + let mut test = TestSetup::new(); + let mix_id = test.add_legacy_mixnode("mix-owner", None); + let owner = "owner"; + + test.add_immediate_delegation(owner, 1000u32, mix_id); + test.start_unbonding_mixnode(mix_id); + + let res = query_node_delegation(test.deps(), mix_id, owner.into(), None).unwrap(); + assert_eq!(res.delegation.as_ref().unwrap().owner.as_str(), owner); + assert_eq!(res.delegation.as_ref().unwrap().amount.amount.u128(), 1000); + assert!(!res.mixnode_still_bonded); + assert!(!res.node_still_bonded); + } + + #[allow(deprecated)] + #[test] + fn when_delegation_exists_with_fully_bonded_node() { + let mut test = TestSetup::new(); + let mix_id = test.add_legacy_mixnode("mix-owner", None); + let owner = "owner"; + + test.add_immediate_delegation(owner, 1000u32, mix_id); + + let res = query_node_delegation(test.deps(), mix_id, owner.into(), None).unwrap(); + assert_eq!(res.delegation.as_ref().unwrap().owner.as_str(), owner); + assert_eq!(res.delegation.as_ref().unwrap().amount.amount.u128(), 1000); + assert!(res.mixnode_still_bonded); + assert!(res.node_still_bonded); + } } - #[test] - fn when_delegation_exists_but_mixnode_has_unbonded() { - let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); - let owner = "owner"; + #[cfg(test)] + mod nym_nodes { + use super::*; - test.add_immediate_delegation(owner, 1000u32, mix_id); - test.immediately_unbond_mixnode(mix_id); + #[test] + fn when_delegation_doesnt_exist() { + let mut test = TestSetup::new(); + let node_id = test.add_dummy_nymnode("bond-owner", None); + let owner = "owner"; - let res = query_mixnode_delegation(test.deps(), mix_id, owner.into(), None).unwrap(); - assert_eq!(res.delegation.as_ref().unwrap().owner.as_str(), owner); - assert_eq!(res.delegation.as_ref().unwrap().amount.amount.u128(), 1000); - assert!(!res.mixnode_still_bonded); - } + let res = query_node_delegation(test.deps(), node_id, owner.into(), None).unwrap(); + assert!(res.delegation.is_none()); + assert!(res.node_still_bonded); + } - #[test] - fn when_delegation_exists_but_mixnode_is_unbonding() { - let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); - let owner = "owner"; + #[test] + fn when_delegation_exists_but_mixnode_has_unbonded() { + let mut test = TestSetup::new(); + let node_id = test.add_dummy_nymnode("bond-owner", None); + let owner = "owner"; - test.add_immediate_delegation(owner, 1000u32, mix_id); - test.start_unbonding_mixnode(mix_id); + test.add_immediate_delegation(owner, 1000u32, node_id); + test.immediately_unbond_nymnode(node_id); - let res = query_mixnode_delegation(test.deps(), mix_id, owner.into(), None).unwrap(); - assert_eq!(res.delegation.as_ref().unwrap().owner.as_str(), owner); - assert_eq!(res.delegation.as_ref().unwrap().amount.amount.u128(), 1000); - assert!(!res.mixnode_still_bonded); - } + let res = query_node_delegation(test.deps(), node_id, owner.into(), None).unwrap(); + assert_eq!(res.delegation.as_ref().unwrap().owner.as_str(), owner); + assert_eq!(res.delegation.as_ref().unwrap().amount.amount.u128(), 1000); + assert!(!res.node_still_bonded); + } - #[test] - fn when_delegation_exists_with_fully_bonded_node() { - let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); - let owner = "owner"; + #[test] + fn when_delegation_exists_but_mixnode_is_unbonding() { + let mut test = TestSetup::new(); + let node_id = test.add_dummy_nymnode("bond-owner", None); + let owner = "owner"; - test.add_immediate_delegation(owner, 1000u32, mix_id); + test.add_immediate_delegation(owner, 1000u32, node_id); + test.start_unbonding_nymnode(node_id); - let res = query_mixnode_delegation(test.deps(), mix_id, owner.into(), None).unwrap(); - assert_eq!(res.delegation.as_ref().unwrap().owner.as_str(), owner); - assert_eq!(res.delegation.as_ref().unwrap().amount.amount.u128(), 1000); - assert!(res.mixnode_still_bonded); + let res = query_node_delegation(test.deps(), node_id, owner.into(), None).unwrap(); + assert_eq!(res.delegation.as_ref().unwrap().owner.as_str(), owner); + assert_eq!(res.delegation.as_ref().unwrap().amount.amount.u128(), 1000); + assert!(!res.node_still_bonded); + } + + #[test] + fn when_delegation_exists_with_fully_bonded_node() { + let mut test = TestSetup::new(); + let node_id = test.add_dummy_nymnode("bond-owner", None); + let owner = "owner"; + + test.add_immediate_delegation(owner, 1000u32, node_id); + + let res = query_node_delegation(test.deps(), node_id, owner.into(), None).unwrap(); + assert_eq!(res.delegation.as_ref().unwrap().owner.as_str(), owner); + assert_eq!(res.delegation.as_ref().unwrap().amount.amount.u128(), 1000); + assert!(res.node_still_bonded); + } } } } diff --git a/contracts/mixnet/src/delegations/storage.rs b/contracts/mixnet/src/delegations/storage.rs index 5d3e6b45f2..3fa2931f64 100644 --- a/contracts/mixnet/src/delegations/storage.rs +++ b/contracts/mixnet/src/delegations/storage.rs @@ -6,15 +6,15 @@ use crate::constants::{ }; use cw_storage_plus::{Index, IndexList, IndexedMap, MultiIndex}; use mixnet_contract_common::delegation::OwnerProxySubKey; -use mixnet_contract_common::{Addr, Delegation, MixId}; +use mixnet_contract_common::{Addr, Delegation, NodeId}; // It's a composite key on node's id and delegator address -type PrimaryKey = (MixId, OwnerProxySubKey); +type PrimaryKey = (NodeId, OwnerProxySubKey); pub(crate) struct DelegationIndex<'a> { pub(crate) owner: MultiIndex<'a, Addr, Delegation, PrimaryKey>, - pub(crate) mixnode: MultiIndex<'a, MixId, Delegation, PrimaryKey>, + pub(crate) mixnode: MultiIndex<'a, NodeId, Delegation, PrimaryKey>, } impl<'a> IndexList for DelegationIndex<'a> { @@ -32,7 +32,7 @@ pub(crate) fn delegations<'a>() -> IndexedMap<'a, PrimaryKey, Delegation, Delega DELEGATION_OWNER_IDX_NAMESPACE, ), mixnode: MultiIndex::new( - |_pk, d| d.mix_id, + |_pk, d| d.node_id, DELEGATION_PK_NAMESPACE, DELEGATION_MIXNODE_IDX_NAMESPACE, ), diff --git a/contracts/mixnet/src/delegations/transactions.rs b/contracts/mixnet/src/delegations/transactions.rs index b91c6c8582..714afe33f0 100644 --- a/contracts/mixnet/src/delegations/transactions.rs +++ b/contracts/mixnet/src/delegations/transactions.rs @@ -4,21 +4,22 @@ use super::storage; use crate::interval::storage as interval_storage; use crate::mixnet_contract_settings::storage as mixnet_params_storage; -use crate::mixnodes::storage as mixnodes_storage; -use crate::support::helpers::{ensure_epoch_in_progress_state, validate_delegation_stake}; +use crate::support::helpers::{ + ensure_any_node_bonded, ensure_epoch_in_progress_state, validate_delegation_stake, +}; use cosmwasm_std::{DepsMut, Env, MessageInfo, Response}; use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::events::{ new_pending_delegation_event, new_pending_undelegation_event, }; use mixnet_contract_common::pending_events::PendingEpochEventKind; -use mixnet_contract_common::{Delegation, MixId}; +use mixnet_contract_common::{Delegation, NodeId}; -pub(crate) fn try_delegate_to_mixnode( +pub(crate) fn try_delegate_to_node( deps: DepsMut<'_>, env: Env, info: MessageInfo, - mix_id: MixId, + mix_id: NodeId, ) -> Result { // delegation is only allowed if the epoch is currently not in the process of being advanced ensure_epoch_in_progress_state(deps.storage)?; @@ -27,18 +28,12 @@ pub(crate) fn try_delegate_to_mixnode( let contract_state = mixnet_params_storage::CONTRACT_STATE.load(deps.storage)?; let delegation = validate_delegation_stake( info.funds, - contract_state.params.minimum_mixnode_delegation, + contract_state.params.minimum_delegation, contract_state.rewarding_denom, )?; // check if the target node actually exists and is still bonded - match mixnodes_storage::mixnode_bonds().may_load(deps.storage, mix_id)? { - None => return Err(MixnetContractError::MixNodeBondNotFound { mix_id }), - Some(bond) if bond.is_unbonding => { - return Err(MixnetContractError::MixnodeIsUnbonding { mix_id }) - } - _ => (), - } + ensure_any_node_bonded(deps.storage, mix_id)?; // push the event onto the queue and wait for it to be picked up at the end of the epoch let cosmos_event = new_pending_delegation_event(&info.sender, &delegation, mix_id); @@ -49,33 +44,33 @@ pub(crate) fn try_delegate_to_mixnode( Ok(Response::new().add_event(cosmos_event)) } -pub(crate) fn try_remove_delegation_from_mixnode( +pub(crate) fn try_remove_delegation_from_node( deps: DepsMut<'_>, env: Env, info: MessageInfo, - mix_id: MixId, + node_id: NodeId, ) -> Result { // undelegation is only allowed if the epoch is currently not in the process of being advanced ensure_epoch_in_progress_state(deps.storage)?; // see if the delegation even exists - let storage_key = Delegation::generate_storage_key(mix_id, &info.sender, None); + let storage_key = Delegation::generate_storage_key(node_id, &info.sender, None); if storage::delegations() .may_load(deps.storage, storage_key)? .is_none() { - return Err(MixnetContractError::NoMixnodeDelegationFound { - mix_id, + return Err(MixnetContractError::NodeDelegationNotFound { + node_id, address: info.sender.into_string(), proxy: None, }); } // push the event onto the queue and wait for it to be picked up at the end of the epoch - let cosmos_event = new_pending_undelegation_event(&info.sender, mix_id); + let cosmos_event = new_pending_undelegation_event(&info.sender, node_id); - let epoch_event = PendingEpochEventKind::new_undelegate(info.sender, mix_id); + let epoch_event = PendingEpochEventKind::new_undelegate(info.sender, node_id); interval_storage::push_new_epoch_event(deps.storage, &env, epoch_event)?; Ok(Response::new().add_event(cosmos_event)) @@ -94,6 +89,7 @@ mod tests { use crate::support::tests::test_helpers::TestSetup; use cosmwasm_std::testing::mock_info; use cosmwasm_std::{coin, Addr, Decimal}; + use mixnet_contract_common::nym_node::Role; use mixnet_contract_common::{EpochState, EpochStatus}; #[test] @@ -104,7 +100,9 @@ mod tests { final_node_id: 0, }, EpochState::ReconcilingEvents, - EpochState::AdvancingEpoch, + EpochState::RoleAssignment { + next: Role::first(), + }, ]; for bad_state in bad_states { @@ -118,10 +116,10 @@ mod tests { let env = test.env(); let owner = "delegator"; - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_legacy_mixnode("mix-owner", None); let sender = mock_info(owner, &[coin(50_000_000, TEST_COIN_DENOM)]); - let res = try_delegate_to_mixnode(test.deps_mut(), env.clone(), sender, mix_id); + let res = try_delegate_to_node(test.deps_mut(), env.clone(), sender, mix_id); assert!(matches!( res, Err(MixnetContractError::EpochAdvancementInProgress { .. }) @@ -136,10 +134,10 @@ mod tests { let owner = "delegator"; let sender = mock_info(owner, &[coin(100_000_000, TEST_COIN_DENOM)]); - let res = try_delegate_to_mixnode(test.deps_mut(), env, sender, 42); + let res = try_delegate_to_node(test.deps_mut(), env, sender, 42); assert_eq!( res, - Err(MixnetContractError::MixNodeBondNotFound { mix_id: 42 }) + Err(MixnetContractError::NymNodeBondNotFound { node_id: 42 }) ) } @@ -149,16 +147,16 @@ mod tests { let env = test.env(); let owner = "delegator"; - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_legacy_mixnode("mix-owner", None); let sender1 = mock_info(owner, &[coin(0, TEST_COIN_DENOM)]); let sender2 = mock_info(owner, &[]); let sender3 = mock_info(owner, &[coin(1000, "some-weird-coin")]); - let res = try_delegate_to_mixnode(test.deps_mut(), env.clone(), sender1, mix_id); + let res = try_delegate_to_node(test.deps_mut(), env.clone(), sender1, mix_id); assert_eq!(res, Err(MixnetContractError::EmptyDelegation)); - let res = try_delegate_to_mixnode(test.deps_mut(), env.clone(), sender2, mix_id); + let res = try_delegate_to_node(test.deps_mut(), env.clone(), sender2, mix_id); assert_eq!(res, Err(MixnetContractError::EmptyDelegation)); - let res = try_delegate_to_mixnode(test.deps_mut(), env, sender3, mix_id); + let res = try_delegate_to_node(test.deps_mut(), env, sender3, mix_id); assert_eq!( res, Err(MixnetContractError::WrongDenom { @@ -174,7 +172,7 @@ mod tests { let env = test.env(); let owner = "delegator"; - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_legacy_mixnode("mix-owner", None); let sender1 = mock_info(owner, &[coin(100_000_000, TEST_COIN_DENOM)]); let sender2 = mock_info(owner, &[coin(150_000_000, TEST_COIN_DENOM)]); @@ -182,12 +180,12 @@ mod tests { let mut contract_state = mixnet_params_storage::CONTRACT_STATE .load(test.deps().storage) .unwrap(); - contract_state.params.minimum_mixnode_delegation = Some(min_delegation.clone()); + contract_state.params.minimum_delegation = Some(min_delegation.clone()); mixnet_params_storage::CONTRACT_STATE .save(test.deps_mut().storage, &contract_state) .unwrap(); - let res = try_delegate_to_mixnode(test.deps_mut(), env.clone(), sender1, mix_id); + let res = try_delegate_to_node(test.deps_mut(), env.clone(), sender1, mix_id); assert_eq!( res, Err(MixnetContractError::InsufficientDelegation { @@ -196,7 +194,7 @@ mod tests { }) ); - let res = try_delegate_to_mixnode(test.deps_mut(), env, sender2, mix_id); + let res = try_delegate_to_node(test.deps_mut(), env, sender2, mix_id); assert!(res.is_ok()) } @@ -207,10 +205,10 @@ mod tests { let owner = "delegator"; let sender = mock_info(owner, &[coin(100_000_000, TEST_COIN_DENOM)]); - let mix_id_unbonding = test.add_dummy_mixnode("mix-owner-unbonding", None); - let mix_id_unbonded = test.add_dummy_mixnode("mix-owner-unbonded", None); + let mix_id_unbonding = test.add_legacy_mixnode("mix-owner-unbonding", None); + let mix_id_unbonded = test.add_legacy_mixnode("mix-owner-unbonded", None); let mix_id_unbonded_leftover = - test.add_dummy_mixnode("mix-owner-unbonded-leftover", None); + test.add_legacy_mixnode("mix-owner-unbonded-leftover", None); // manually adjust delegation info as to indicate the rewarding information shouldnt get removed let mut rewarding_details = rewards_storage::MIXNODE_REWARDING @@ -247,7 +245,7 @@ mod tests { ) .unwrap(); - let res = try_delegate_to_mixnode( + let res = try_delegate_to_node( test.deps_mut(), env.clone(), sender.clone(), @@ -260,7 +258,7 @@ mod tests { }) ); - let res = try_delegate_to_mixnode( + let res = try_delegate_to_node( test.deps_mut(), env.clone(), sender.clone(), @@ -268,17 +266,16 @@ mod tests { ); assert_eq!( res, - Err(MixnetContractError::MixNodeBondNotFound { - mix_id: mix_id_unbonded + Err(MixnetContractError::NymNodeBondNotFound { + node_id: mix_id_unbonded }) ); - let res = - try_delegate_to_mixnode(test.deps_mut(), env, sender, mix_id_unbonded_leftover); + let res = try_delegate_to_node(test.deps_mut(), env, sender, mix_id_unbonded_leftover); assert_eq!( res, - Err(MixnetContractError::MixNodeBondNotFound { - mix_id: mix_id_unbonded_leftover + Err(MixnetContractError::NymNodeBondNotFound { + node_id: mix_id_unbonded_leftover }) ); } @@ -289,15 +286,15 @@ mod tests { let env = test.env(); let owner = "delegator"; - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_legacy_mixnode("mix-owner", None); let sender1 = mock_info(owner, &[coin(100_000_000, TEST_COIN_DENOM)]); let sender2 = mock_info(owner, &[coin(50_000_000, TEST_COIN_DENOM)]); - let res = try_delegate_to_mixnode(test.deps_mut(), env.clone(), sender1, mix_id); + let res = try_delegate_to_node(test.deps_mut(), env.clone(), sender1, mix_id); assert!(res.is_ok()); // still OK - let res = try_delegate_to_mixnode(test.deps_mut(), env, sender2, mix_id); + let res = try_delegate_to_node(test.deps_mut(), env, sender2, mix_id); assert!(res.is_ok()) } @@ -307,13 +304,13 @@ mod tests { let env = test.env(); let owner = "delegator"; - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_legacy_mixnode("mix-owner", None); let amount1 = coin(100_000_000, TEST_COIN_DENOM); let sender1 = mock_info(owner, &[amount1.clone()]); - try_delegate_to_mixnode(test.deps_mut(), env.clone(), sender1, mix_id).unwrap(); + try_delegate_to_node(test.deps_mut(), env.clone(), sender1, mix_id).unwrap(); let events = test.pending_epoch_events(); @@ -332,6 +329,7 @@ mod tests { use crate::support::tests::test_helpers::TestSetup; use cosmwasm_std::coin; use cosmwasm_std::testing::mock_info; + use mixnet_contract_common::nym_node::Role; use mixnet_contract_common::{EpochState, EpochStatus}; #[test] @@ -342,12 +340,14 @@ mod tests { final_node_id: 0, }, EpochState::ReconcilingEvents, - EpochState::AdvancingEpoch, + EpochState::RoleAssignment { + next: Role::first(), + }, ]; for bad_state in bad_states { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("owner", None); + let mix_id = test.add_legacy_mixnode("owner", None); test.add_immediate_delegation("foomp", 1000u32, mix_id); let mut status = EpochStatus::new(test.rewarding_validator().sender); @@ -356,7 +356,7 @@ mod tests { .unwrap(); let env = test.env(); - let res = try_remove_delegation_from_mixnode( + let res = try_remove_delegation_from_node( test.deps_mut(), env.clone(), mock_info("sender", &[]), @@ -375,13 +375,13 @@ mod tests { let env = test.env(); let owner = "delegator"; let sender = mock_info(owner, &[]); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let node_id = test.add_legacy_mixnode("mix-owner", None); - let res = try_remove_delegation_from_mixnode(test.deps_mut(), env, sender, mix_id); + let res = try_remove_delegation_from_node(test.deps_mut(), env, sender, node_id); assert_eq!( res, - Err(MixnetContractError::NoMixnodeDelegationFound { - mix_id, + Err(MixnetContractError::NodeDelegationNotFound { + node_id, address: owner.to_string(), proxy: None }) @@ -394,17 +394,17 @@ mod tests { let env = test.env(); let owner = "delegator"; - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let node_id = test.add_legacy_mixnode("mix-owner", None); let sender1 = mock_info(owner, &[coin(100_000_000, TEST_COIN_DENOM)]); let sender2 = mock_info(owner, &[]); - try_delegate_to_mixnode(test.deps_mut(), env.clone(), sender1, mix_id).unwrap(); + try_delegate_to_node(test.deps_mut(), env.clone(), sender1, node_id).unwrap(); - let res = try_remove_delegation_from_mixnode(test.deps_mut(), env, sender2, mix_id); + let res = try_remove_delegation_from_node(test.deps_mut(), env, sender2, node_id); assert_eq!( res, - Err(MixnetContractError::NoMixnodeDelegationFound { - mix_id, + Err(MixnetContractError::NodeDelegationNotFound { + node_id, address: owner.to_string(), proxy: None }) @@ -419,10 +419,10 @@ mod tests { let owner = "delegator"; let sender = mock_info(owner, &[]); - let normal_mix_id = test.add_dummy_mixnode("mix-owner", None); - let mix_id_unbonding = test.add_dummy_mixnode("mix-owner-unbonding", None); + let normal_mix_id = test.add_legacy_mixnode("mix-owner", None); + let mix_id_unbonding = test.add_legacy_mixnode("mix-owner-unbonding", None); let mix_id_unbonded_leftover = - test.add_dummy_mixnode("mix-owner-unbonded-leftover", None); + test.add_legacy_mixnode("mix-owner-unbonded-leftover", None); test.add_immediate_delegation(owner, 10000u32, normal_mix_id); test.add_immediate_delegation(owner, 10000u32, mix_id_unbonding); @@ -443,7 +443,7 @@ mod tests { ) .unwrap(); - let res = try_remove_delegation_from_mixnode( + let res = try_remove_delegation_from_node( test.deps_mut(), env.clone(), sender.clone(), @@ -451,7 +451,7 @@ mod tests { ); assert!(res.is_ok()); - let res = try_remove_delegation_from_mixnode( + let res = try_remove_delegation_from_node( test.deps_mut(), env.clone(), sender.clone(), @@ -459,7 +459,7 @@ mod tests { ); assert!(res.is_ok()); - let res = try_remove_delegation_from_mixnode( + let res = try_remove_delegation_from_node( test.deps_mut(), env, sender, diff --git a/contracts/mixnet/src/families/mod.rs b/contracts/mixnet/src/families/mod.rs deleted file mode 100644 index f3c80cce37..0000000000 --- a/contracts/mixnet/src/families/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright 2022-2023 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -pub mod queries; -pub mod signature_helpers; -pub mod storage; -pub mod transactions; diff --git a/contracts/mixnet/src/families/queries.rs b/contracts/mixnet/src/families/queries.rs deleted file mode 100644 index 96c7f79e23..0000000000 --- a/contracts/mixnet/src/families/queries.rs +++ /dev/null @@ -1,116 +0,0 @@ -// Copyright 2022-2023 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use super::storage::{families, get_members, must_get_family, MEMBERS}; -use crate::constants::{FAMILIES_DEFAULT_RETRIEVAL_LIMIT, FAMILIES_MAX_RETRIEVAL_LIMIT}; -use crate::families::storage::must_get_family_by_label; -use cosmwasm_std::{Order, Storage}; -use cw_storage_plus::Bound; -use mixnet_contract_common::families::{ - Family, FamilyByHeadResponse, FamilyByLabelResponse, FamilyHead, FamilyMembersByHeadResponse, - PagedFamiliesResponse, PagedMembersResponse, -}; -use mixnet_contract_common::{error::MixnetContractError, IdentityKeyRef}; -use mixnet_contract_common::{FamilyMembersByLabelResponse, IdentityKey}; - -pub fn get_family_by_label( - label: String, - storage: &dyn Storage, -) -> Result { - let family = families() - .idx - .label - .item(storage, label.clone())? - .map(|o| o.1); - Ok(FamilyByLabelResponse { label, family }) -} - -pub fn get_family_by_head( - head: IdentityKeyRef<'_>, - storage: &dyn Storage, -) -> Result { - let family = families().may_load(storage, head.to_string())?; - Ok(FamilyByHeadResponse { - head: FamilyHead::new(head), - family, - }) -} - -// TODO: this should be returning a paged response! -pub fn get_family_members_by_head( - head: IdentityKeyRef<'_>, - storage: &dyn Storage, -) -> Result { - let family_head = FamilyHead::new(head); - let family = must_get_family(&family_head, storage)?; - let members = get_members(&family, storage)?; - - Ok(FamilyMembersByHeadResponse { - head: family.head().to_owned(), - members, - }) -} - -// TODO: this should be returning a paged response! -pub fn get_family_members_by_label( - label: String, - storage: &dyn Storage, -) -> Result { - let family = must_get_family_by_label(label.clone(), storage)?; - let members = get_members(&family, storage)?; - - Ok(FamilyMembersByLabelResponse { label, members }) -} - -pub fn get_all_families_paged( - storage: &dyn Storage, - start_after: Option, - limit: Option, -) -> Result { - let limit = limit - .unwrap_or(FAMILIES_DEFAULT_RETRIEVAL_LIMIT) - .min(FAMILIES_MAX_RETRIEVAL_LIMIT) as usize; - - let start = start_after.map(Bound::exclusive); - - let response = families() - .range(storage, start, None, Order::Ascending) - .take(limit) - .filter_map(|r| r.ok()) - .map(|(_key, family)| family) - .collect::>(); - - let start_next_after = response - .last() - .map(|response| response.head_identity().to_string()); - - Ok(PagedFamiliesResponse { - families: response, - start_next_after, - }) -} - -pub fn get_all_members_paged( - storage: &dyn Storage, - start_after: Option, - limit: Option, -) -> Result { - let limit = limit - .unwrap_or(FAMILIES_DEFAULT_RETRIEVAL_LIMIT) - .min(FAMILIES_MAX_RETRIEVAL_LIMIT) as usize; - - let start = start_after.map(Bound::exclusive); - - let response = MEMBERS - .range(storage, start, None, Order::Ascending) - .take(limit) - .filter_map(|r| r.ok()) - .collect::>(); - - let start_next_after = response.last().map(|r| r.0.clone()); - - Ok(PagedMembersResponse { - members: response, - start_next_after, - }) -} diff --git a/contracts/mixnet/src/families/signature_helpers.rs b/contracts/mixnet/src/families/signature_helpers.rs deleted file mode 100644 index 3c6bb79626..0000000000 --- a/contracts/mixnet/src/families/signature_helpers.rs +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::mixnodes::storage as mixnodes_storage; -use crate::signing::storage as signing_storage; -use crate::support::helpers::decode_ed25519_identity_key; -use cosmwasm_std::Deps; -use mixnet_contract_common::error::MixnetContractError; -use mixnet_contract_common::families::FamilyHead; -use mixnet_contract_common::{construct_family_join_permit, IdentityKeyRef}; -use nym_contracts_common::signing::{MessageSignature, Verifier}; - -pub(crate) fn verify_family_join_permit( - deps: Deps<'_>, - granter: FamilyHead, - member: IdentityKeyRef, - signature: MessageSignature, -) -> Result<(), MixnetContractError> { - // recover the public key - let public_key = decode_ed25519_identity_key(granter.identity())?; - - // that's kinda a backwards way of getting the granter's nonce, but it works, so ¯\_(ツ)_/¯ - let Some(head_mixnode) = mixnodes_storage::mixnode_bonds() - .idx - .identity_key - .item(deps.storage, granter.identity().to_owned())? - .map(|record| record.1) - else { - return Err(MixnetContractError::FamilyDoesNotExist { - head: granter.identity().to_string(), - }); - }; - let nonce = signing_storage::get_signing_nonce(deps.storage, head_mixnode.owner)?; - let msg = construct_family_join_permit(nonce, granter, member.to_owned()); - - if deps.api.verify_message(msg, signature, &public_key)? { - Ok(()) - } else { - Err(MixnetContractError::InvalidEd25519Signature) - } -} diff --git a/contracts/mixnet/src/families/storage.rs b/contracts/mixnet/src/families/storage.rs deleted file mode 100644 index 05b9566b7d..0000000000 --- a/contracts/mixnet/src/families/storage.rs +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright 2022-2023 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use cosmwasm_std::{Order, Storage}; -use cw_storage_plus::{Index, IndexList, IndexedMap, Map, UniqueIndex}; -use mixnet_contract_common::families::{Family, FamilyHead}; -use mixnet_contract_common::{error::MixnetContractError, IdentityKey, IdentityKeyRef}; - -use crate::constants::{FAMILIES_INDEX_NAMESPACE, FAMILIES_MAP_NAMESPACE, MEMBERS_MAP_NAMESPACE}; - -type FamilyHeadKey = IdentityKey; - -pub struct FamilyIndex<'a> { - pub label: UniqueIndex<'a, FamilyHeadKey, Family>, -} - -impl<'a> IndexList for FamilyIndex<'a> { - fn get_indexes(&'_ self) -> Box> + '_> { - let v: Vec<&dyn Index> = vec![&self.label]; - Box::new(v.into_iter()) - } -} - -// storage access function. -pub fn families<'a>() -> IndexedMap<'a, FamilyHeadKey, Family, FamilyIndex<'a>> { - let indexes = FamilyIndex { - label: UniqueIndex::new(|d| d.label().to_string(), FAMILIES_INDEX_NAMESPACE), - }; - IndexedMap::new(FAMILIES_MAP_NAMESPACE, indexes) -} - -pub const MEMBERS: Map = Map::new(MEMBERS_MAP_NAMESPACE); - -// TODO: this introduces an unbounded query. We should redesign it. -pub fn get_members( - family: &Family, - store: &dyn Storage, -) -> Result, MixnetContractError> { - Ok(MEMBERS - .range(store, None, None, Order::Ascending) - .filter_map(|res| res.ok()) - .filter(|(_member, head)| head == family.head()) - .map(|(member, _storage_key)| member) - .collect()) -} - -pub fn must_get_family( - head: &FamilyHead, - store: &dyn Storage, -) -> Result { - let key = head.identity(); - - families() - .may_load(store, key.to_string())? - .ok_or(MixnetContractError::FamilyDoesNotExist { - head: head.identity().to_string(), - }) -} - -pub fn must_get_family_by_label( - label: String, - store: &dyn Storage, -) -> Result { - families() - .idx - .label - .item(store, label.clone())? - .map(|record| record.1) - .ok_or(MixnetContractError::FamilyLabelDoesNotExist { label }) -} - -pub fn save_family(f: &Family, store: &mut dyn Storage) -> Result<(), MixnetContractError> { - Ok(families().save(store, f.head_identity().to_string(), f)?) -} - -pub fn add_family_member( - f: &Family, - store: &mut dyn Storage, - member: IdentityKeyRef<'_>, -) -> Result<(), MixnetContractError> { - Ok(MEMBERS.save(store, member.to_string(), f.head())?) -} - -pub fn remove_family_member(store: &mut dyn Storage, member: IdentityKeyRef<'_>) { - MEMBERS.remove(store, member.to_string()) -} - -pub fn is_family_member( - store: &dyn Storage, - f: &Family, - member: IdentityKeyRef<'_>, -) -> Result { - let existing_head = MEMBERS.may_load(store, member.to_owned())?; - Ok(existing_head.as_ref() == Some(f.head())) -} - -pub fn is_any_member( - store: &dyn Storage, - member: IdentityKeyRef<'_>, -) -> Result, MixnetContractError> { - Ok(MEMBERS.may_load(store, member.to_string())?) -} diff --git a/contracts/mixnet/src/families/transactions.rs b/contracts/mixnet/src/families/transactions.rs deleted file mode 100644 index ee88f98e63..0000000000 --- a/contracts/mixnet/src/families/transactions.rs +++ /dev/null @@ -1,291 +0,0 @@ -// Copyright 2022-2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use super::storage::{ - add_family_member, is_any_member, is_family_member, must_get_family, remove_family_member, - save_family, -}; -use crate::families::queries::get_family_by_label; -use crate::families::signature_helpers::verify_family_join_permit; -use crate::support::helpers::ensure_bonded; -use cosmwasm_std::{DepsMut, MessageInfo, Response}; -use mixnet_contract_common::families::{Family, FamilyHead}; -use mixnet_contract_common::{error::MixnetContractError, IdentityKey}; -use nym_contracts_common::signing::MessageSignature; - -/// Creates a new MixNode family with senders node as head -pub(crate) fn try_create_family( - deps: DepsMut, - info: MessageInfo, - label: String, -) -> Result { - let existing_bond = - crate::mixnodes::helpers::must_get_mixnode_bond_by_owner(deps.storage, &info.sender)?; - - ensure_bonded(&existing_bond)?; - - let family_head = FamilyHead::new(existing_bond.identity()); - - // can't overwrite existing family - if must_get_family(&family_head, deps.storage).is_ok() { - return Err(MixnetContractError::FamilyCanHaveOnlyOne); - } - - // the label must be unique - if get_family_by_label(label.clone(), deps.storage)? - .family - .is_some() - { - return Err(MixnetContractError::FamilyWithLabelExists(label)); - } - - let family = Family::new(family_head, label); - save_family(&family, deps.storage)?; - Ok(Response::default()) -} - -pub(crate) fn try_join_family( - deps: DepsMut, - info: MessageInfo, - join_permit: MessageSignature, - family_head: FamilyHead, -) -> Result { - let existing_bond = - crate::mixnodes::helpers::must_get_mixnode_bond_by_owner(deps.storage, &info.sender)?; - - ensure_bonded(&existing_bond)?; - - if family_head.identity() == existing_bond.identity() { - return Err(MixnetContractError::CantJoinOwnFamily { - head: family_head.identity().to_string(), - member: existing_bond.identity().to_string(), - }); - } - - if let Some(family) = is_any_member(deps.storage, existing_bond.identity())? { - return Err(MixnetContractError::AlreadyMemberOfFamily( - family.identity().to_string(), - )); - } - - verify_family_join_permit( - deps.as_ref(), - family_head.clone(), - existing_bond.identity(), - join_permit, - )?; - - let family = must_get_family(&family_head, deps.storage)?; - - add_family_member(&family, deps.storage, existing_bond.identity())?; - - Ok(Response::default()) -} - -pub(crate) fn try_leave_family( - deps: DepsMut, - info: MessageInfo, - family_head: FamilyHead, -) -> Result { - let existing_bond = - crate::mixnodes::helpers::must_get_mixnode_bond_by_owner(deps.storage, &info.sender)?; - - ensure_bonded(&existing_bond)?; - - if family_head.identity() == existing_bond.identity() { - return Err(MixnetContractError::CantLeaveOwnFamily { - head: family_head.identity().to_string(), - member: existing_bond.identity().to_string(), - }); - } - - let family = must_get_family(&family_head, deps.storage)?; - if !is_family_member(deps.storage, &family, existing_bond.identity())? { - return Err(MixnetContractError::NotAMember { - head: family_head.identity().to_string(), - member: existing_bond.identity().to_string(), - }); - } - - remove_family_member(deps.storage, existing_bond.identity()); - - Ok(Response::default()) -} - -pub(crate) fn try_head_kick_member( - deps: DepsMut, - info: MessageInfo, - member: IdentityKey, -) -> Result { - let head_bond = - crate::mixnodes::helpers::must_get_mixnode_bond_by_owner(deps.storage, &info.sender)?; - - // make sure we're still in the mixnet - ensure_bonded(&head_bond)?; - - // make sure we're not trying to kick ourselves... - if member == head_bond.identity() { - return Err(MixnetContractError::CantLeaveOwnFamily { - head: head_bond.identity().to_string(), - member, - }); - } - - // get the family details - let family_head = FamilyHead::new(head_bond.identity()); - let family = must_get_family(&family_head, deps.storage)?; - - // make sure the member we're trying to kick is an actual member - if !is_family_member(deps.storage, &family, &member)? { - return Err(MixnetContractError::NotAMember { - head: family_head.identity().to_string(), - member, - }); - } - - // finally get rid of the member - remove_family_member(deps.storage, &member); - Ok(Response::default()) -} - -#[cfg(test)] -mod test { - use super::*; - use crate::families::queries::get_family_by_head; - use crate::mixnet_contract_settings::storage::minimum_mixnode_pledge; - use crate::support::tests::fixtures; - use crate::support::tests::test_helpers::TestSetup; - use cosmwasm_std::testing::mock_info; - - #[test] - fn test_family_crud() { - let mut test = TestSetup::new(); - let env = test.env(); - - let head = "alice"; - let malicious_head = "timmy"; - let member = "bob"; - - let minimum_pledge = minimum_mixnode_pledge(test.deps().storage).unwrap(); - let cost_params = fixtures::mix_node_cost_params_fixture(); - - let (head_mixnode, head_bond_sig, head_keypair) = test.mixnode_with_signature(head, None); - let (malicious_mixnode, malicious_bond_sig, _malicious_keypair) = - test.mixnode_with_signature(malicious_head, None); - let (member_mixnode, member_bond_sig, _member_keypair) = - test.mixnode_with_signature(member, None); - - crate::mixnodes::transactions::try_add_mixnode( - test.deps_mut(), - env.clone(), - mock_info(head, &[minimum_pledge.clone()]), - head_mixnode.clone(), - cost_params.clone(), - head_bond_sig, - ) - .unwrap(); - - crate::mixnodes::transactions::try_add_mixnode( - test.deps_mut(), - env.clone(), - mock_info(malicious_head, &[minimum_pledge.clone()]), - malicious_mixnode, - cost_params.clone(), - malicious_bond_sig, - ) - .unwrap(); - - crate::mixnodes::transactions::try_add_mixnode( - test.deps_mut(), - env, - mock_info(member, &[minimum_pledge]), - member_mixnode.clone(), - cost_params, - member_bond_sig, - ) - .unwrap(); - - try_create_family(test.deps_mut(), mock_info(head, &[]), "test".to_string()).unwrap(); - let family_head = FamilyHead::new(&head_mixnode.identity_key); - assert!(must_get_family(&family_head, test.deps().storage).is_ok()); - - let nope = try_create_family( - test.deps_mut(), - mock_info(malicious_head, &[]), - "test".to_string(), - ); - - match nope { - Ok(_) => panic!("This should fail, since family with label already exists"), - Err(e) => match e { - MixnetContractError::FamilyWithLabelExists(label) => assert_eq!(label, "test"), - _ => panic!("This should return FamilyWithLabelExists"), - }, - } - - let family = get_family_by_label("test".to_string(), test.deps().storage) - .unwrap() - .family; - assert!(family.is_some()); - assert_eq!(family.unwrap().head_identity(), family_head.identity()); - - let family = get_family_by_head(family_head.identity(), test.deps().storage) - .unwrap() - .family - .unwrap(); - assert_eq!(family.head_identity(), family_head.identity()); - - let join_permit = - test.generate_family_join_permit(&head_keypair, &member_mixnode.identity_key); - - try_join_family( - test.deps_mut(), - mock_info(member, &[]), - join_permit, - family_head.clone(), - ) - .unwrap(); - - let family = must_get_family(&family_head, test.deps().storage).unwrap(); - - assert!( - is_family_member(test.deps().storage, &family, &member_mixnode.identity_key).unwrap() - ); - - try_leave_family(test.deps_mut(), mock_info(member, &[]), family_head.clone()).unwrap(); - - let family = must_get_family(&family_head, test.deps().storage).unwrap(); - assert!( - !is_family_member(test.deps().storage, &family, &member_mixnode.identity_key).unwrap() - ); - - let new_join_permit = - test.generate_family_join_permit(&head_keypair, &member_mixnode.identity_key); - - try_join_family( - test.deps_mut(), - mock_info(member, &[]), - new_join_permit, - family_head.clone(), - ) - .unwrap(); - - let family = must_get_family(&family_head, test.deps().storage).unwrap(); - - assert!( - is_family_member(test.deps().storage, &family, &member_mixnode.identity_key).unwrap() - ); - - try_head_kick_member( - test.deps_mut(), - mock_info(head, &[]), - member_mixnode.identity_key.clone(), - ) - .unwrap(); - - let family = must_get_family(&family_head, test.deps().storage).unwrap(); - assert!( - !is_family_member(test.deps().storage, &family, &member_mixnode.identity_key).unwrap() - ); - } -} diff --git a/contracts/mixnet/src/gateways/mod.rs b/contracts/mixnet/src/gateways/mod.rs index ebff4a6ace..f3a3c9941f 100644 --- a/contracts/mixnet/src/gateways/mod.rs +++ b/contracts/mixnet/src/gateways/mod.rs @@ -3,6 +3,5 @@ pub mod helpers; pub mod queries; -pub mod signature_helpers; pub mod storage; pub mod transactions; diff --git a/contracts/mixnet/src/gateways/queries.rs b/contracts/mixnet/src/gateways/queries.rs index bfd7eb0c4d..1cb19655e4 100644 --- a/contracts/mixnet/src/gateways/queries.rs +++ b/contracts/mixnet/src/gateways/queries.rs @@ -5,6 +5,7 @@ use super::storage; use crate::constants::{GATEWAY_BOND_DEFAULT_RETRIEVAL_LIMIT, GATEWAY_BOND_MAX_RETRIEVAL_LIMIT}; // Keeps gateway and mixnode retrieval in sync by re-using the constant. Could be split into its own constant. use cosmwasm_std::{Deps, Order, StdResult}; use cw_storage_plus::Bound; +use mixnet_contract_common::gateway::{PreassignedGatewayIdsResponse, PreassignedId}; use mixnet_contract_common::{ GatewayBond, GatewayBondResponse, GatewayOwnershipResponse, IdentityKey, PagedGatewayResponse, }; @@ -56,6 +57,29 @@ pub fn query_gateway_bond(deps: Deps<'_>, identity: IdentityKey) -> StdResult, + start_after: Option, + limit: Option, +) -> StdResult { + let limit = limit.unwrap_or(50).min(100) as usize; + + let start = start_after.as_deref().map(Bound::exclusive); + + let ids = storage::PREASSIGNED_LEGACY_IDS + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|res| res.map(|(identity, node_id)| PreassignedId { identity, node_id })) + .collect::>>()?; + + let start_next_after = ids.last().map(|id| id.identity.clone()); + + Ok(PreassignedGatewayIdsResponse { + ids, + start_next_after, + }) +} + #[cfg(test)] pub(crate) mod tests { use super::*; @@ -113,61 +137,44 @@ pub(crate) mod tests { #[test] fn gateway_pagination_works() { - let mut deps = test_helpers::init_contract(); - let mut rng = test_helpers::test_rng(); let stake = tests::fixtures::good_gateway_pledge(); + let mut test = TestSetup::new(); - // prepare 4 messages and identities that are sorted by the generated identities + // prepare 4 gateways that are sorted by the generated identities // (because we query them in an ascended manner) - let mut exec_data = (0..4) - .map(|i| { - let sender = format!("nym-addr{}", i); - let (msg, identity) = tests::messages::valid_bond_gateway_msg( - &mut rng, - deps.as_ref(), - stake.clone(), - &sender, - ); - (msg, (sender, identity)) - }) + let mut gateways = (0..4) + .map(|i| test.gateway_with_signature(format!("sender{}", i), None).0) .collect::>(); - exec_data.sort_by(|(_, (_, id1)), (_, (_, id2))| id1.cmp(id2)); - let (messages, sender_identities): (Vec<_>, Vec<_>) = exec_data.into_iter().unzip(); + gateways.sort_by(|g1, g2| g1.identity_key.cmp(&g2.identity_key)); - let info = mock_info(&sender_identities[0].0.clone(), &stake); - execute(deps.as_mut(), mock_env(), info, messages[0].clone()).unwrap(); + let info = mock_info("sender0", &stake); + test.save_legacy_gateway(gateways[0].clone(), &info); let per_page = 2; - let page1 = query_gateways_paged(deps.as_ref(), None, Option::from(per_page)).unwrap(); + let page1 = query_gateways_paged(test.deps(), None, Option::from(per_page)).unwrap(); // page should have 1 result on it assert_eq!(1, page1.nodes.len()); // save another - let info = mock_info( - &sender_identities[1].0.clone(), - &tests::fixtures::good_gateway_pledge(), - ); - execute(deps.as_mut(), mock_env(), info, messages[1].clone()).unwrap(); + let info = mock_info("sender1", &stake); + test.save_legacy_gateway(gateways[1].clone(), &info); // page1 should have 2 results on it - let page1 = query_gateways_paged(deps.as_ref(), None, Option::from(per_page)).unwrap(); + let page1 = query_gateways_paged(test.deps(), None, Option::from(per_page)).unwrap(); assert_eq!(2, page1.nodes.len()); - let info = mock_info( - &sender_identities[2].0.clone(), - &tests::fixtures::good_gateway_pledge(), - ); - execute(deps.as_mut(), mock_env(), info, messages[2].clone()).unwrap(); + let info = mock_info("sender2", &stake); + test.save_legacy_gateway(gateways[2].clone(), &info); // page1 still has 2 results - let page1 = query_gateways_paged(deps.as_ref(), None, Option::from(per_page)).unwrap(); + let page1 = query_gateways_paged(test.deps(), None, Option::from(per_page)).unwrap(); assert_eq!(2, page1.nodes.len()); // retrieving the next page should start after the last key on this page let start_after = page1.start_next_after.unwrap(); let page2 = query_gateways_paged( - deps.as_ref(), + test.deps(), Option::from(start_after.clone()), Option::from(per_page), ) @@ -176,14 +183,11 @@ pub(crate) mod tests { assert_eq!(1, page2.nodes.len()); // save another one - let info = mock_info( - &sender_identities[3].0.clone(), - &tests::fixtures::good_gateway_pledge(), - ); - execute(deps.as_mut(), mock_env(), info, messages[3].clone()).unwrap(); + let info = mock_info("sender3", &stake); + test.save_legacy_gateway(gateways[3].clone(), &info); let page2 = query_gateways_paged( - deps.as_ref(), + test.deps(), Option::from(start_after), Option::from(per_page), ) @@ -202,13 +206,13 @@ pub(crate) mod tests { assert!(res.gateway.is_none()); // gateway was added to "bob", "fred" still does not own one - test.add_dummy_gateway("bob", None); + test.add_legacy_gateway("bob", None); let res = query_owned_gateway(test.deps(), "fred".to_string()).unwrap(); assert!(res.gateway.is_none()); // "fred" now owns a gateway! - test.add_dummy_gateway("fred", None); + test.add_legacy_gateway("fred", None); let res = query_owned_gateway(test.deps(), "fred".to_string()).unwrap(); assert!(res.gateway.is_some()); diff --git a/contracts/mixnet/src/gateways/signature_helpers.rs b/contracts/mixnet/src/gateways/signature_helpers.rs deleted file mode 100644 index 6f9b712554..0000000000 --- a/contracts/mixnet/src/gateways/signature_helpers.rs +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::signing::storage as signing_storage; -use crate::support::helpers::decode_ed25519_identity_key; -use cosmwasm_std::{Addr, Coin, Deps}; -use mixnet_contract_common::error::MixnetContractError; -use mixnet_contract_common::{ - construct_gateway_bonding_sign_payload, construct_legacy_gateway_bonding_sign_payload, Gateway, -}; -use nym_contracts_common::signing::MessageSignature; -use nym_contracts_common::signing::Verifier; - -pub(crate) fn verify_gateway_bonding_signature( - deps: Deps<'_>, - sender: Addr, - pledge: Coin, - gateway: Gateway, - signature: MessageSignature, -) -> Result<(), MixnetContractError> { - // recover the public key - let public_key = decode_ed25519_identity_key(&gateway.identity_key)?; - - // reconstruct the payload, first try the current format, then attempt legacy - let nonce = signing_storage::get_signing_nonce(deps.storage, sender.clone())?; - let msg = construct_gateway_bonding_sign_payload( - nonce, - sender.clone(), - pledge.clone(), - gateway.clone(), - ); - - if deps - .api - .verify_message(msg, signature.clone(), &public_key)? - { - Ok(()) - } else { - // attempt to use legacy - let msg_legacy = - construct_legacy_gateway_bonding_sign_payload(nonce, sender, pledge, gateway); - if deps - .api - .verify_message(msg_legacy, signature, &public_key)? - { - Ok(()) - } else { - Err(MixnetContractError::InvalidEd25519Signature) - } - } -} diff --git a/contracts/mixnet/src/gateways/storage.rs b/contracts/mixnet/src/gateways/storage.rs index 9b5892844a..95bf11dcc9 100644 --- a/contracts/mixnet/src/gateways/storage.rs +++ b/contracts/mixnet/src/gateways/storage.rs @@ -1,10 +1,16 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::constants::{GATEWAYS_OWNER_IDX_NAMESPACE, GATEWAYS_PK_NAMESPACE}; +use crate::constants::{ + GATEWAYS_OWNER_IDX_NAMESPACE, GATEWAYS_PK_NAMESPACE, LEGACY_GATEWAY_ID_NAMESPACE, +}; use cosmwasm_std::Addr; -use cw_storage_plus::{Index, IndexList, IndexedMap, UniqueIndex}; -use mixnet_contract_common::{GatewayBond, IdentityKeyRef}; +use cw_storage_plus::{Index, IndexList, IndexedMap, Map, UniqueIndex}; +use mixnet_contract_common::{GatewayBond, IdentityKeyRef, NodeId}; +use nym_contracts_common::IdentityKey; + +pub(crate) const PREASSIGNED_LEGACY_IDS: Map = + Map::new(LEGACY_GATEWAY_ID_NAMESPACE); pub(crate) struct GatewayBondIndex<'a> { pub(crate) owner: UniqueIndex<'a, Addr, GatewayBond>, diff --git a/contracts/mixnet/src/gateways/transactions.rs b/contracts/mixnet/src/gateways/transactions.rs index 6075c25894..5720c48455 100644 --- a/contracts/mixnet/src/gateways/transactions.rs +++ b/contracts/mixnet/src/gateways/transactions.rs @@ -3,21 +3,21 @@ use super::helpers::must_get_gateway_bond_by_owner; use super::storage; -use crate::gateways::signature_helpers::verify_gateway_bonding_signature; +use crate::constants::default_node_costs; use crate::mixnet_contract_settings::storage as mixnet_params_storage; -use crate::signing::storage as signing_storage; -use crate::support::helpers::{ensure_no_existing_bond, validate_pledge}; -use cosmwasm_std::{BankMsg, DepsMut, Env, MessageInfo, Response}; +use crate::nodes::helpers::save_new_nymnode_with_id; +use crate::nodes::transactions::add_nym_node_inner; +use crate::support::helpers::ensure_epoch_in_progress_state; +use crate::support::helpers::AttachSendTokens; +use cosmwasm_std::{DepsMut, Env, MessageInfo, Response}; use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::events::{ - new_gateway_bonding_event, new_gateway_config_update_event, new_gateway_unbonding_event, + new_gateway_config_update_event, new_gateway_unbonding_event, new_migrated_gateway_event, }; use mixnet_contract_common::gateway::GatewayConfigUpdate; -use mixnet_contract_common::{Gateway, GatewayBond}; +use mixnet_contract_common::{Gateway, GatewayBondingPayload, NodeCostParams}; use nym_contracts_common::signing::MessageSignature; -// TODO: perhaps also require the user to explicitly provide what it thinks is the current nonce -// so that we could return a better error message if it doesn't match? pub(crate) fn try_add_gateway( deps: DepsMut<'_>, env: Env, @@ -25,51 +25,19 @@ pub(crate) fn try_add_gateway( gateway: Gateway, owner_signature: MessageSignature, ) -> Result { - // check if the pledge contains any funds of the appropriate denomination - let minimum_pledge = mixnet_params_storage::minimum_gateway_pledge(deps.storage)?; - let pledge = validate_pledge(info.funds, minimum_pledge)?; + let signed_payload = GatewayBondingPayload::new(gateway.clone()); + let denom = mixnet_params_storage::rewarding_denom(deps.storage)?; + let cost_params = default_node_costs(denom); - // if the client has an active bonded mixnode or gateway, don't allow bonding - ensure_no_existing_bond(&info.sender, deps.storage)?; - - // check if somebody else has already bonded a gateway with this identity - if let Some(existing_bond) = - storage::gateways().may_load(deps.storage, &gateway.identity_key)? - { - if existing_bond.owner != info.sender { - return Err(MixnetContractError::DuplicateGateway { - owner: existing_bond.owner, - }); - } - } - - // check if this sender actually owns the gateway by checking the signature - verify_gateway_bonding_signature( - deps.as_ref(), - info.sender.clone(), - pledge.clone(), - gateway.clone(), + add_nym_node_inner( + deps, + env, + info, + gateway.into(), + cost_params, owner_signature, - )?; - - // update the signing nonce associated with this sender so that the future signature would be made on the new value - signing_storage::increment_signing_nonce(deps.storage, info.sender.clone())?; - - let gateway_identity = gateway.identity_key.clone(); - let bond = GatewayBond::new( - pledge.clone(), - info.sender.clone(), - env.block.height, - gateway, - ); - - storage::gateways().save(deps.storage, bond.identity(), &bond)?; - - Ok(Response::new().add_event(new_gateway_bonding_event( - &info.sender, - &pledge, - &gateway_identity, - ))) + signed_payload, + ) } pub(crate) fn try_remove_gateway( @@ -77,31 +45,18 @@ pub(crate) fn try_remove_gateway( info: MessageInfo, ) -> Result { // try to find the node of the sender - let gateway_bond = match storage::gateways() - .idx - .owner - .item(deps.storage, info.sender.clone())? - { - Some(record) => record.1, - None => return Err(MixnetContractError::NoAssociatedGatewayBond { owner: info.sender }), - }; - - // send bonded funds back to the bond owner - let return_tokens = BankMsg::Send { - to_address: info.sender.to_string(), - amount: vec![gateway_bond.pledge_amount()], - }; + let gateway_bond = must_get_gateway_bond_by_owner(deps.storage, &info.sender)?; // remove the bond storage::gateways().remove(deps.storage, gateway_bond.identity())?; Ok(Response::new() - .add_message(return_tokens) .add_event(new_gateway_unbonding_event( &info.sender, &gateway_bond.pledge_amount, gateway_bond.identity(), - ))) + )) + .send_tokens(&info.sender, gateway_bond.pledge_amount)) } pub(crate) fn try_update_gateway_config( @@ -129,19 +84,72 @@ pub(crate) fn try_update_gateway_config( Ok(Response::new().add_event(cfg_update_event)) } +pub fn try_migrate_to_nymnode( + deps: DepsMut, + info: MessageInfo, + cost_params: Option, +) -> Result { + let gateway_bond = must_get_gateway_bond_by_owner(deps.storage, &info.sender)?; + + // currently on mainnet there are no gateways bonded with vesting tokens + // if somebody decides to make one between now and when this is deployed, + // it's on them. they have to unbond and rebond. simple as that. + if gateway_bond.proxy.is_some() { + return Err(MixnetContractError::VestingNodeMigration); + } + + ensure_epoch_in_progress_state(deps.storage)?; + + // remove the bond + storage::gateways().remove(deps.storage, gateway_bond.identity())?; + + let cost_params = + cost_params.unwrap_or_else(|| default_node_costs(&gateway_bond.pledge_amount.denom)); + + let gateway_identity = gateway_bond.gateway.identity_key.clone(); + + // this should have been added during migration + let node_id = storage::PREASSIGNED_LEGACY_IDS + .may_load(deps.storage, gateway_identity.clone())? + .ok_or_else(|| MixnetContractError::InconsistentState { + comment: "legacy gateway did not have a pre-assigned node id".to_string(), + })?; + + // create nym-node entry + // for gateways it's quite straightforward as there are no delegations or rewards to worry about + save_new_nymnode_with_id( + deps.storage, + node_id, + gateway_bond.block_height, + gateway_bond.gateway.into(), + cost_params, + info.sender.clone(), + gateway_bond.pledge_amount, + )?; + + storage::PREASSIGNED_LEGACY_IDS.remove(deps.storage, gateway_identity.clone()); + + Ok(Response::new().add_event(new_migrated_gateway_event( + &info.sender, + &gateway_identity, + node_id, + ))) +} + #[cfg(test)] pub mod tests { use super::*; use crate::contract::execute; use crate::gateways::queries; use crate::interval::pending_events; - use crate::mixnet_contract_settings::storage::minimum_gateway_pledge; + use crate::mixnet_contract_settings::storage::minimum_node_pledge; + use crate::nodes::helpers::get_node_details_by_identity; + use crate::signing::storage as signing_storage; use crate::support::tests; - use crate::support::tests::fixtures; - use crate::support::tests::fixtures::good_mixnode_pledge; + use crate::support::tests::fixtures::{good_gateway_pledge, good_mixnode_pledge}; use crate::support::tests::test_helpers::TestSetup; use cosmwasm_std::testing::mock_info; - use cosmwasm_std::{Addr, Uint128}; + use cosmwasm_std::{Addr, BankMsg, Uint128}; use mixnet_contract_common::ExecuteMsg; #[test] @@ -150,7 +158,7 @@ pub mod tests { // if we fail validation (by say not sending enough funds let sender = "alice"; - let minimum_pledge = minimum_gateway_pledge(test.deps().storage).unwrap(); + let minimum_pledge = minimum_node_pledge(test.deps().storage).unwrap(); let mut insufficient_pledge = minimum_pledge.clone(); insufficient_pledge.amount -= Uint128::new(1000); @@ -180,7 +188,7 @@ pub mod tests { let info = mock_info(sender, &[minimum_pledge]); // if there was already a gateway bonded by particular user - test.add_dummy_gateway(sender, None); + test.add_legacy_gateway(sender, None); // it fails let result = try_add_gateway(test.deps_mut(), env.clone(), info, gateway, sig); @@ -189,9 +197,9 @@ pub mod tests { // the same holds if the user already owns a mixnode let sender2 = "mixnode-owner"; - let mix_id = test.add_dummy_mixnode(sender2, None); + let mix_id = test.add_legacy_mixnode(sender2, None); - let info = mock_info(sender2, &fixtures::good_gateway_pledge()); + let info = mock_info(sender2, &good_gateway_pledge()); let (gateway, sig) = test.gateway_with_signature(sender2, None); let result = try_add_gateway( @@ -206,8 +214,14 @@ pub mod tests { // but after he unbonds it, it's all fine again pending_events::unbond_mixnode(test.deps_mut(), &env, 123, mix_id).unwrap(); - let result = try_add_gateway(test.deps_mut(), env, info, gateway, sig); + let result = try_add_gateway(test.deps_mut(), env, info.clone(), gateway.clone(), sig); assert!(result.is_ok()); + + // and the node has been added as a nym-node + let nym_node = get_node_details_by_identity(test.deps().storage, gateway.identity_key) + .unwrap() + .unwrap(); + assert_eq!(nym_node.bond_information.owner, info.sender) } #[test] @@ -276,7 +290,8 @@ pub mod tests { .unwrap(); assert_eq!(1, updated_nonce); - try_remove_gateway(test.deps_mut(), info.clone()).unwrap(); + // the moment gateway got bonded, it got added as a nymnode thus we have to remove nym-node + test.immediately_unbond_node(gateway.identity_key.clone()); let res = try_add_gateway(test.deps_mut(), env, info, gateway, signature); assert_eq!(res, Err(MixnetContractError::InvalidEd25519Signature)); @@ -301,7 +316,7 @@ pub mod tests { ); // let's add a node owned by bob - test.add_dummy_gateway("bob", None); + test.add_legacy_gateway("bob", None); // attempt to unbond fred's node, which doesn't exist let info = mock_info("fred", &[]); @@ -324,7 +339,7 @@ pub mod tests { assert_eq!(&Addr::unchecked("bob"), first_node.owner()); // add a node owned by fred - let fred_identity = test.add_dummy_gateway("fred", None); + let fred_identity = test.add_legacy_gateway("fred", None); // let's make sure we now have 2 nodes: let nodes = queries::query_gateways_paged(test.deps(), None, None) @@ -340,7 +355,7 @@ pub mod tests { // we should see a funds transfer from the contract back to fred let expected_message = BankMsg::Send { to_address: String::from(info.sender), - amount: tests::fixtures::good_gateway_pledge(), + amount: good_gateway_pledge(), }; // run the executor and check that we got back the correct results @@ -386,7 +401,7 @@ pub mod tests { }) ); - test.add_dummy_gateway(owner, None); + test.add_legacy_gateway(owner, None); // "normal" update succeeds let res = try_update_gateway_config(test.deps_mut(), info, update.clone()); diff --git a/contracts/mixnet/src/interval/helpers.rs b/contracts/mixnet/src/interval/helpers.rs index dd45c7511a..f1802973ac 100644 --- a/contracts/mixnet/src/interval/helpers.rs +++ b/contracts/mixnet/src/interval/helpers.rs @@ -1,12 +1,13 @@ -// Copyright 2022 - Nym Technologies SA +// Copyright 2022-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use crate::interval::storage; +use crate::rewards; use crate::rewards::storage as rewards_storage; -use cosmwasm_std::{Response, Storage}; +use cosmwasm_std::{Env, Response, Storage}; use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::events::new_interval_config_update_event; -use mixnet_contract_common::{BlockHeight, Interval}; +use mixnet_contract_common::{BlockHeight, EpochId, Interval}; use std::time::Duration; pub(crate) fn change_interval_config( @@ -33,6 +34,26 @@ pub(crate) fn change_interval_config( ))) } +/// Update the storage to advance into the next epoch. +/// It's responsibility of the caller to ensure the epoch is actually over. +pub(crate) fn advance_epoch( + storage: &mut dyn Storage, + env: Env, +) -> Result { + let current_interval = storage::current_interval(storage)?; + + // if the current **INTERVAL** is over, apply reward pool changes + if current_interval.is_current_interval_over(&env) { + // this one is a very important one! + rewards::helpers::apply_reward_pool_changes(storage)?; + } + + let updated_interval = current_interval.advance_epoch(); + storage::save_interval(storage, &updated_interval)?; + + Ok(updated_interval.current_epoch_absolute_id()) +} + #[cfg(test)] mod tests { use super::*; diff --git a/contracts/mixnet/src/interval/pending_events.rs b/contracts/mixnet/src/interval/pending_events.rs index fa44e36ec3..59e39e434e 100644 --- a/contracts/mixnet/src/interval/pending_events.rs +++ b/contracts/mixnet/src/interval/pending_events.rs @@ -1,21 +1,22 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use cosmwasm_std::{Addr, Coin, DepsMut, Env, Response}; +use cosmwasm_std::{Addr, BankMsg, Coin, DepsMut, Env, Response}; use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::events::{ - new_active_set_update_event, new_delegation_event, new_delegation_on_unbonded_node_event, - new_mixnode_cost_params_update_event, new_mixnode_unbonding_event, new_pledge_decrease_event, - new_pledge_increase_event, new_rewarding_params_update_event, new_undelegation_event, + new_active_set_update_event, new_active_set_update_failure, new_cost_params_update_event, + new_delegation_event, new_delegation_on_unbonded_node_event, new_mixnode_unbonding_event, + new_nym_node_unbonding_event, new_pledge_decrease_event, new_pledge_increase_event, + new_rewarding_params_update_event, new_undelegation_event, }; -use mixnet_contract_common::mixnode::MixNodeCostParams; +use mixnet_contract_common::mixnode::NodeCostParams; use mixnet_contract_common::pending_events::{ PendingEpochEventData, PendingEpochEventKind, PendingIntervalEventData, PendingIntervalEventKind, }; -use mixnet_contract_common::reward_params::IntervalRewardingParamsUpdate; -use mixnet_contract_common::{BlockHeight, Delegation, MixId}; +use mixnet_contract_common::reward_params::{ActiveSetUpdate, IntervalRewardingParamsUpdate}; +use mixnet_contract_common::{BlockHeight, Delegation, NodeId}; use crate::delegations; use crate::delegations::storage as delegations_storage; @@ -23,8 +24,11 @@ use crate::interval::helpers::change_interval_config; use crate::interval::storage; use crate::mixnodes::helpers::{cleanup_post_unbond_mixnode_storage, get_mixnode_details_by_id}; use crate::mixnodes::storage as mixnodes_storage; +use crate::nodes::helpers::{cleanup_post_unbond_nym_node_storage, get_node_details_by_id}; +use crate::nodes::storage as nymnodes_storage; use crate::rewards::storage as rewards_storage; -use crate::support::helpers::AttachSendTokens; +use crate::rewards::storage::RewardingStorage; +use crate::support::helpers::{ensure_any_node_bonded, AttachSendTokens}; pub(crate) trait ContractExecutableEvent { // note: the error only means a HARD error like we failed to read from storage. @@ -38,33 +42,37 @@ pub(crate) fn delegate( env: &Env, created_at: BlockHeight, owner: Addr, - mix_id: MixId, + node_id: NodeId, amount: Coin, ) -> Result { - // check if the target node still exists (it might have unbonded between this event getting created - // and being executed). Do note that it's absolutely possible for a mixnode to get immediately - // unbonded at this very block (if the event was pending), but that's tough luck, then it's up - // to the delegator to click the undelegate button - let mixnode_details = match get_mixnode_details_by_id(deps.storage, mix_id)? { - Some(details) - if details.rewarding_details.still_bonded() - && !details.bond_information.is_unbonding => - { - details - } - _ => { - // if mixnode is no longer bonded or in the process of unbonding, return the tokens back to the - // delegator; - let response = Response::new() - .send_tokens(&owner, amount.clone()) - .add_event(new_delegation_on_unbonded_node_event(&owner, mix_id)); - - return Ok(response); - } + // first check - see if we have any rewarding information in the storage, if not, the node has fully unbonded + // (and also didn't have any delegations) + let Some(mut node_rewarding) = + rewards_storage::NYMNODE_REWARDING.may_load(deps.storage, node_id)? + else { + return Ok(Response::new() + .send_tokens(&owner, amount) + .add_event(new_delegation_on_unbonded_node_event(&owner, node_id))); }; + // more extensive check to see if node is still bonded, however, this time we have to unfortunately check + // BOTH nym-node and legacy nym-node. + // the underlying target node might have unbonded between this event getting created + // and being executed. Do note that it's absolutely possible for a node to get immediately + // unbonded at this very block (if the event was pending), but that's tough luck, then it's up + // to the delegator to click the undelegate button + match ensure_any_node_bonded(deps.storage, node_id) { + // cosmwasm-std errors are critical and recoverable because they imply issues with the underlying storage + Err(MixnetContractError::StdErr { source }) => return Err(source.into()), + Err(_unbonded) => { + return Ok(Response::new() + .send_tokens(&owner, amount) + .add_event(new_delegation_on_unbonded_node_event(&owner, node_id))); + } + Ok(_) => {} + } + let new_delegation_amount = amount.clone(); - let mut mix_rewarding = mixnode_details.rewarding_details; // the delegation_amount might get increased if there's already a pre-existing delegation on this mixnode // (in that case we just create a fresh delegation with the sum of both) @@ -72,12 +80,12 @@ pub(crate) fn delegate( // if there's an existing delegation, then withdraw the full reward and create a new delegation // with the sum of both - let storage_key = Delegation::generate_storage_key(mix_id, &owner, None); + let storage_key = Delegation::generate_storage_key(node_id, &owner, None); let old_delegation = if let Some(existing_delegation) = delegations_storage::delegations().may_load(deps.storage, storage_key.clone())? { // completely remove the delegation from the node - let og_with_reward = mix_rewarding.undelegate(&existing_delegation)?; + let og_with_reward = node_rewarding.undelegate(&existing_delegation)?; // and adjust the new value by the amount removed (which contains the original delegation // alongside any earned rewards) @@ -89,20 +97,20 @@ pub(crate) fn delegate( }; // add the amount we're intending to delegate (whether it's fresh or we're adding to the existing one) - mix_rewarding.add_base_delegation(stored_delegation_amount.amount)?; + node_rewarding.add_base_delegation(stored_delegation_amount.amount)?; let cosmos_event = new_delegation_event( created_at, &owner, &new_delegation_amount, - mix_id, - mix_rewarding.total_unit_reward, + node_id, + node_rewarding.total_unit_reward, ); let delegation = Delegation::new( owner, - mix_id, - mix_rewarding.total_unit_reward, + node_id, + node_rewarding.total_unit_reward, stored_delegation_amount, env.block.height, ); @@ -114,7 +122,7 @@ pub(crate) fn delegate( Some(&delegation), old_delegation.as_ref(), )?; - rewards_storage::MIXNODE_REWARDING.save(deps.storage, mix_id, &mix_rewarding)?; + rewards_storage::NYMNODE_REWARDING.save(deps.storage, node_id, &node_rewarding)?; Ok(Response::new().add_event(cosmos_event)) } @@ -123,7 +131,7 @@ pub(crate) fn undelegate( deps: DepsMut<'_>, created_at: BlockHeight, owner: Addr, - mix_id: MixId, + mix_id: NodeId, ) -> Result { // see if the delegation still exists (in case of impatient user who decided to send multiple // undelegation requests in an epoch) @@ -132,13 +140,17 @@ pub(crate) fn undelegate( None => return Ok(Response::default()), Some(delegation) => delegation, }; - let mix_rewarding = - rewards_storage::MIXNODE_REWARDING.may_load(deps.storage, mix_id)?.ok_or(MixnetContractError::inconsistent_state( - "mixnode rewarding got removed from the storage whilst there's still an existing delegation", + + // note: rewarding information for nym-nodes and mixnodes are stored under the same storage structure + // and in the same underlying map so it doesn't matter which one we load + + let rewarding_info = + rewards_storage::NYMNODE_REWARDING.may_load(deps.storage, mix_id)?.ok_or(MixnetContractError::inconsistent_state( + "node rewarding got removed from the storage whilst there's still an existing delegation", ))?; // this also appropriately adjusts the storage let tokens_to_return = - delegations::helpers::undelegate(deps.storage, delegation, mix_rewarding)?; + delegations::helpers::undelegate(deps.storage, delegation, rewarding_info)?; let response = Response::new() .send_tokens(&owner, tokens_to_return.clone()) @@ -147,11 +159,55 @@ pub(crate) fn undelegate( Ok(response) } +pub(crate) fn unbond_nym_node( + deps: DepsMut<'_>, + env: &Env, + created_at: BlockHeight, + node_id: NodeId, +) -> Result { + // if we're here it means user executed `try_remove_nym_node` and as a result node was set to be + // in unbonding state and thus nothing could have been done to it (such as attempting to double unbond it) + // thus the node with all its associated information MUST exist in the storage. + let node_details = get_node_details_by_id(deps.storage, node_id)?.ok_or( + MixnetContractError::inconsistent_state( + "nym node getting processed to get unbonded doesn't exist in the storage", + ), + )?; + if node_details.pending_changes.pledge_change.is_some() { + return Err(MixnetContractError::inconsistent_state( + "attempted to unbond nym node while there are associated pending pledge changes", + )); + } + + // the denom on the original pledge was validated at the time of bonding, so we can safely reuse it here + let rewarding_denom = &node_details.bond_information.original_pledge.denom; + let tokens = node_details + .rewarding_details + .operator_pledge_with_reward(rewarding_denom); + + let owner = &node_details.bond_information.owner; + + // send bonded funds (alongside all earned rewards) to the bond owner + let return_tokens = BankMsg::Send { + to_address: owner.to_string(), + amount: vec![tokens], + }; + + // remove the bond and if there are no delegations left, also the rewarding information + cleanup_post_unbond_nym_node_storage(deps.storage, env, &node_details)?; + + let response = Response::new() + .add_message(return_tokens) + .add_event(new_nym_node_unbonding_event(created_at, node_id)); + + Ok(response) +} + pub(crate) fn unbond_mixnode( deps: DepsMut<'_>, env: &Env, created_at: BlockHeight, - mix_id: MixId, + mix_id: NodeId, ) -> Result { // if we're here it means user executed `_try_remove_mixnode` and as a result node was set to be // in unbonding state and thus nothing could have been done to it (such as attempting to double unbond it) @@ -186,28 +242,76 @@ pub(crate) fn unbond_mixnode( Ok(response) } -pub(crate) fn update_active_set_size( +pub(crate) fn update_active_set( deps: DepsMut<'_>, created_at: BlockHeight, - active_set_size: u32, + update: ActiveSetUpdate, ) -> Result { // We don't have to check for authorization as this event can only be pushed // by the authorized entity. // Furthermore, we don't need to check whether the epoch is finished as the // queue is only emptied upon the epoch finishing. - // Also, we know the update is valid as we checked for that before pushing the event onto the queue. + let global_rewarding_params_storage = RewardingStorage::load().global_rewarding_params; + let mut rewarding_params = global_rewarding_params_storage.load(deps.storage)?; - let mut rewarding_params = rewards_storage::REWARDING_PARAMS.load(deps.storage)?; - rewarding_params.try_change_active_set_size(active_set_size)?; - rewards_storage::REWARDING_PARAMS.save(deps.storage, &rewarding_params)?; + // unfortunately this will be a silent error, + // but for this to happen an admin must have been screwing around by pushing active set update onto the queue + // but updating rewarded set immediately, so it's on them + if let Err(err) = rewarding_params.try_change_active_set(update) { + return Ok(Response::new().add_event(new_active_set_update_failure(err))); + } + global_rewarding_params_storage.save(deps.storage, &rewarding_params)?; - Ok(Response::new().add_event(new_active_set_update_event(created_at, active_set_size))) + Ok(Response::new().add_event(new_active_set_update_event(created_at, update))) } -pub(crate) fn increase_pledge( +pub(crate) fn increase_nym_node_pledge( deps: DepsMut<'_>, created_at: BlockHeight, - mix_id: MixId, + node_id: NodeId, + increase: Coin, +) -> Result { + // note: we have already validated the amount to know it has the correct denomination + + // the target node MUST exist - we have checked it at the time of putting this event onto the queue + // we have also verified there were no preceding unbond events + let node_details = get_node_details_by_id(deps.storage, node_id)?.ok_or( + MixnetContractError::inconsistent_state( + "nym node getting processed to increase its pledge doesn't exist in the storage", + ), + )?; + if node_details.pending_changes.pledge_change.is_none() { + return Err(MixnetContractError::inconsistent_state( + "attempted to increase nym node pledge while there are no associated pending changes", + )); + } + + let mut updated_bond = node_details.bond_information.clone(); + let mut updated_rewarding = node_details.rewarding_details; + + updated_bond.original_pledge.amount += increase.amount; + updated_rewarding.increase_operator_uint128(increase.amount)?; + + let mut pending_changes = node_details.pending_changes; + pending_changes.pledge_change = None; + + // update all: bond information, rewarding details and pending pledge changes + nymnodes_storage::nym_nodes().replace( + deps.storage, + node_id, + Some(&updated_bond), + Some(&node_details.bond_information), + )?; + rewards_storage::NYMNODE_REWARDING.save(deps.storage, node_id, &updated_rewarding)?; + nymnodes_storage::PENDING_NYMNODE_CHANGES.save(deps.storage, node_id, &pending_changes)?; + + Ok(Response::new().add_event(new_pledge_increase_event(created_at, node_id, &increase))) +} + +pub(crate) fn increase_mixnode_pledge( + deps: DepsMut<'_>, + created_at: BlockHeight, + mix_id: NodeId, increase: Coin, ) -> Result { // note: we have already validated the amount to know it has the correct denomination @@ -247,10 +351,10 @@ pub(crate) fn increase_pledge( Ok(Response::new().add_event(new_pledge_increase_event(created_at, mix_id, &increase))) } -pub(crate) fn decrease_pledge( +pub(crate) fn decrease_mixnode_pledge( deps: DepsMut<'_>, created_at: BlockHeight, - mix_id: MixId, + mix_id: NodeId, decrease_by: Coin, ) -> Result { // the target node MUST exist - we have checked it at the time of putting this event onto the queue @@ -296,6 +400,61 @@ pub(crate) fn decrease_pledge( Ok(response) } +pub(crate) fn decrease_nym_node_pledge( + deps: DepsMut<'_>, + created_at: BlockHeight, + node_id: NodeId, + decrease_by: Coin, +) -> Result { + // the target node MUST exist - we have checked it at the time of putting this event onto the queue + // we have also verified there were no preceding unbond events + let node_details = get_node_details_by_id(deps.storage, node_id)?.ok_or( + MixnetContractError::inconsistent_state( + "nym node getting processed to increase its pledge doesn't exist in the storage", + ), + )?; + if node_details.pending_changes.pledge_change.is_none() { + return Err(MixnetContractError::inconsistent_state( + "attempted to increase nym node pledge while there are no associated pending changes", + )); + } + + let mut updated_bond = node_details.bond_information.clone(); + let mut updated_rewarding = node_details.rewarding_details; + + let mut pending_changes = node_details.pending_changes; + pending_changes.pledge_change = None; + + // SAFETY: the subtraction here can't overflow as before the event was pushed into the queue, + // we checked that the new value will be higher than minimum pledge (which is also strictly positive) + updated_bond.original_pledge.amount -= decrease_by.amount; + updated_rewarding.decrease_operator_uint128(decrease_by.amount)?; + + let owner = &node_details.bond_information.owner; + + // send the removed tokens back to the operator + let return_tokens = BankMsg::Send { + to_address: owner.to_string(), + amount: vec![decrease_by.clone()], + }; + + // update all: bond information, rewarding details and pending pledge changes + nymnodes_storage::nym_nodes().replace( + deps.storage, + node_id, + Some(&updated_bond), + Some(&node_details.bond_information), + )?; + rewards_storage::NYMNODE_REWARDING.save(deps.storage, node_id, &updated_rewarding)?; + nymnodes_storage::PENDING_NYMNODE_CHANGES.save(deps.storage, node_id, &pending_changes)?; + + let response = Response::new() + .add_message(return_tokens) + .add_event(new_pledge_decrease_event(created_at, node_id, &decrease_by)); + + Ok(response) +} + impl ContractExecutableEvent for PendingEpochEventData { fn execute(self, deps: DepsMut<'_>, env: &Env) -> Result { // note that the basic validation on all those events was already performed before @@ -303,25 +462,37 @@ impl ContractExecutableEvent for PendingEpochEventData { match self.kind { PendingEpochEventKind::Delegate { owner, - mix_id, + node_id: mix_id, amount, .. } => delegate(deps, env, self.created_at, owner, mix_id, amount), - PendingEpochEventKind::Undelegate { owner, mix_id, .. } => { - undelegate(deps, self.created_at, owner, mix_id) + PendingEpochEventKind::Undelegate { + owner, + node_id: mix_id, + .. + } => undelegate(deps, self.created_at, owner, mix_id), + PendingEpochEventKind::NymNodePledgeMore { node_id, amount } => { + increase_nym_node_pledge(deps, self.created_at, node_id, amount) } - PendingEpochEventKind::PledgeMore { mix_id, amount } => { - increase_pledge(deps, self.created_at, mix_id, amount) + PendingEpochEventKind::MixnodePledgeMore { mix_id, amount } => { + increase_mixnode_pledge(deps, self.created_at, mix_id, amount) } - PendingEpochEventKind::DecreasePledge { + PendingEpochEventKind::NymNodeDecreasePledge { + node_id, + decrease_by, + } => decrease_nym_node_pledge(deps, self.created_at, node_id, decrease_by), + PendingEpochEventKind::MixnodeDecreasePledge { mix_id, decrease_by, - } => decrease_pledge(deps, self.created_at, mix_id, decrease_by), + } => decrease_mixnode_pledge(deps, self.created_at, mix_id, decrease_by), PendingEpochEventKind::UnbondMixnode { mix_id } => { unbond_mixnode(deps, env, self.created_at, mix_id) } - PendingEpochEventKind::UpdateActiveSetSize { new_size } => { - update_active_set_size(deps, self.created_at, new_size) + PendingEpochEventKind::UnbondNymNode { node_id } => { + unbond_nym_node(deps, env, self.created_at, node_id) + } + PendingEpochEventKind::UpdateActiveSet { update } => { + update_active_set(deps, self.created_at, update) } } } @@ -330,8 +501,8 @@ impl ContractExecutableEvent for PendingEpochEventData { pub(crate) fn change_mix_cost_params( deps: DepsMut<'_>, created_at: BlockHeight, - mix_id: MixId, - new_costs: MixNodeCostParams, + mix_id: NodeId, + new_costs: NodeCostParams, ) -> Result { // almost an entire interval might have passed since the request was issued -> check if the // node still exists @@ -345,12 +516,48 @@ pub(crate) fn change_mix_cost_params( _ => return Ok(Response::default()), }; - let cosmos_event = new_mixnode_cost_params_update_event(created_at, mix_id, &new_costs); + let mut pending_changes = + mixnodes_storage::PENDING_MIXNODE_CHANGES.load(deps.storage, mix_id)?; + pending_changes.cost_params_change = None; + + let cosmos_event = new_cost_params_update_event(created_at, mix_id, &new_costs); // TODO: can we just change cost_params without breaking rewarding calculation? // (I'm almost certain we can, but well, it has to be tested) mix_rewarding.cost_params = new_costs; rewards_storage::MIXNODE_REWARDING.save(deps.storage, mix_id, &mix_rewarding)?; + mixnodes_storage::PENDING_MIXNODE_CHANGES.save(deps.storage, mix_id, &pending_changes)?; + + Ok(Response::new().add_event(cosmos_event)) +} + +pub(crate) fn change_nym_node_cost_params( + deps: DepsMut<'_>, + created_at: BlockHeight, + node_id: NodeId, + new_costs: NodeCostParams, +) -> Result { + // almost an entire interval might have passed since the request was issued -> check if the + // node still exists + // + // note: there's no check if the bond is in "unbonding" state, as epoch actions would get + // cleared before touching interval actions + let mut node_rewarding = + match rewards_storage::NYMNODE_REWARDING.may_load(deps.storage, node_id)? { + Some(node_rewarding) if node_rewarding.still_bonded() => node_rewarding, + // if node doesn't exist anymore, don't do anything, simple as that. + _ => return Ok(Response::default()), + }; + + let mut pending_changes = + nymnodes_storage::PENDING_NYMNODE_CHANGES.load(deps.storage, node_id)?; + pending_changes.cost_params_change = None; + + let cosmos_event = new_cost_params_update_event(created_at, node_id, &new_costs); + + node_rewarding.cost_params = new_costs; + rewards_storage::NYMNODE_REWARDING.save(deps.storage, node_id, &node_rewarding)?; + nymnodes_storage::PENDING_NYMNODE_CHANGES.save(deps.storage, node_id, &pending_changes)?; Ok(Response::new().add_event(cosmos_event)) } @@ -404,10 +611,12 @@ impl ContractExecutableEvent for PendingIntervalEventData { // note that the basic validation on all those events was already performed before // they were pushed onto the queue match self.kind { - PendingIntervalEventKind::ChangeMixCostParams { - mix_id: mix, - new_costs, - } => change_mix_cost_params(deps, self.created_at, mix, new_costs), + PendingIntervalEventKind::ChangeMixCostParams { mix_id, new_costs } => { + change_mix_cost_params(deps, self.created_at, mix_id, new_costs) + } + PendingIntervalEventKind::ChangeNymNodeCostParams { node_id, new_costs } => { + change_nym_node_cost_params(deps, self.created_at, node_id, new_costs) + } PendingIntervalEventKind::UpdateRewardingParams { update } => { update_rewarding_params(deps, self.created_at, update) } @@ -449,7 +658,7 @@ mod tests { #[test] fn returns_the_tokens_if_mixnode_has_unbonded() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_rewarded_legacy_mixnode("mix-owner", None); let delegation = 120_000_000u128; let delegation_coin = coin(delegation, TEST_COIN_DENOM); @@ -512,7 +721,7 @@ mod tests { #[test] fn returns_the_tokens_is_mixnode_is_unbonding() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_rewarded_legacy_mixnode("mix-owner", None); let delegation = 120_000_000u128; let delegation_coin = coin(delegation, TEST_COIN_DENOM); @@ -575,7 +784,8 @@ mod tests { #[test] fn if_delegation_already_exists_a_fresh_one_with_sum_of_both_is_created() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", Some(100_000_000_000u128.into())); + let mix_id = + test.add_rewarded_legacy_mixnode("mix-owner", Some(100_000_000_000u128.into())); let delegation_og = 120_000_000u128; let delegation_new = 543_000_000u128; @@ -619,7 +829,8 @@ mod tests { #[test] fn if_delegation_already_exists_with_unclaimed_rewards_fresh_one_is_created() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", Some(100_000_000_000u128.into())); + let mix_id = + test.add_rewarded_legacy_mixnode("mix-owner", Some(100_000_000_000u128.into())); let delegation_og = 120_000_000u128; let delegation_new = 543_000_000u128; @@ -628,12 +839,12 @@ mod tests { // perform some rewarding here to advance the unit delegation beyond the initial value test.force_change_rewarded_set(vec![mix_id]); test.skip_to_next_epoch_end(); - test.reward_with_distribution_with_state_bypass( + test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(100.0), ); test.skip_to_next_epoch_end(); - test.reward_with_distribution_with_state_bypass( + test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(100.0), ); @@ -642,12 +853,12 @@ mod tests { test.add_immediate_delegation(owner, delegation_og, mix_id); test.skip_to_next_epoch_end(); - let dist1 = test.reward_with_distribution_with_state_bypass( + let dist1 = test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(100.0), ); test.skip_to_next_epoch_end(); - let dist2 = test.reward_with_distribution_with_state_bypass( + let dist2 = test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(100.0), ); @@ -701,7 +912,8 @@ mod tests { #[test] fn appropriately_updates_state_for_fresh_delegation() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", Some(100_000_000_000u128.into())); + let mix_id = + test.add_rewarded_legacy_mixnode("mix-owner", Some(100_000_000_000u128.into())); let owner = "delegator"; let delegation = 120_000_000u128; @@ -710,12 +922,12 @@ mod tests { // perform some rewarding here to advance the unit delegation beyond the initial value test.force_change_rewarded_set(vec![mix_id]); test.skip_to_next_epoch_end(); - test.reward_with_distribution_with_state_bypass( + test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(100.0), ); test.skip_to_next_epoch_end(); - test.reward_with_distribution_with_state_bypass( + test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(100.0), ); @@ -766,7 +978,7 @@ mod tests { #[test] fn doesnt_return_any_tokens_if_it_doesnt_exist() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_rewarded_legacy_mixnode("mix-owner", None); let owner = Addr::unchecked("delegator"); @@ -777,7 +989,7 @@ mod tests { #[test] fn errors_out_if_mix_rewarding_doesnt_exist() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_rewarded_legacy_mixnode("mix-owner", None); let owner = Addr::unchecked("delegator"); test.add_immediate_delegation(owner.as_str(), 100_000_000u32, mix_id); @@ -795,36 +1007,27 @@ mod tests { #[test] fn returns_all_delegated_tokens_with_earned_rewards() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", Some(100_000_000_000u128.into())); + let mix_id = + test.add_rewarded_legacy_mixnode("mix-owner", Some(100_000_000_000u128.into())); let owner = "delegator"; let delegation = 120_000_000u128; + let active_params = test.active_node_params(100.0); + // perform some rewarding here to advance the unit delegation beyond the initial value test.force_change_rewarded_set(vec![mix_id]); test.skip_to_next_epoch_end(); - test.reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + test.reward_with_distribution_ignore_state(mix_id, active_params); test.skip_to_next_epoch_end(); - test.reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + test.reward_with_distribution_ignore_state(mix_id, active_params); test.add_immediate_delegation(owner, delegation, mix_id); test.skip_to_next_epoch_end(); - let dist1 = test.reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + let dist1 = test.reward_with_distribution_ignore_state(mix_id, active_params); test.skip_to_next_epoch_end(); - let dist2 = test.reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + let dist2 = test.reward_with_distribution_ignore_state(mix_id, active_params); let expected_reward = dist1.delegates + dist2.delegates; let truncated_reward = truncate_reward_amount(expected_reward); @@ -853,12 +1056,19 @@ mod tests { #[cfg(test)] mod mixnode_unbonding { - use super::*; + use crate::compat::transactions::{try_decrease_pledge, try_increase_pledge}; + use crate::interval::pending_events::unbond_mixnode; use crate::mixnodes::storage as mixnodes_storage; - use crate::mixnodes::transactions::{try_decrease_pledge, try_increase_pledge}; - use crate::support::tests::test_helpers::get_bank_send_msg; + use crate::mixnodes::transactions::{ + try_decrease_mixnode_pledge, try_increase_mixnode_pledge, + }; + use crate::rewards::storage as rewards_storage; + use crate::support::tests::fixtures::TEST_COIN_DENOM; + use crate::support::tests::test_helpers; + use crate::support::tests::test_helpers::{get_bank_send_msg, TestSetup}; use cosmwasm_std::testing::mock_info; - use cosmwasm_std::Uint128; + use cosmwasm_std::{coin, to_binary, Addr, CosmosMsg, Uint128, WasmMsg}; + use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::mixnode::{PendingMixNodeChanges, UnbondedMixnode}; use mixnet_contract_common::rewarding::helpers::truncate_reward_amount; @@ -884,14 +1094,9 @@ mod tests { // increase let owner = "mix-owner1"; let pledge = Uint128::new(250_000_000); - let mix_id = test.add_dummy_mixnode(owner, Some(pledge)); + let mix_id = test.add_rewarded_legacy_mixnode(owner, Some(pledge)); - try_increase_pledge( - test.deps_mut(), - env.clone(), - mock_info(owner, &change.clone()), - ) - .unwrap(); + try_increase_pledge(test.deps_mut(), env.clone(), mock_info(owner, &change)).unwrap(); let res = unbond_mixnode(test.deps_mut(), &env, 123, mix_id); assert!(matches!( @@ -902,7 +1107,7 @@ mod tests { // decrease let owner = "mix-owner2"; let pledge = Uint128::new(250_000_000); - let mix_id = test.add_dummy_mixnode(owner, Some(pledge)); + let mix_id = test.add_rewarded_legacy_mixnode(owner, Some(pledge)); try_decrease_pledge( test.deps_mut(), @@ -921,10 +1126,11 @@ mod tests { // artificial let owner = "mix-owner3"; let pledge = Uint128::new(250_000_000); - let mix_id = test.add_dummy_mixnode(owner, Some(pledge)); + let mix_id = test.add_rewarded_legacy_mixnode(owner, Some(pledge)); let changes = PendingMixNodeChanges { pledge_change: Some(1234), + cost_params_change: None, }; mixnodes_storage::PENDING_MIXNODE_CHANGES @@ -943,20 +1149,19 @@ mod tests { let owner = "mix-owner"; let pledge = Uint128::new(250_000_000); - let mix_id = test.add_dummy_mixnode(owner, Some(pledge)); + let mix_id = test.add_rewarded_legacy_mixnode(owner, Some(pledge)); let mix_details = mixnodes_storage::mixnode_bonds() .load(test.deps().storage, mix_id) .unwrap(); - let layer = mix_details.layer; test.force_change_rewarded_set(vec![mix_id]); test.skip_to_next_epoch_end(); - let dist1 = test.reward_with_distribution_with_state_bypass( + let dist1 = test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(100.0), ); test.skip_to_next_epoch_end(); - let dist2 = test.reward_with_distribution_with_state_bypass( + let dist2 = test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(100.0), ); @@ -991,10 +1196,6 @@ mod tests { .load(test.deps().storage, mix_id) .unwrap() ); - assert_eq!( - mixnodes_storage::LAYERS.load(test.deps().storage).unwrap()[layer], - 0 - ) } } @@ -1012,7 +1213,7 @@ mod tests { let mut test = TestSetup::new(); let amount = test.coin(123); - let res = increase_pledge(test.deps_mut(), 123, 1, amount); + let res = increase_mixnode_pledge(test.deps_mut(), 123, 1, amount); assert!(matches!( res, Err(MixnetContractError::InconsistentState { .. }) @@ -1026,9 +1227,9 @@ mod tests { let owner = "mix-owner"; let pledge = Uint128::new(250_000_000); - let mix_id = test.add_dummy_mixnode(owner, Some(pledge)); + let mix_id = test.add_rewarded_legacy_mixnode(owner, Some(pledge)); - let res = increase_pledge(test.deps_mut(), 123, mix_id, change); + let res = increase_mixnode_pledge(test.deps_mut(), 123, mix_id, change); assert!(matches!( res, Err(MixnetContractError::InconsistentState { .. }) @@ -1038,7 +1239,7 @@ mod tests { #[test] fn updates_stored_bond_information_and_rewarding_details() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_rewarded_legacy_mixnode("mix-owner", None); test.set_pending_pledge_change(mix_id, None); let old_details = get_mixnode_details_by_id(test.deps().storage, mix_id) @@ -1046,7 +1247,7 @@ mod tests { .unwrap(); let amount = test.coin(12345); - increase_pledge(test.deps_mut(), 123, mix_id, amount.clone()).unwrap(); + increase_mixnode_pledge(test.deps_mut(), 123, mix_id, amount.clone()).unwrap(); let updated_details = get_mixnode_details_by_id(test.deps().storage, mix_id) .unwrap() @@ -1071,13 +1272,13 @@ mod tests { let pledge2 = Uint128::new(50_000_000); let pledge3 = Uint128::new(200_000_000); - let mix_id_repledge = test.add_dummy_mixnode("mix-owner1", Some(pledge1)); + let mix_id_repledge = test.add_rewarded_legacy_mixnode("mix-owner1", Some(pledge1)); test.set_pending_pledge_change(mix_id_repledge, None); let increase = test.coin(pledge2.u128()); - increase_pledge(test.deps_mut(), 123, mix_id_repledge, increase).unwrap(); + increase_mixnode_pledge(test.deps_mut(), 123, mix_id_repledge, increase).unwrap(); - let mix_id_full_pledge = test.add_dummy_mixnode("mix-owner2", Some(pledge3)); + let mix_id_full_pledge = test.add_rewarded_legacy_mixnode("mix-owner2", Some(pledge3)); test.add_immediate_delegation("alice", 123_456_789u128, mix_id_repledge); test.add_immediate_delegation("bob", 500_000_000u128, mix_id_repledge); @@ -1090,11 +1291,11 @@ mod tests { test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![mix_id_repledge, mix_id_full_pledge]); - let dist1 = test.reward_with_distribution_with_state_bypass( + let dist1 = test.legacy_reward_with_distribution_with_state_bypass( mix_id_repledge, test_helpers::performance(100.0), ); - let dist2 = test.reward_with_distribution_with_state_bypass( + let dist2 = test.legacy_reward_with_distribution_with_state_bypass( mix_id_full_pledge, test_helpers::performance(100.0), ); @@ -1108,7 +1309,7 @@ mod tests { let pledge1 = Uint128::new(150_000_000_000); let pledge2 = Uint128::new(50_000_000_000); - let mix_id_repledge = test.add_dummy_mixnode("mix-owner1", Some(pledge1)); + let mix_id_repledge = test.add_rewarded_legacy_mixnode("mix-owner1", Some(pledge1)); test.set_pending_pledge_change(mix_id_repledge, None); test.add_immediate_delegation("alice", 123_456_789_000u128, mix_id_repledge); @@ -1118,16 +1319,16 @@ mod tests { test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![mix_id_repledge]); - let dist = test.reward_with_distribution_with_state_bypass( + let dist = test.legacy_reward_with_distribution_with_state_bypass( mix_id_repledge, test_helpers::performance(100.0), ); let increase = test.coin(pledge2.u128()); - increase_pledge(test.deps_mut(), 123, mix_id_repledge, increase).unwrap(); + increase_mixnode_pledge(test.deps_mut(), 123, mix_id_repledge, increase).unwrap(); let pledge3 = Uint128::new(200_000_000_000) + truncate_reward_amount(dist.operator); - let mix_id_full_pledge = test.add_dummy_mixnode("mix-owner2", Some(pledge3)); + let mix_id_full_pledge = test.add_rewarded_legacy_mixnode("mix-owner2", Some(pledge3)); test.add_immediate_delegation("alice", 123_456_789_000u128, mix_id_full_pledge); test.add_immediate_delegation("bob", 500_000_000_000u128, mix_id_full_pledge); @@ -1162,11 +1363,11 @@ mod tests { // go through few epochs of rewarding for _ in 0..500 { test.skip_to_next_epoch_end(); - let dist1 = test.reward_with_distribution_with_state_bypass( + let dist1 = test.legacy_reward_with_distribution_with_state_bypass( mix_id_repledge, test_helpers::performance(100.0), ); - let dist2 = test.reward_with_distribution_with_state_bypass( + let dist2 = test.legacy_reward_with_distribution_with_state_bypass( mix_id_full_pledge, test_helpers::performance(100.0), ); @@ -1181,7 +1382,7 @@ mod tests { let pledge1 = Uint128::new(150_000_000_000); let pledge2 = Uint128::new(50_000_000_000); - let mix_id_repledge = test.add_dummy_mixnode("mix-owner1", Some(pledge1)); + let mix_id_repledge = test.add_rewarded_legacy_mixnode("mix-owner1", Some(pledge1)); test.set_pending_pledge_change(mix_id_repledge, None); test.add_immediate_delegation("alice", 123_456_789_000u128, mix_id_repledge); @@ -1197,7 +1398,7 @@ mod tests { // go few epochs of rewarding before adding more pledge for _ in 0..500 { test.skip_to_next_epoch_end(); - let dist = test.reward_with_distribution_with_state_bypass( + let dist = test.legacy_reward_with_distribution_with_state_bypass( mix_id_repledge, test_helpers::performance(100.0), ); @@ -1206,11 +1407,11 @@ mod tests { } let increase = test.coin(pledge2.u128()); - increase_pledge(test.deps_mut(), 123, mix_id_repledge, increase).unwrap(); + increase_mixnode_pledge(test.deps_mut(), 123, mix_id_repledge, increase).unwrap(); let pledge3 = Uint128::new(200_000_000_000) + truncate_reward_amount(cumulative_op_reward); - let mix_id_full_pledge = test.add_dummy_mixnode("mix-owner2", Some(pledge3)); + let mix_id_full_pledge = test.add_rewarded_legacy_mixnode("mix-owner2", Some(pledge3)); test.add_immediate_delegation("alice", 123_456_789_000u128, mix_id_full_pledge); test.add_immediate_delegation("bob", 500_000_000_000u128, mix_id_full_pledge); @@ -1245,11 +1446,11 @@ mod tests { // go through few more epochs of rewarding for _ in 0..500 { test.skip_to_next_epoch_end(); - let dist1 = test.reward_with_distribution_with_state_bypass( + let dist1 = test.legacy_reward_with_distribution_with_state_bypass( mix_id_repledge, test_helpers::performance(100.0), ); - let dist2 = test.reward_with_distribution_with_state_bypass( + let dist2 = test.legacy_reward_with_distribution_with_state_bypass( mix_id_full_pledge, test_helpers::performance(100.0), ); @@ -1261,11 +1462,11 @@ mod tests { #[test] fn updates_the_pending_pledge_changes_field() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_rewarded_legacy_mixnode("mix-owner", None); test.set_pending_pledge_change(mix_id, None); let amount = test.coin(12345); - increase_pledge(test.deps_mut(), 123, mix_id, amount).unwrap(); + increase_mixnode_pledge(test.deps_mut(), 123, mix_id, amount).unwrap(); let pending = mixnodes_storage::PENDING_MIXNODE_CHANGES .load(test.deps().storage, mix_id) .unwrap(); @@ -1285,7 +1486,7 @@ mod tests { let mut test = TestSetup::new(); let amount = test.coin(123); - let res = decrease_pledge(test.deps_mut(), 123, 1, amount); + let res = decrease_mixnode_pledge(test.deps_mut(), 123, 1, amount); assert!(matches!( res, Err(MixnetContractError::InconsistentState { .. }) @@ -1299,9 +1500,9 @@ mod tests { let owner = "mix-owner"; let pledge = Uint128::new(250_000_000); - let mix_id = test.add_dummy_mixnode(owner, Some(pledge)); + let mix_id = test.add_rewarded_legacy_mixnode(owner, Some(pledge)); - let res = decrease_pledge(test.deps_mut(), 123, mix_id, change); + let res = decrease_mixnode_pledge(test.deps_mut(), 123, mix_id, change); assert!(matches!( res, Err(MixnetContractError::InconsistentState { .. }) @@ -1311,7 +1512,7 @@ mod tests { #[test] fn updates_stored_bond_information_and_rewarding_details() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_rewarded_legacy_mixnode("mix-owner", None); test.set_pending_pledge_change(mix_id, None); let old_details = get_mixnode_details_by_id(test.deps().storage, mix_id) @@ -1319,7 +1520,7 @@ mod tests { .unwrap(); let amount = test.coin(12345); - decrease_pledge(test.deps_mut(), 123, mix_id, amount.clone()).unwrap(); + decrease_mixnode_pledge(test.deps_mut(), 123, mix_id, amount.clone()).unwrap(); let updated_details = get_mixnode_details_by_id(test.deps().storage, mix_id) .unwrap() @@ -1341,11 +1542,12 @@ mod tests { fn returns_tokens_back_to_the_owner() { let mut test = TestSetup::new(); let owner = "mix-owner"; - let mix_id = test.add_dummy_mixnode(owner, None); + let mix_id = test.add_rewarded_legacy_mixnode(owner, None); test.set_pending_pledge_change(mix_id, None); let amount = test.coin(12345); - let res = decrease_pledge(test.deps_mut(), 123, mix_id, amount.clone()).unwrap(); + let res = + decrease_mixnode_pledge(test.deps_mut(), 123, mix_id, amount.clone()).unwrap(); assert_eq!(res.messages.len(), 1); assert_eq!( @@ -1364,13 +1566,13 @@ mod tests { let pledge_change = Uint128::new(50_000_000); let pledge3 = Uint128::new(150_000_000); - let mix_id_repledge = test.add_dummy_mixnode("mix-owner1", Some(pledge1)); + let mix_id_repledge = test.add_rewarded_legacy_mixnode("mix-owner1", Some(pledge1)); test.set_pending_pledge_change(mix_id_repledge, None); let decrease = test.coin(pledge_change.u128()); - decrease_pledge(test.deps_mut(), 123, mix_id_repledge, decrease).unwrap(); + decrease_mixnode_pledge(test.deps_mut(), 123, mix_id_repledge, decrease).unwrap(); - let mix_id_full_pledge = test.add_dummy_mixnode("mix-owner2", Some(pledge3)); + let mix_id_full_pledge = test.add_rewarded_legacy_mixnode("mix-owner2", Some(pledge3)); test.add_immediate_delegation("alice", 123_456_789u128, mix_id_repledge); test.add_immediate_delegation("bob", 500_000_000u128, mix_id_repledge); @@ -1383,11 +1585,11 @@ mod tests { test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![mix_id_repledge, mix_id_full_pledge]); - let dist1 = test.reward_with_distribution_with_state_bypass( + let dist1 = test.legacy_reward_with_distribution_with_state_bypass( mix_id_repledge, test_helpers::performance(100.0), ); - let dist2 = test.reward_with_distribution_with_state_bypass( + let dist2 = test.legacy_reward_with_distribution_with_state_bypass( mix_id_full_pledge, test_helpers::performance(100.0), ); @@ -1401,7 +1603,7 @@ mod tests { let pledge1 = Uint128::new(200_000_000_000); let pledge_change = Uint128::new(50_000_000_000); - let mix_id_repledge = test.add_dummy_mixnode("mix-owner1", Some(pledge1)); + let mix_id_repledge = test.add_rewarded_legacy_mixnode("mix-owner1", Some(pledge1)); test.set_pending_pledge_change(mix_id_repledge, None); test.add_immediate_delegation("alice", 123_456_789_000u128, mix_id_repledge); @@ -1411,16 +1613,16 @@ mod tests { test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![mix_id_repledge]); - let dist = test.reward_with_distribution_with_state_bypass( + let dist = test.legacy_reward_with_distribution_with_state_bypass( mix_id_repledge, test_helpers::performance(100.0), ); let decrease = test.coin(pledge_change.u128()); - decrease_pledge(test.deps_mut(), 123, mix_id_repledge, decrease).unwrap(); + decrease_mixnode_pledge(test.deps_mut(), 123, mix_id_repledge, decrease).unwrap(); let pledge3 = Uint128::new(150_000_000_000) + truncate_reward_amount(dist.operator); - let mix_id_full_pledge = test.add_dummy_mixnode("mix-owner2", Some(pledge3)); + let mix_id_full_pledge = test.add_rewarded_legacy_mixnode("mix-owner2", Some(pledge3)); test.add_immediate_delegation("alice", 123_456_789_000u128, mix_id_full_pledge); test.add_immediate_delegation("bob", 500_000_000_000u128, mix_id_full_pledge); @@ -1455,11 +1657,11 @@ mod tests { // go through few epochs of rewarding for _ in 0..500 { test.skip_to_next_epoch_end(); - let dist1 = test.reward_with_distribution_with_state_bypass( + let dist1 = test.legacy_reward_with_distribution_with_state_bypass( mix_id_repledge, test_helpers::performance(100.0), ); - let dist2 = test.reward_with_distribution_with_state_bypass( + let dist2 = test.legacy_reward_with_distribution_with_state_bypass( mix_id_full_pledge, test_helpers::performance(100.0), ); @@ -1474,7 +1676,7 @@ mod tests { let pledge1 = Uint128::new(200_000_000_000); let pledge_change = Uint128::new(50_000_000_000); - let mix_id_repledge = test.add_dummy_mixnode("mix-owner1", Some(pledge1)); + let mix_id_repledge = test.add_rewarded_legacy_mixnode("mix-owner1", Some(pledge1)); test.set_pending_pledge_change(mix_id_repledge, None); test.add_immediate_delegation("alice", 123_456_789_000u128, mix_id_repledge); @@ -1490,7 +1692,7 @@ mod tests { // go few epochs of rewarding before decreasing pledge for _ in 0..500 { test.skip_to_next_epoch_end(); - let dist = test.reward_with_distribution_with_state_bypass( + let dist = test.legacy_reward_with_distribution_with_state_bypass( mix_id_repledge, test_helpers::performance(100.0), ); @@ -1499,11 +1701,11 @@ mod tests { } let decrease = test.coin(pledge_change.u128()); - decrease_pledge(test.deps_mut(), 123, mix_id_repledge, decrease).unwrap(); + decrease_mixnode_pledge(test.deps_mut(), 123, mix_id_repledge, decrease).unwrap(); let pledge3 = Uint128::new(150_000_000_000) + truncate_reward_amount(cumulative_op_reward); - let mix_id_full_pledge = test.add_dummy_mixnode("mix-owner2", Some(pledge3)); + let mix_id_full_pledge = test.add_rewarded_legacy_mixnode("mix-owner2", Some(pledge3)); test.add_immediate_delegation("alice", 123_456_789_000u128, mix_id_full_pledge); test.add_immediate_delegation("bob", 500_000_000_000u128, mix_id_full_pledge); @@ -1538,11 +1740,11 @@ mod tests { // go through few more epochs of rewarding for _ in 0..500 { test.skip_to_next_epoch_end(); - let dist1 = test.reward_with_distribution_with_state_bypass( + let dist1 = test.legacy_reward_with_distribution_with_state_bypass( mix_id_repledge, test_helpers::performance(100.0), ); - let dist2 = test.reward_with_distribution_with_state_bypass( + let dist2 = test.legacy_reward_with_distribution_with_state_bypass( mix_id_full_pledge, test_helpers::performance(100.0), ); @@ -1554,11 +1756,11 @@ mod tests { #[test] fn updates_the_pending_pledge_changes_field() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_rewarded_legacy_mixnode("mix-owner", None); test.set_pending_pledge_change(mix_id, None); let amount = test.coin(12345); - decrease_pledge(test.deps_mut(), 123, mix_id, amount).unwrap(); + decrease_mixnode_pledge(test.deps_mut(), 123, mix_id, amount).unwrap(); let pending = mixnodes_storage::PENDING_MIXNODE_CHANGES .load(test.deps().storage, mix_id) .unwrap(); @@ -1573,12 +1775,23 @@ mod tests { .load(test.deps().storage) .unwrap(); - update_active_set_size(test.deps_mut(), 123, 50).unwrap(); + update_active_set( + test.deps_mut(), + 123, + ActiveSetUpdate { + entry_gateways: 50, + exit_gateways: 50, + mixnodes: 100, + }, + ) + .unwrap(); let updated = rewards_storage::REWARDING_PARAMS .load(test.deps().storage) .unwrap(); - assert_ne!(current.active_set_size, updated.active_set_size); - assert_eq!(updated.active_set_size, 50) + assert_ne!(updated.rewarded_set, current.rewarded_set); + assert_eq!(updated.rewarded_set.mixnodes, 100); + assert_eq!(updated.rewarded_set.entry_gateways, 50); + assert_eq!(updated.rewarded_set.exit_gateways, 50); } #[cfg(test)] @@ -1591,12 +1804,12 @@ mod tests { #[test] fn doesnt_do_anything_if_mixnode_has_unbonded() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_rewarded_legacy_mixnode("mix-owner", None); let env = test.env(); unbond_mixnode(test.deps_mut(), &env, 123, mix_id).unwrap(); - let new_params = MixNodeCostParams { + let new_params = NodeCostParams { profit_margin_percent: Percent::from_percentage_value(42).unwrap(), interval_operating_cost: coin(123_456_789, TEST_COIN_DENOM), }; @@ -1608,11 +1821,16 @@ mod tests { #[test] fn for_bonded_mixnode_updates_saved_value() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_rewarded_legacy_mixnode("mix-owner", None); let before = test.mix_rewarding(mix_id).cost_params; - let new_params = MixNodeCostParams { + // this would have been normally populated when creating the event itself + mixnodes_storage::PENDING_MIXNODE_CHANGES + .save(test.deps_mut().storage, mix_id, &Default::default()) + .unwrap(); + + let new_params = NodeCostParams { profit_margin_percent: Percent::from_percentage_value(42).unwrap(), interval_operating_cost: coin(123_456_789, TEST_COIN_DENOM), }; @@ -1620,13 +1838,11 @@ mod tests { let res = change_mix_cost_params(test.deps_mut(), 123, mix_id, new_params.clone()); assert_eq!( res, - Ok( - Response::new().add_event(new_mixnode_cost_params_update_event( - 123, - mix_id, - &new_params, - )) - ) + Ok(Response::new().add_event(new_cost_params_update_event( + 123, + mix_id, + &new_params, + ))) ); let after = test.mix_rewarding(mix_id).cost_params; @@ -1655,7 +1871,7 @@ mod tests { sybil_resistance_percent: Some(Percent::from_percentage_value(42).unwrap()), active_set_work_factor: None, interval_pool_emission: None, - rewarded_set_size: None, + rewarded_set_params: None, }; let res = update_rewarding_params(test.deps_mut(), 123, update); diff --git a/contracts/mixnet/src/interval/queries.rs b/contracts/mixnet/src/interval/queries.rs index 423d9a90ae..5649d5bb83 100644 --- a/contracts/mixnet/src/interval/queries.rs +++ b/contracts/mixnet/src/interval/queries.rs @@ -4,7 +4,6 @@ use crate::constants::{ EPOCH_EVENTS_DEFAULT_RETRIEVAL_LIMIT, EPOCH_EVENTS_MAX_RETRIEVAL_LIMIT, INTERVAL_EVENTS_DEFAULT_RETRIEVAL_LIMIT, INTERVAL_EVENTS_MAX_RETRIEVAL_LIMIT, - REWARDED_SET_DEFAULT_RETRIEVAL_LIMIT, REWARDED_SET_MAX_RETRIEVAL_LIMIT, }; use crate::interval::storage; use cosmwasm_std::{Deps, Env, Order, StdResult}; @@ -12,9 +11,9 @@ use cw_storage_plus::Bound; use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::pending_events::{PendingEpochEvent, PendingIntervalEvent}; use mixnet_contract_common::{ - CurrentIntervalResponse, EpochEventId, EpochStatus, IntervalEventId, MixId, - NumberOfPendingEventsResponse, PagedRewardedSetResponse, PendingEpochEventResponse, - PendingEpochEventsResponse, PendingIntervalEventResponse, PendingIntervalEventsResponse, + CurrentIntervalResponse, EpochEventId, EpochStatus, IntervalEventId, + NumberOfPendingEventsResponse, PendingEpochEventResponse, PendingEpochEventsResponse, + PendingIntervalEventResponse, PendingIntervalEventsResponse, }; pub fn query_epoch_status(deps: Deps<'_>) -> StdResult { @@ -30,30 +29,6 @@ pub fn query_current_interval_details( Ok(CurrentIntervalResponse::new(interval, env)) } -pub fn query_rewarded_set_paged( - deps: Deps<'_>, - start_after: Option, - limit: Option, -) -> StdResult { - let limit = limit - .unwrap_or(REWARDED_SET_DEFAULT_RETRIEVAL_LIMIT) - .min(REWARDED_SET_MAX_RETRIEVAL_LIMIT) as usize; - - let start = start_after.map(Bound::exclusive); - - let nodes = storage::REWARDED_SET - .range(deps.storage, start, None, Order::Ascending) - .take(limit) - .collect::>>()?; - - let start_next_after = nodes.last().map(|node| node.0); - - Ok(PagedRewardedSetResponse { - nodes, - start_next_after, - }) -} - pub fn query_pending_epoch_events_paged( deps: Deps<'_>, env: Env, @@ -174,7 +149,7 @@ mod tests { fn push_dummy_interval_action(test: &mut TestSetup) { let dummy_action = PendingIntervalEventKind::ChangeMixCostParams { mix_id: test.rng.next_u32(), - new_costs: fixtures::mix_node_cost_params_fixture(), + new_costs: fixtures::node_cost_params_fixture(), }; let env = test.env(); storage::push_new_interval_event(test.deps_mut().storage, &env, dummy_action).unwrap(); @@ -214,95 +189,6 @@ mod tests { assert_eq!(res.current_blocktime, env.block.time.seconds()); } - #[cfg(test)] - mod rewarded_set { - use super::*; - - fn set_rewarded_set_to_n_nodes(test: &mut TestSetup, n: usize) { - let set = (1u32..).take(n).collect::>(); - test.force_change_rewarded_set(set) - } - - #[test] - fn obeys_limits() { - let mut test = TestSetup::new(); - set_rewarded_set_to_n_nodes(&mut test, 200); - - let limit = 2; - let page1 = query_rewarded_set_paged(test.deps(), None, Some(limit)).unwrap(); - assert_eq!(limit, page1.nodes.len() as u32); - } - - #[test] - fn has_default_limit() { - let mut test = TestSetup::new(); - set_rewarded_set_to_n_nodes(&mut test, 2000); - - // query without explicitly setting a limit - let page1 = query_rewarded_set_paged(test.deps(), None, None).unwrap(); - - assert_eq!( - REWARDED_SET_DEFAULT_RETRIEVAL_LIMIT, - page1.nodes.len() as u32 - ); - } - - #[test] - fn has_max_limit() { - let mut test = TestSetup::new(); - set_rewarded_set_to_n_nodes(&mut test, 2000); - - // query with a crazily high limit in an attempt to use too many resources - let crazy_limit = 10000; - let page1 = query_rewarded_set_paged(test.deps(), None, Some(crazy_limit)).unwrap(); - - assert_eq!(REWARDED_SET_MAX_RETRIEVAL_LIMIT, page1.nodes.len() as u32); - } - - #[test] - fn pagination_works() { - let mut test = TestSetup::new(); - - set_rewarded_set_to_n_nodes(&mut test, 1); - - let per_page = 2; - let page1 = query_rewarded_set_paged(test.deps(), None, Some(per_page)).unwrap(); - - // page should have 1 result on it - assert_eq!(1, page1.nodes.len()); - - set_rewarded_set_to_n_nodes(&mut test, 2); - - // page1 should have 2 results on it - let page1 = query_rewarded_set_paged(test.deps(), None, Some(per_page)).unwrap(); - assert_eq!(2, page1.nodes.len()); - - set_rewarded_set_to_n_nodes(&mut test, 3); - - // page1 still has the same 2 results - let another_page1 = - query_rewarded_set_paged(test.deps(), None, Some(per_page)).unwrap(); - assert_eq!(2, another_page1.nodes.len()); - assert_eq!(page1, another_page1); - - // retrieving the next page should start after the last key on this page - let start_after = page1.start_next_after.unwrap(); - let page2 = - query_rewarded_set_paged(test.deps(), Some(start_after), Some(per_page)).unwrap(); - - assert_eq!(1, page2.nodes.len()); - - // save another one - set_rewarded_set_to_n_nodes(&mut test, 4); - - let page2 = - query_rewarded_set_paged(test.deps(), Some(start_after), Some(per_page)).unwrap(); - - // now we have 2 pages, with 2 results on the second page - assert_eq!(2, page2.nodes.len()); - } - } - #[cfg(test)] mod pending_epoch_events { use super::*; @@ -605,7 +491,7 @@ mod tests { // it exists let dummy_action = PendingIntervalEventKind::ChangeMixCostParams { mix_id: test.rng.next_u32(), - new_costs: fixtures::mix_node_cost_params_fixture(), + new_costs: fixtures::node_cost_params_fixture(), }; let env = test.env(); storage::push_new_interval_event(test.deps_mut().storage, &env, dummy_action.clone()) diff --git a/contracts/mixnet/src/interval/storage.rs b/contracts/mixnet/src/interval/storage.rs index 33eddd4d4e..2a359cd43f 100644 --- a/contracts/mixnet/src/interval/storage.rs +++ b/contracts/mixnet/src/interval/storage.rs @@ -4,22 +4,19 @@ use crate::constants::{ CURRENT_EPOCH_STATUS_KEY, CURRENT_INTERVAL_KEY, EPOCH_EVENT_ID_COUNTER_KEY, INTERVAL_EVENT_ID_COUNTER_KEY, LAST_EPOCH_EVENT_ID_KEY, LAST_INTERVAL_EVENT_ID_KEY, - PENDING_EPOCH_EVENTS_NAMESPACE, PENDING_INTERVAL_EVENTS_NAMESPACE, REWARDED_SET_KEY, + PENDING_EPOCH_EVENTS_NAMESPACE, PENDING_INTERVAL_EVENTS_NAMESPACE, }; -use cosmwasm_std::{Addr, Env, Order, StdResult, Storage}; +use cosmwasm_std::{Addr, Env, StdResult, Storage}; use cw_storage_plus::{Item, Map}; use mixnet_contract_common::pending_events::{ PendingEpochEventData, PendingEpochEventKind, PendingIntervalEventData, }; use mixnet_contract_common::{ - EpochEventId, EpochStatus, Interval, IntervalEventId, MixId, PendingIntervalEventKind, - RewardedSetNodeStatus, + EpochEventId, EpochStatus, Interval, IntervalEventId, PendingIntervalEventKind, }; -use std::collections::HashMap; pub(crate) const CURRENT_EPOCH_STATUS: Item<'_, EpochStatus> = Item::new(CURRENT_EPOCH_STATUS_KEY); pub(crate) const CURRENT_INTERVAL: Item<'_, Interval> = Item::new(CURRENT_INTERVAL_KEY); -pub(crate) const REWARDED_SET: Map = Map::new(REWARDED_SET_KEY); pub(crate) const EPOCH_EVENT_ID_COUNTER: Item = Item::new(EPOCH_EVENT_ID_COUNTER_KEY); pub(crate) const INTERVAL_EVENT_ID_COUNTER: Item = @@ -111,49 +108,6 @@ pub(crate) fn push_new_interval_event( Ok(event_id) } -pub(crate) fn update_rewarded_set( - storage: &mut dyn Storage, - active_set_size: u32, - new_set: Vec, -) -> StdResult<()> { - // our goal is to reduce the number of reads and writes to the underlying storage, - // whilst completely overwriting the current rewarded set. - // the naive implementation would be to read the entire current rewarded set, - // remove all of those entries - // and write the new one in its place. - // However, very often it might turn out that a node hasn't changed its status in the updated epoch, - // and in those cases we can save on having to remove the entry and writing a new one. - - // Note: so far it seems the contract compiles (and stores) fine with a `HashMap`, but if we ever - // run into any issues due to any randomness? we can switch it up for a BTreeMap - let mut old_nodes = REWARDED_SET - .range(storage, None, None, Order::Ascending) - .collect::, _>>()?; - - for (i, node_id) in new_set.into_iter().enumerate() { - // first k nodes are active - let set_status = if i < active_set_size as usize { - RewardedSetNodeStatus::Active - } else { - RewardedSetNodeStatus::Standby - }; - - if !matches!(old_nodes.get(&node_id), Some(status) if status == &set_status) { - // if the status changed, or didn't exist, write it down: - REWARDED_SET.save(storage, node_id, &set_status)?; - } - - old_nodes.remove(&node_id); - } - - // finally remove the entries for nodes that no longer exist [in the rewarded set] - for old_node_id in old_nodes.keys() { - REWARDED_SET.remove(storage, *old_node_id) - } - - Ok(()) -} - pub(crate) fn initialise_storage( storage: &mut dyn Storage, starting_interval: Interval, @@ -172,50 +126,8 @@ mod tests { use super::*; use crate::support::tests::fixtures; use crate::support::tests::test_helpers::TestSetup; - use cosmwasm_std::testing::mock_dependencies; use rand_chacha::rand_core::RngCore; - fn read_entire_set(storage: &dyn Storage) -> HashMap { - REWARDED_SET - .range(storage, None, None, Order::Ascending) - .map(|r| r.unwrap()) - .collect() - } - - #[test] - fn updating_rewarded_set() { - // just some variables to keep test assertions more concise - let active = &RewardedSetNodeStatus::Active; - let standby = &RewardedSetNodeStatus::Standby; - - let mut deps = mock_dependencies(); - let store = deps.as_mut().storage; - assert!(read_entire_set(store).is_empty()); - - // writing initial rewarded set shouldn't do anything fancy - update_rewarded_set(store, 2, vec![6, 2, 7, 4, 1]).unwrap(); - let current_set = read_entire_set(store); - assert_eq!(current_set.len(), 5); - assert_eq!(active, current_set.get(&6).unwrap()); - assert_eq!(active, current_set.get(&2).unwrap()); - assert_eq!(standby, current_set.get(&7).unwrap()); - assert_eq!(standby, current_set.get(&4).unwrap()); - assert_eq!(standby, current_set.get(&1).unwrap()); - assert!(!current_set.contains_key(&42)); - - update_rewarded_set(store, 2, vec![2, 5, 6, 3, 4]).unwrap(); - let current_set = read_entire_set(store); - assert_eq!(current_set.len(), 5); - assert_eq!(active, current_set.get(&2).unwrap()); - assert_eq!(active, current_set.get(&5).unwrap()); - assert_eq!(standby, current_set.get(&6).unwrap()); - assert_eq!(standby, current_set.get(&3).unwrap()); - assert_eq!(standby, current_set.get(&4).unwrap()); - // those no longer are in the rewarded set - assert!(!current_set.contains_key(&7)); - assert!(!current_set.contains_key(&1)); - } - #[test] fn pushing_new_epoch_event_returns_its_id() { let mut test = TestSetup::new(); @@ -252,7 +164,7 @@ mod tests { for _ in 0..500 { let dummy_action = PendingIntervalEventKind::ChangeMixCostParams { mix_id: test.rng.next_u32(), - new_costs: fixtures::mix_node_cost_params_fixture(), + new_costs: fixtures::node_cost_params_fixture(), }; let id = push_new_interval_event(test.deps_mut().storage, &env, dummy_action).unwrap(); let expected = INTERVAL_EVENT_ID_COUNTER.load(test.deps().storage).unwrap(); @@ -264,7 +176,7 @@ mod tests { for _ in 0..10 { let dummy_action = PendingIntervalEventKind::ChangeMixCostParams { mix_id: test.rng.next_u32(), - new_costs: fixtures::mix_node_cost_params_fixture(), + new_costs: fixtures::node_cost_params_fixture(), }; let id = push_new_interval_event(test.deps_mut().storage, &env, dummy_action).unwrap(); let expected = INTERVAL_EVENT_ID_COUNTER.load(test.deps().storage).unwrap(); diff --git a/contracts/mixnet/src/interval/transactions.rs b/contracts/mixnet/src/interval/transactions.rs index adc8faa9cc..ca4c7c0563 100644 --- a/contracts/mixnet/src/interval/transactions.rs +++ b/contracts/mixnet/src/interval/transactions.rs @@ -2,26 +2,26 @@ // SPDX-License-Identifier: Apache-2.0 use super::storage; -use crate::interval::helpers::change_interval_config; +use crate::interval::helpers::{advance_epoch, change_interval_config}; use crate::interval::pending_events::ContractExecutableEvent; use crate::interval::storage::push_new_interval_event; use crate::mixnet_contract_settings::storage::ADMIN; -use crate::mixnodes::transactions::update_mixnode_layer; -use crate::rewards; -use crate::rewards::storage as rewards_storage; +use crate::nodes::storage as nymnodes_storage; +use crate::nodes::storage::{read_rewarded_set_metadata, reset_inactive_metadata}; +use crate::rewards::storage::RewardingStorage; use crate::support::helpers::{ ensure_can_advance_epoch, ensure_epoch_in_progress_state, ensure_is_authorized, }; -use cosmwasm_std::{DepsMut, Env, MessageInfo, Order, Response, Storage}; +use cosmwasm_std::{DepsMut, Env, MessageInfo, Response}; use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::events::{ - new_advance_epoch_event, new_epoch_transition_start_event, + new_advance_epoch_event, new_assigned_role_event, new_epoch_transition_start_event, new_pending_epoch_events_execution_event, new_pending_interval_config_update_event, new_pending_interval_events_execution_event, new_reconcile_pending_events, }; +use mixnet_contract_common::nym_node::Role; use mixnet_contract_common::pending_events::PendingIntervalEventKind; -use mixnet_contract_common::{EpochState, EpochStatus, LayerAssignment, MixId}; -use std::collections::BTreeSet; +use mixnet_contract_common::{EpochState, EpochStatus, RoleAssignment}; // those two should be called in separate tx (from advancing epoch), // since there might be a lot of events to execute. @@ -176,51 +176,15 @@ pub fn try_reconcile_epoch_events( }; if progress { - current_epoch_status.state = EpochState::AdvancingEpoch; + current_epoch_status.state = EpochState::RoleAssignment { + next: Role::first(), + }; storage::save_current_epoch_status(deps.storage, ¤t_epoch_status)?; } Ok(response) } -fn update_rewarded_set( - storage: &mut dyn Storage, - new_rewarded_set: Vec, - expected_active_set_size: u32, -) -> Result<(), MixnetContractError> { - let reward_params = rewards_storage::REWARDING_PARAMS.load(storage)?; - - // the rewarded set has been determined based off active set size taken from the contract, - // thus the expected value HAS TO match - if expected_active_set_size != reward_params.active_set_size { - return Err(MixnetContractError::UnexpectedActiveSetSize { - received: expected_active_set_size, - expected: reward_params.active_set_size, - }); - } - - if new_rewarded_set.len() as u32 > reward_params.rewarded_set_size { - return Err(MixnetContractError::UnexpectedRewardedSetSize { - received: new_rewarded_set.len() as u32, - expected: reward_params.rewarded_set_size, - }); - } - - // check for duplicates - let mut tmp_set = BTreeSet::new(); - for node_id in &new_rewarded_set { - if !tmp_set.insert(node_id) { - return Err(MixnetContractError::DuplicateRewardedSetNode { mix_id: *node_id }); - } - } - - Ok(storage::update_rewarded_set( - storage, - expected_active_set_size, - new_rewarded_set, - )?) -} - pub fn try_begin_epoch_transition( deps: DepsMut<'_>, env: Env, @@ -231,31 +195,29 @@ pub fn try_begin_epoch_transition( // can't do pre-mature epoch transition... let current_interval = storage::current_interval(deps.storage)?; - if !current_interval.is_current_epoch_over(&env) { - return Err(MixnetContractError::EpochInProgress { - current_block_time: env.block.time.seconds(), - epoch_start: current_interval.current_epoch_start_unix_timestamp(), - epoch_end: current_interval.current_epoch_end_unix_timestamp(), - }); - } + current_interval.ensure_current_epoch_is_over(&env)?; // ensure some other validator (currently not a problem), hasn't already committed to epoch progression ensure_epoch_in_progress_state(deps.storage)?; - // Note: if at any point we decide to change our rewarded set to be few thousand nodes - // and the below call fails, we'll have to pass `last_node_in_rewarded_set` as an argument to this function - // and then verify whether the provided value is valid (by using range iterator on `REWARDED_SET` - // and checking if there are any other entries following the provided value) - let rewarded_set = storage::REWARDED_SET - .range(deps.storage, None, None, Order::Ascending) - .map(|kv| kv.map(|kv| kv.0)) - .collect::, _>>()?; + let metadata = read_rewarded_set_metadata(deps.storage)?; + + // TODO: with pre-announcing rewarded set, this will have to happen elsewhere + reset_inactive_metadata( + deps.storage, + current_interval.current_epoch_absolute_id() + 1, + )?; + + // make sure to reset the submitted work for this epoch (since it's 0 now) + RewardingStorage::load().reset_cumulative_epoch_work(deps.storage)?; + + let final_node_id = metadata.highest_rewarded_id(); // if there are no nodes to reward (i.e. empty rewarded set), we go straight into event reconciliation - let new_epoch_state = if let Some(last) = rewarded_set.last() { + let new_epoch_state = if final_node_id != 0 { EpochState::Rewarding { last_rewarded: 0, - final_node_id: *last, + final_node_id, } } else { EpochState::ReconcilingEvents @@ -271,51 +233,51 @@ pub fn try_begin_epoch_transition( Ok(Response::new().add_event(new_epoch_transition_start_event(current_interval))) } -pub fn try_advance_epoch( +pub fn try_assign_roles( deps: DepsMut<'_>, env: Env, info: MessageInfo, - layer_assignments: Vec, - expected_active_set_size: u32, + assignment: RoleAssignment, ) -> Result { // Only rewarding validator can attempt to advance epoch let mut current_epoch_status = ensure_can_advance_epoch(&info.sender, deps.storage)?; - current_epoch_status.ensure_is_in_advancement_state()?; + current_epoch_status.ensure_is_in_expected_role_assignment_state(assignment.role)?; - // we must make sure that we roll into new epoch / interval with up to date state - // with no pending actions (like somebody wanting to update their profit margin) - let current_interval = storage::current_interval(deps.storage)?; - if !current_interval.is_current_epoch_over(&env) { - return Err(MixnetContractError::EpochInProgress { - current_block_time: env.block.time.seconds(), - epoch_start: current_interval.current_epoch_start_unix_timestamp(), - epoch_end: current_interval.current_epoch_end_unix_timestamp(), - }); - } + let role = assignment.role; + let assigned = assignment.nodes.len() as u32; - // if the current interval is over, apply reward pool changes - if current_interval.is_current_interval_over(&env) { - // this one is a very important one! - rewards::helpers::apply_reward_pool_changes(deps.storage)?; - } + let rewarded_set_params = RewardingStorage::load() + .global_rewarding_params + .load(deps.storage)? + .rewarded_set; - let updated_interval = current_interval.advance_epoch(); - let num_nodes = layer_assignments.len(); + // make sure we're not attempting to assign too many nodes to particular role + rewarded_set_params.ensure_role_count(role, assigned)?; - let new_rewarded_set = layer_assignments.iter().map(|l| l.mix_id()).collect(); + let next = assignment.role.next(); - // finally save updated interval and the rewarded set - storage::save_interval(deps.storage, &updated_interval)?; - update_rewarded_set(deps.storage, new_rewarded_set, expected_active_set_size)?; + // save the nodes for this layer + nymnodes_storage::save_assignment(deps.storage, assignment)?; - for a in layer_assignments { - update_mixnode_layer(a.mix_id(), a.layer(), deps.storage)?; - } + // TODO: optimise: if next is standby and standby set is empty, immediately advance + let event = match next { + Some(next_roles) => { + // update the state for the next assignment + current_epoch_status.state = EpochState::RoleAssignment { next: next_roles }; + new_assigned_role_event(role, assigned) + } + None => { + // the last role has been assigned => we're ready to progress into the next epoch + nymnodes_storage::swap_active_role_bucket(deps.storage)?; + let epoch_id = advance_epoch(deps.storage, env)?; + current_epoch_status.state = EpochState::InProgress; + new_advance_epoch_event(epoch_id) + } + }; - current_epoch_status.state = EpochState::InProgress; storage::save_current_epoch_status(deps.storage, ¤t_epoch_status)?; - Ok(Response::new().add_event(new_advance_epoch_event(updated_interval, num_nodes as u32))) + Ok(Response::new().add_event(event)) } pub(crate) fn try_update_interval_config( @@ -370,10 +332,13 @@ pub(crate) fn try_update_interval_config( #[cfg(test)] mod tests { use super::*; + use crate::mixnodes::storage as mixnodes_storage; + use crate::rewards::storage as rewards_storage; use crate::support::tests::fixtures; use crate::support::tests::test_helpers::TestSetup; use cosmwasm_std::Addr; use mixnet_contract_common::pending_events::PendingEpochEventKind; + use mixnet_contract_common::NodeId; fn push_n_dummy_epoch_actions(test: &mut TestSetup, n: usize) { // if you attempt to undelegate non-existent delegation, @@ -381,7 +346,7 @@ mod tests { let env = test.env(); for i in 0..n { let dummy_action = - PendingEpochEventKind::new_undelegate(Addr::unchecked("foomp"), i as MixId); + PendingEpochEventKind::new_undelegate(Addr::unchecked("foomp"), i as NodeId); storage::push_new_epoch_event(test.deps_mut().storage, &env, dummy_action).unwrap(); } } @@ -392,8 +357,8 @@ mod tests { let env = test.env(); for i in 0..n { let dummy_action = PendingIntervalEventKind::ChangeMixCostParams { - mix_id: i as MixId, - new_costs: fixtures::mix_node_cost_params_fixture(), + mix_id: i as NodeId, + new_costs: fixtures::node_cost_params_fixture(), }; storage::push_new_interval_event(test.deps_mut().storage, &env, dummy_action).unwrap(); } @@ -402,7 +367,7 @@ mod tests { #[cfg(test)] mod performing_pending_epoch_actions { use super::*; - use crate::support::tests::fixtures::TEST_COIN_DENOM; + use crate::support::tests::fixtures::{active_set_update_fixture, TEST_COIN_DENOM}; use cosmwasm_std::{coin, coins, BankMsg, Empty, SubMsg}; use mixnet_contract_common::events::{ new_active_set_update_event, new_delegation_on_unbonded_node_event, @@ -470,7 +435,9 @@ mod tests { ); push_n_dummy_epoch_actions(&mut test, 10); - let action_with_event = PendingEpochEventKind::UpdateActiveSetSize { new_size: 50 }; + let action_with_event = PendingEpochEventKind::UpdateActiveSet { + update: active_set_update_fixture(), + }; storage::push_new_epoch_event(test.deps_mut().storage, &env, action_with_event) .unwrap(); push_n_dummy_epoch_actions(&mut test, 10); @@ -478,7 +445,10 @@ mod tests { perform_pending_epoch_actions(test.deps_mut(), &env, None).unwrap(); assert_eq!( res, - Response::new().add_event(new_active_set_update_event(env.block.height, 50)) + Response::new().add_event(new_active_set_update_event( + env.block.height, + active_set_update_fixture() + )) ); assert_eq!(executed, 21); assert_eq!( @@ -494,7 +464,7 @@ mod tests { let mut test = TestSetup::new(); let env = test.env(); - let legit_mix = test.add_dummy_mixnode("mix-owner", None); + let legit_mix = test.add_legacy_mixnode("mix-owner", None); let delegator = Addr::unchecked("delegator"); let amount = 123_456_789u128; test.add_immediate_delegation(delegator.as_str(), amount, legit_mix); @@ -522,10 +492,15 @@ mod tests { })); // updating active set should only emit events and no cosmos messages - let action_with_event = PendingEpochEventKind::UpdateActiveSetSize { new_size: 50 }; + let action_with_event = PendingEpochEventKind::UpdateActiveSet { + update: active_set_update_fixture(), + }; storage::push_new_epoch_event(test.deps_mut().storage, &env, action_with_event) .unwrap(); - expected_events.push(new_active_set_update_event(env.block.height, 50)); + expected_events.push(new_active_set_update_event( + env.block.height, + active_set_update_fixture(), + )); // undelegation just returns tokens and emits event let legit_undelegate = @@ -613,10 +588,10 @@ mod tests { use crate::support::tests::fixtures::TEST_COIN_DENOM; use cosmwasm_std::{coin, Empty, SubMsg}; use mixnet_contract_common::events::{ - new_interval_config_update_event, new_mixnode_cost_params_update_event, + new_cost_params_update_event, new_interval_config_update_event, new_rewarding_params_update_event, }; - use mixnet_contract_common::mixnode::MixNodeCostParams; + use mixnet_contract_common::mixnode::NodeCostParams; use mixnet_contract_common::reward_params::IntervalRewardingParamsUpdate; use mixnet_contract_common::Percent; @@ -682,7 +657,7 @@ mod tests { push_n_dummy_interval_actions(&mut test, 10); let update = IntervalRewardingParamsUpdate { - rewarded_set_size: Some(500), + interval_pool_emission: Some(Percent::from_percentage_value(42).unwrap()), ..Default::default() }; let action_with_event = PendingIntervalEventKind::UpdateRewardingParams { update }; @@ -717,25 +692,30 @@ mod tests { let mut expected_events = Vec::new(); let expected_messages: Vec> = Vec::new(); - let legit_mix = test.add_dummy_mixnode("mix-owner", None); - let new_costs = MixNodeCostParams { + let legit_mix = test.add_legacy_mixnode("mix-owner", None); + let new_costs = NodeCostParams { profit_margin_percent: Percent::from_percentage_value(12).unwrap(), interval_operating_cost: coin(123_000, TEST_COIN_DENOM), }; + // this would have been normally populated when creating the event itself + mixnodes_storage::PENDING_MIXNODE_CHANGES + .save(test.deps_mut().storage, legit_mix, &Default::default()) + .unwrap(); + let cost_change = PendingIntervalEventKind::ChangeMixCostParams { mix_id: legit_mix, new_costs: new_costs.clone(), }; storage::push_new_interval_event(test.deps_mut().storage, &env, cost_change).unwrap(); - expected_events.push(new_mixnode_cost_params_update_event( + expected_events.push(new_cost_params_update_event( env.block.height, legit_mix, &new_costs, )); let update = IntervalRewardingParamsUpdate { - rewarded_set_size: Some(500), + interval_pool_emission: Some(Percent::from_percentage_value(42).unwrap()), ..Default::default() }; let change_params = PendingIntervalEventKind::UpdateRewardingParams { update }; @@ -858,7 +838,9 @@ mod tests { final_node_id: 0, }, EpochState::ReconcilingEvents, - EpochState::AdvancingEpoch, + EpochState::RoleAssignment { + next: Role::first(), + }, ]; for bad_state in bad_states { @@ -972,6 +954,7 @@ mod tests { new_delegation_on_unbonded_node_event, new_rewarding_params_update_event, }; use mixnet_contract_common::reward_params::IntervalRewardingParamsUpdate; + use nym_contracts_common::Percent; #[test] fn can_only_be_performed_if_in_reconciling_state() { @@ -981,7 +964,9 @@ mod tests { last_rewarded: 0, final_node_id: 0, }, - EpochState::AdvancingEpoch, + EpochState::RoleAssignment { + next: Role::first(), + }, ]; for bad_state in bad_states { @@ -1019,7 +1004,9 @@ mod tests { let expected = EpochStatus { being_advanced_by: test.rewarding_validator().sender, - state: EpochState::AdvancingEpoch, + state: EpochState::RoleAssignment { + next: Role::first(), + }, }; assert_eq!( expected, @@ -1068,7 +1055,9 @@ mod tests { let expected = EpochStatus { being_advanced_by: test.rewarding_validator().sender, - state: EpochState::AdvancingEpoch, + state: EpochState::RoleAssignment { + next: Role::first(), + }, }; assert_eq!( expected, @@ -1092,7 +1081,9 @@ mod tests { let expected = EpochStatus { being_advanced_by: test.rewarding_validator().sender, - state: EpochState::AdvancingEpoch, + state: EpochState::RoleAssignment { + next: Role::first(), + }, }; assert_eq!( expected, @@ -1312,7 +1303,7 @@ mod tests { // interval event let update = IntervalRewardingParamsUpdate { - rewarded_set_size: Some(500), + interval_pool_emission: Some(Percent::from_percentage_value(42).unwrap()), ..Default::default() }; let change_params = PendingIntervalEventKind::UpdateRewardingParams { update }; @@ -1354,89 +1345,24 @@ mod tests { } } - #[test] - fn updating_rewarded_set() { - // the actual logic behind writing stuff to the storage has been tested in - // different unit test - let mut test = TestSetup::new(); - let current_active_set = test.rewarding_params().active_set_size; - let current_rewarded_set = test.rewarding_params().rewarded_set_size; - - // active set size has to match the expectation - let err = update_rewarded_set( - test.deps_mut().storage, - vec![1, 2, 3], - current_active_set - 10, - ) - .unwrap_err(); - assert_eq!( - err, - MixnetContractError::UnexpectedActiveSetSize { - received: current_active_set - 10, - expected: current_active_set, - } - ); - - // number of nodes provided has to be equal or smaller than the current rewarded set size - - // fewer nodes - let res = update_rewarded_set(test.deps_mut().storage, vec![1, 2, 3], current_active_set); - assert!(res.is_ok()); - - let exact_num = (1u32..) - .take(current_rewarded_set as usize) - .collect::>(); - let res = update_rewarded_set(test.deps_mut().storage, exact_num, current_active_set); - assert!(res.is_ok()); - - // one more - let too_many = (1u32..) - .take((current_rewarded_set + 1) as usize) - .collect::>(); - let err = - update_rewarded_set(test.deps_mut().storage, too_many, current_active_set).unwrap_err(); - assert_eq!( - err, - MixnetContractError::UnexpectedRewardedSetSize { - received: current_rewarded_set + 1, - expected: current_rewarded_set, - } - ); - - // doesn't allow for duplicates - let nodes_with_duplicate = vec![1, 2, 3, 4, 5, 1]; - let err = update_rewarded_set( - test.deps_mut().storage, - nodes_with_duplicate, - current_active_set, - ) - .unwrap_err(); - assert_eq!( - err, - MixnetContractError::DuplicateRewardedSetNode { mix_id: 1 } - ); - let nodes_with_duplicate = vec![1, 2, 3, 5, 4, 5]; - let err = update_rewarded_set( - test.deps_mut().storage, - nodes_with_duplicate, - current_active_set, - ) - .unwrap_err(); - assert_eq!( - err, - MixnetContractError::DuplicateRewardedSetNode { mix_id: 5 } - ); - } - #[cfg(test)] - mod advancing_epoch { + mod assigning_roles { use super::*; - use crate::mixnodes::queries::query_mixnode_details; - use crate::rewards::models::RewardPoolChange; use cosmwasm_std::testing::mock_info; - use cosmwasm_std::{Decimal, Uint128}; - use mixnet_contract_common::reward_params::IntervalRewardingParamsUpdate; - use mixnet_contract_common::{Layer, RewardedSetNodeStatus}; + use cosmwasm_std::Uint128; + + fn setup_test() -> TestSetup { + let mut test = TestSetup::new(); + + for i in 0..10 { + test.add_dummy_nymnode(&format!("node-owner-{i}"), None); + } + + test.skip_to_current_epoch_end(); + test.set_epoch_role_assignment_state(); + + test + } #[test] fn can_only_be_performed_if_in_advancing_epoch_state() { @@ -1451,10 +1377,9 @@ mod tests { for bad_state in bad_states { let mut test = TestSetup::new(); - test.add_dummy_mixnode("1", Some(Uint128::new(100000000))); - test.add_dummy_mixnode("2", Some(Uint128::new(100000000))); - test.add_dummy_mixnode("3", Some(Uint128::new(100000000))); - let current_active_set = test.rewarding_params().active_set_size; + test.add_legacy_mixnode("1", Some(Uint128::new(100000000))); + test.add_legacy_mixnode("2", Some(Uint128::new(100000000))); + test.add_legacy_mixnode("3", Some(Uint128::new(100000000))); test.skip_to_current_epoch_end(); @@ -1462,24 +1387,17 @@ mod tests { status.state = bad_state; storage::save_current_epoch_status(test.deps_mut().storage, &status).unwrap(); - let layer_assignments = vec![ - LayerAssignment::new(1, Layer::One), - LayerAssignment::new(2, Layer::Two), - LayerAssignment::new(3, Layer::Three), - ]; + let role_assignment = RoleAssignment { + role: Role::Layer1, + nodes: vec![1, 2, 3], + }; let env = test.env(); let sender = test.rewarding_validator(); - let res = try_advance_epoch( - test.deps_mut(), - env, - sender, - layer_assignments, - current_active_set, - ); + let res = try_assign_roles(test.deps_mut(), env, sender, role_assignment); assert_eq!( res, - Err(MixnetContractError::EpochNotInAdvancementState { + Err(MixnetContractError::EpochNotInRoleAssignmentState { current_state: bad_state }) ); @@ -1489,276 +1407,381 @@ mod tests { #[test] fn epoch_state_is_correctly_updated() { let mut test = TestSetup::new(); - test.add_dummy_mixnode("1", Some(Uint128::new(100000000))); - test.add_dummy_mixnode("2", Some(Uint128::new(100000000))); - test.add_dummy_mixnode("3", Some(Uint128::new(100000000))); - let current_active_set = test.rewarding_params().active_set_size; - test.skip_to_current_epoch_end(); - test.set_epoch_advancement_state(); + test.set_epoch_role_assignment_state(); - let layer_assignments = vec![ - LayerAssignment::new(1, Layer::One), - LayerAssignment::new(2, Layer::Two), - LayerAssignment::new(3, Layer::Three), + let cases = vec![ + ( + RoleAssignment { + role: Role::ExitGateway, + nodes: vec![1, 2, 3], + }, + EpochState::RoleAssignment { + next: Role::EntryGateway, + }, + ), + ( + RoleAssignment { + role: Role::EntryGateway, + nodes: vec![4, 5, 6], + }, + EpochState::RoleAssignment { next: Role::Layer1 }, + ), + ( + RoleAssignment { + role: Role::Layer1, + nodes: vec![7, 8, 9], + }, + EpochState::RoleAssignment { next: Role::Layer2 }, + ), + ( + RoleAssignment { + role: Role::Layer2, + nodes: vec![9, 10, 11], + }, + EpochState::RoleAssignment { next: Role::Layer3 }, + ), + ( + RoleAssignment { + role: Role::Layer3, + nodes: vec![12], + }, + EpochState::RoleAssignment { + next: Role::Standby, + }, + ), + ( + RoleAssignment { + role: Role::Standby, + nodes: vec![42], + }, + EpochState::InProgress, + ), ]; - let env = test.env(); - let sender = test.rewarding_validator(); - try_advance_epoch( - test.deps_mut(), - env, - sender, - layer_assignments, - current_active_set, - ) - .unwrap(); + for (assignment, expected) in cases { + let env = test.env(); + let sender = test.rewarding_validator(); + try_assign_roles(test.deps_mut(), env, sender, assignment).unwrap(); - let expected = EpochStatus { - being_advanced_by: test.rewarding_validator().sender, - state: EpochState::InProgress, - }; - assert_eq!( - expected, - storage::current_epoch_status(test.deps().storage).unwrap() - ) + let expected = EpochStatus { + being_advanced_by: test.rewarding_validator().sender, + state: expected, + }; + assert_eq!( + expected, + storage::current_epoch_status(test.deps().storage).unwrap() + ); + } } #[test] fn can_only_be_performed_by_specified_rewarding_validator() { let mut test = TestSetup::new(); - test.add_dummy_mixnode("1", Some(Uint128::new(100000000))); - test.add_dummy_mixnode("2", Some(Uint128::new(100000000))); - test.add_dummy_mixnode("3", Some(Uint128::new(100000000))); - let current_active_set = test.rewarding_params().active_set_size; + test.add_dummy_nymnode("1", Some(Uint128::new(100000000))); + test.add_dummy_nymnode("2", Some(Uint128::new(100000000))); + test.add_dummy_nymnode("3", Some(Uint128::new(100000000))); let some_sender = mock_info("foomper", &[]); test.skip_to_current_epoch_end(); - test.set_epoch_advancement_state(); + test.set_epoch_role_assignment_state(); - let layer_assignments = vec![ - LayerAssignment::new(1, Layer::One), - LayerAssignment::new(2, Layer::Two), - LayerAssignment::new(3, Layer::Three), - ]; + let role_assignment = RoleAssignment { + role: Role::first(), + nodes: vec![1, 2, 3], + }; let env = test.env(); - let res = try_advance_epoch( - test.deps_mut(), - env, - some_sender, - layer_assignments.clone(), - current_active_set, - ); + let res = try_assign_roles(test.deps_mut(), env, some_sender, role_assignment.clone()); assert_eq!(res, Err(MixnetContractError::Unauthorized)); // good address (sanity check) let env = test.env(); let sender = test.rewarding_validator(); - let res = try_advance_epoch( - test.deps_mut(), - env, - sender, - layer_assignments, - current_active_set, - ); + let res = try_assign_roles(test.deps_mut(), env, sender, role_assignment); assert!(res.is_ok()) } #[test] - fn can_only_be_performed_if_epoch_is_over() { - let mut test = TestSetup::new(); - test.set_epoch_advancement_state(); + fn has_maximum_nodes_per_role() -> anyhow::Result<()> { + fn nodes_vec(start: NodeId, count: u32) -> Vec { + (start..start + count).collect() + } - let current_active_set = test.rewarding_params().active_set_size; + let mut test = setup_test(); - test.add_dummy_mixnode("1", Some(Uint128::new(100000000))); - test.add_dummy_mixnode("2", Some(Uint128::new(100000000))); - test.add_dummy_mixnode("3", Some(Uint128::new(100000000))); - - let layer_assignments = vec![ - LayerAssignment::new(1, Layer::One), - LayerAssignment::new(2, Layer::Two), - LayerAssignment::new(3, Layer::Three), + let roles = [ + Role::ExitGateway, + Role::EntryGateway, + Role::Layer1, + Role::Layer2, + Role::Layer3, + Role::Standby, ]; let env = test.env(); let sender = test.rewarding_validator(); - let res = try_advance_epoch( - test.deps_mut(), - env, - sender.clone(), - layer_assignments.clone(), - current_active_set, - ); - assert!(matches!( - res, - Err(MixnetContractError::EpochInProgress { .. }) - )); - let mixnode_1 = query_mixnode_details(test.deps.as_ref(), 1).unwrap(); - assert_eq!( - mixnode_1.mixnode_details.unwrap().bond_information.layer, - Layer::One - ); + for role in roles { + let max_count = test.max_role_count(role); - let mixnode_1 = query_mixnode_details(test.deps.as_ref(), 2).unwrap(); - assert_eq!( - mixnode_1.mixnode_details.unwrap().bond_information.layer, - Layer::Two - ); - - let mixnode_1 = query_mixnode_details(test.deps.as_ref(), 3).unwrap(); - assert_eq!( - mixnode_1.mixnode_details.unwrap().bond_information.layer, - Layer::Three - ); - - // sanity check - test.skip_to_current_epoch_end(); - - let env = test.env(); - let res = try_advance_epoch( - test.deps_mut(), - env, - sender, - layer_assignments, - current_active_set, - ); - assert!(res.is_ok()) - } - - #[test] - fn if_interval_is_over_applies_reward_pool_changes() { - let mut test = TestSetup::new(); - test.set_epoch_advancement_state(); - - let current_active_set = test.rewarding_params().active_set_size; - - test.add_dummy_mixnode("1", Some(Uint128::new(100000000))); - test.add_dummy_mixnode("2", Some(Uint128::new(100000000))); - test.add_dummy_mixnode("3", Some(Uint128::new(100000000))); - - let start_params = test.rewarding_params(); - - let pool_update = Decimal::from_atomics(100_000_000u32, 0).unwrap(); - // push some changes - rewards_storage::PENDING_REWARD_POOL_CHANGE - .save( - test.deps_mut().storage, - &RewardPoolChange { - removed: pool_update, - added: Default::default(), + let res = try_assign_roles( + test.deps_mut(), + env.clone(), + sender.clone(), + RoleAssignment { + role, + nodes: nodes_vec(1, max_count + 1), }, - ) - .unwrap(); + ); + assert_eq!( + res.unwrap_err(), + MixnetContractError::IllegalRoleCount { + role, + assigned: max_count + 1, + allowed: max_count, + } + ); - let layer_assignments = vec![ - LayerAssignment::new(1, Layer::One), - LayerAssignment::new(2, Layer::Two), - LayerAssignment::new(3, Layer::Three), - ]; + let res = try_assign_roles( + test.deps_mut(), + env.clone(), + sender.clone(), + RoleAssignment { + role, + nodes: nodes_vec(1, max_count), + }, + ); + assert!(res.is_ok()); + } - // end of epoch - nothing has happened - let sender = test.rewarding_validator(); - test.skip_to_current_epoch_end(); - - let env = test.env(); - try_advance_epoch( - test.deps_mut(), - env, - sender, - layer_assignments.clone(), - current_active_set, - ) - .unwrap(); - - let params = test.rewarding_params(); - let pool_change = rewards_storage::PENDING_REWARD_POOL_CHANGE - .load(test.deps().storage) - .unwrap(); - assert_eq!(params, start_params); - assert_eq!(pool_change.removed, pool_update); - - let sender = test.rewarding_validator(); - test.skip_to_current_interval_end(); - test.set_epoch_advancement_state(); - - let env = test.env(); - try_advance_epoch( - test.deps_mut(), - env, - sender, - layer_assignments, - current_active_set, - ) - .unwrap(); - - let epochs_in_interval = test.current_interval().epochs_in_interval(); - let update = IntervalRewardingParamsUpdate { - reward_pool: Some(start_params.interval.reward_pool - pool_update), - staking_supply: Some(start_params.interval.staking_supply + pool_update), - ..Default::default() - }; - let mut expected = start_params; - expected - .try_apply_updates(update, epochs_in_interval) - .unwrap(); - - let params = test.rewarding_params(); - let pool_change = rewards_storage::PENDING_REWARD_POOL_CHANGE - .load(test.deps().storage) - .unwrap(); - assert_eq!(params, expected); - assert_eq!(pool_change.removed, Decimal::zero()); + Ok(()) } #[test] - fn updates_rewarded_set_and_interval_data() { - let mut test = TestSetup::new(); - test.set_epoch_advancement_state(); + fn cant_be_performed_out_of_order() -> anyhow::Result<()> { + let mut test = setup_test(); - let current_active_set = test.rewarding_params().active_set_size; + let env = test.env(); + let sender = test.rewarding_validator(); - test.add_dummy_mixnode("1", Some(Uint128::new(100000000))); - test.add_dummy_mixnode("2", Some(Uint128::new(100000000))); - test.add_dummy_mixnode("3", Some(Uint128::new(100000000))); - - let interval_pre = test.current_interval(); - let rewarded_set_pre = test.rewarded_set(); - assert!(rewarded_set_pre.is_empty()); - - let layer_assignments = vec![ - LayerAssignment::new(1, Layer::One), - LayerAssignment::new(2, Layer::Two), - LayerAssignment::new(3, Layer::Three), + let expected_order = [ + Role::ExitGateway, + Role::EntryGateway, + Role::Layer1, + Role::Layer2, + Role::Layer3, + Role::Standby, ]; - let sender = test.rewarding_validator(); - test.skip_to_current_interval_end(); - let env = test.env(); - try_advance_epoch( - test.deps_mut(), - env, - sender, - layer_assignments, - current_active_set, - ) - .unwrap(); + for (i, role) in expected_order.iter().enumerate() { + let wrong_role = if role == &Role::Layer1 { + Role::Layer2 + } else { + Role::Layer1 + }; - let interval_post = test.current_interval(); - let rewarded_set = test.rewarded_set(); + let res = try_assign_roles( + test.deps_mut(), + env.clone(), + sender.clone(), + RoleAssignment { + role: wrong_role, + nodes: vec![i as u32], + }, + ); + assert_eq!( + res.unwrap_err(), + MixnetContractError::UnexpectedRoleAssignment { + expected: *role, + got: wrong_role + } + ); - let expected_id = interval_pre.current_epoch_absolute_id() + 1; - assert_eq!(interval_post.current_epoch_absolute_id(), expected_id); - assert_eq!( - rewarded_set, - vec![ - (1, RewardedSetNodeStatus::Active), - (2, RewardedSetNodeStatus::Active), - (3, RewardedSetNodeStatus::Active) - ] - ); + let res = try_assign_roles( + test.deps_mut(), + env.clone(), + sender.clone(), + RoleAssignment { + role: *role, + nodes: vec![i as u32], + }, + ); + assert!(res.is_ok()); + } + + Ok(()) + } + + #[cfg(test)] + mod correctly_updates_storage { + use super::*; + use mixnet_contract_common::nym_node::RoleMetadata; + + fn perform_partial_assignment(test: &mut TestSetup) -> anyhow::Result<()> { + let env = test.env(); + let sender = test.rewarding_validator(); + try_assign_roles( + test.deps_mut(), + env.clone(), + sender.clone(), + RoleAssignment { + role: Role::ExitGateway, + nodes: vec![1, 2, 3], + }, + )?; + + try_assign_roles( + test.deps_mut(), + env.clone(), + sender.clone(), + RoleAssignment { + role: Role::EntryGateway, + nodes: vec![4, 5, 6], + }, + )?; + + try_assign_roles( + test.deps_mut(), + env, + sender, + RoleAssignment { + role: Role::Layer1, + nodes: vec![7, 8], + }, + )?; + + Ok(()) + } + + #[test] + fn updates_metadata() -> anyhow::Result<()> { + let mut test = setup_test(); + + let initial = test.inactive_roles_metadata(); + + // initial state + let empty = RoleMetadata::default(); + assert_eq!(empty, initial.entry_gateway_metadata); + assert_eq!(empty, initial.layer1_metadata); + assert_eq!(empty, initial.layer2_metadata); + assert_eq!(empty, initial.layer3_metadata); + assert_eq!(empty, initial.exit_gateway_metadata); + assert_eq!(empty, initial.standby_metadata); + + perform_partial_assignment(&mut test)?; + + let updated = test.inactive_roles_metadata(); + assert_eq!(3, updated.exit_gateway_metadata.highest_id); + assert_eq!(3, updated.exit_gateway_metadata.num_nodes); + assert_eq!(6, updated.entry_gateway_metadata.highest_id); + assert_eq!(3, updated.entry_gateway_metadata.num_nodes); + assert_eq!(8, updated.layer1_metadata.highest_id); + assert_eq!(2, updated.layer1_metadata.num_nodes); + + assert_eq!(empty, updated.layer2_metadata); + assert_eq!(empty, updated.layer3_metadata); + assert_eq!(empty, updated.standby_metadata); + + Ok(()) + } + + #[test] + fn updates_role_data() -> anyhow::Result<()> { + let mut test = setup_test(); + + assert!(test.inactive_roles(Role::ExitGateway).is_empty()); + assert!(test.inactive_roles(Role::EntryGateway).is_empty()); + assert!(test.inactive_roles(Role::Layer1).is_empty()); + assert!(test.inactive_roles(Role::Layer2).is_empty()); + assert!(test.inactive_roles(Role::Layer3).is_empty()); + assert!(test.inactive_roles(Role::Standby).is_empty()); + + perform_partial_assignment(&mut test)?; + + assert_eq!(3, test.inactive_roles(Role::ExitGateway).len()); + assert_eq!(3, test.inactive_roles(Role::EntryGateway).len()); + assert_eq!(2, test.inactive_roles(Role::Layer1).len()); + assert!(test.inactive_roles(Role::Layer2).is_empty()); + assert!(test.inactive_roles(Role::Layer3).is_empty()); + assert!(test.inactive_roles(Role::Standby).is_empty()); + + Ok(()) + } + + #[test] + fn updates_epoch_status() -> anyhow::Result<()> { + let mut test = setup_test(); + + let env = test.env(); + let sender = test.rewarding_validator(); + + let roles = [ + Role::ExitGateway, + Role::EntryGateway, + Role::Layer1, + Role::Layer2, + Role::Layer3, + Role::Standby, + ]; + + for (i, role) in roles.into_iter().enumerate() { + let expected_next = role.next(); + + try_assign_roles( + test.deps_mut(), + env.clone(), + sender.clone(), + RoleAssignment { + role, + nodes: vec![i as u32], + }, + )?; + + let state = test.epoch_state(); + match expected_next { + None => assert_eq!(state, EpochState::InProgress), + Some(next) => assert_eq!(state, EpochState::RoleAssignment { next }), + } + } + + Ok(()) + } + + #[test] + fn swaps_roles_buckets_after_final_role() -> anyhow::Result<()> { + let mut test = setup_test(); + + let env = test.env(); + let sender = test.rewarding_validator(); + + let active = test.active_roles_bucket(); + + let roles = [ + Role::ExitGateway, + Role::EntryGateway, + Role::Layer1, + Role::Layer2, + Role::Layer3, + Role::Standby, + ]; + + for (i, role) in roles.into_iter().enumerate() { + try_assign_roles( + test.deps_mut(), + env.clone(), + sender.clone(), + RoleAssignment { + role, + nodes: vec![i as u32], + }, + )?; + } + + assert_eq!(test.active_roles_bucket(), active.other()); + + Ok(()) + } } } @@ -1778,7 +1801,9 @@ mod tests { final_node_id: 0, }, EpochState::ReconcilingEvents, - EpochState::AdvancingEpoch, + EpochState::RoleAssignment { + next: Role::first(), + }, ]; for bad_state in bad_states { diff --git a/contracts/mixnet/src/lib.rs b/contracts/mixnet/src/lib.rs index 985cffda57..8e1450421d 100644 --- a/contracts/mixnet/src/lib.rs +++ b/contracts/mixnet/src/lib.rs @@ -1,17 +1,19 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 #![warn(clippy::expect_used)] #![warn(clippy::unwrap_used)] +#![warn(clippy::todo)] -mod constants; +pub(crate) mod compat; +pub mod constants; pub mod contract; mod delegations; -mod families; mod gateways; mod interval; mod mixnet_contract_settings; mod mixnodes; +mod nodes; mod queued_migrations; mod rewards; pub mod signing; diff --git a/contracts/mixnet/src/mixnet_contract_settings/queries.rs b/contracts/mixnet/src/mixnet_contract_settings/queries.rs index 5d80fec78a..415704d04a 100644 --- a/contracts/mixnet/src/mixnet_contract_settings/queries.rs +++ b/contracts/mixnet/src/mixnet_contract_settings/queries.rs @@ -49,9 +49,8 @@ pub(crate) mod tests { vesting_contract_address: Addr::unchecked("foomp"), rewarding_denom: "unym".to_string(), params: ContractStateParams { - minimum_mixnode_delegation: None, - minimum_mixnode_pledge: coin(123u128, "unym"), - minimum_gateway_pledge: coin(456u128, "unym"), + minimum_delegation: None, + minimum_pledge: coin(123u128, "unym"), profit_margin: Default::default(), interval_operating_cost: Default::default(), }, diff --git a/contracts/mixnet/src/mixnet_contract_settings/storage.rs b/contracts/mixnet/src/mixnet_contract_settings/storage.rs index 0c8502089e..3897585856 100644 --- a/contracts/mixnet/src/mixnet_contract_settings/storage.rs +++ b/contracts/mixnet/src/mixnet_contract_settings/storage.rs @@ -7,7 +7,9 @@ use cosmwasm_std::{Coin, StdResult}; use cw_controllers::Admin; use cw_storage_plus::Item; use mixnet_contract_common::error::MixnetContractError; -use mixnet_contract_common::{ContractState, OperatingCostRange, ProfitMarginRange}; +use mixnet_contract_common::{ + ContractState, ContractStateParams, OperatingCostRange, ProfitMarginRange, +}; pub(crate) const CONTRACT_STATE: Item<'_, ContractState> = Item::new(CONTRACT_STATE_KEY); pub(crate) const ADMIN: Admin = Admin::new(ADMIN_STORAGE_KEY); @@ -18,16 +20,10 @@ pub fn rewarding_validator_address(storage: &dyn Storage) -> Result Result { +pub(crate) fn minimum_node_pledge(storage: &dyn Storage) -> Result { Ok(CONTRACT_STATE .load(storage) - .map(|state| state.params.minimum_mixnode_pledge)?) -} - -pub(crate) fn minimum_gateway_pledge(storage: &dyn Storage) -> Result { - Ok(CONTRACT_STATE - .load(storage) - .map(|state| state.params.minimum_gateway_pledge)?) + .map(|state| state.params.minimum_pledge)?) } pub(crate) fn profit_margin_range( @@ -52,7 +48,7 @@ pub(crate) fn minimum_delegation_stake( ) -> Result, MixnetContractError> { Ok(CONTRACT_STATE .load(storage) - .map(|state| state.params.minimum_mixnode_delegation)?) + .map(|state| state.params.minimum_delegation)?) } pub(crate) fn rewarding_denom(storage: &dyn Storage) -> Result { @@ -67,6 +63,12 @@ pub(crate) fn vesting_contract_address(storage: &dyn Storage) -> Result Result { + Ok(CONTRACT_STATE.load(storage).map(|state| state.params)?) +} + pub(crate) fn initialise_storage( deps: DepsMut<'_>, initial_state: ContractState, diff --git a/contracts/mixnet/src/mixnet_contract_settings/transactions.rs b/contracts/mixnet/src/mixnet_contract_settings/transactions.rs index b458f6fbbc..16750eb44c 100644 --- a/contracts/mixnet/src/mixnet_contract_settings/transactions.rs +++ b/contracts/mixnet/src/mixnet_contract_settings/transactions.rs @@ -75,7 +75,7 @@ pub(crate) fn try_update_contract_settings( #[cfg(test)] pub mod tests { use super::*; - use crate::constants::{INITIAL_GATEWAY_PLEDGE_AMOUNT, INITIAL_MIXNODE_PLEDGE_AMOUNT}; + use crate::constants::INITIAL_PLEDGE_AMOUNT; use crate::mixnet_contract_settings::queries::query_rewarding_validator_address; use crate::mixnet_contract_settings::storage::rewarding_denom; use crate::support::tests::test_helpers; @@ -129,14 +129,10 @@ pub mod tests { let denom = rewarding_denom(deps.as_ref().storage).unwrap(); let new_params = ContractStateParams { - minimum_mixnode_delegation: None, - minimum_mixnode_pledge: Coin { + minimum_delegation: None, + minimum_pledge: Coin { denom: denom.clone(), - amount: INITIAL_MIXNODE_PLEDGE_AMOUNT, - }, - minimum_gateway_pledge: Coin { - denom, - amount: INITIAL_GATEWAY_PLEDGE_AMOUNT + Uint128::new(1234), + amount: Uint128::new(12345), }, profit_margin: Default::default(), interval_operating_cost: Default::default(), diff --git a/contracts/mixnet/src/mixnodes/helpers.rs b/contracts/mixnet/src/mixnodes/helpers.rs index b8dd15ae6e..fc2d56254f 100644 --- a/contracts/mixnet/src/mixnodes/helpers.rs +++ b/contracts/mixnet/src/mixnodes/helpers.rs @@ -2,15 +2,11 @@ // SPDX-License-Identifier: Apache-2.0 use super::storage; -use crate::interval::storage as interval_storage; -use crate::mixnodes::storage::{assign_layer, next_mixnode_id_counter}; use crate::rewards::storage as rewards_storage; -use cosmwasm_std::{Addr, Coin, Decimal, Env, StdResult, Storage}; +use cosmwasm_std::{Addr, Decimal, Env, StdResult, Storage}; use mixnet_contract_common::error::MixnetContractError; -use mixnet_contract_common::mixnode::{ - MixNodeCostParams, MixNodeDetails, MixNodeRewarding, UnbondedMixnode, -}; -use mixnet_contract_common::{IdentityKey, Layer, MixId, MixNode, MixNodeBond}; +use mixnet_contract_common::mixnode::{MixNodeDetails, UnbondedMixnode}; +use mixnet_contract_common::{IdentityKey, MixNodeBond, NodeId}; pub(crate) fn must_get_mixnode_bond_by_owner( store: &dyn Storage, @@ -50,7 +46,7 @@ pub(crate) fn attach_mix_details( pub(crate) fn get_mixnode_details_by_id( store: &dyn Storage, - mix_id: MixId, + mix_id: NodeId, ) -> StdResult> { if let Some(bond_information) = storage::mixnode_bonds().may_load(store, mix_id)? { attach_mix_details(store, bond_information).map(Some) @@ -91,38 +87,13 @@ pub(crate) fn get_mixnode_details_by_identity( } } -pub(crate) fn save_new_mixnode( - storage: &mut dyn Storage, - env: Env, - mixnode: MixNode, - cost_params: MixNodeCostParams, - owner: Addr, - pledge: Coin, -) -> Result<(MixId, Layer), MixnetContractError> { - let layer = assign_layer(storage)?; - let mix_id = next_mixnode_id_counter(storage)?; - let current_epoch = interval_storage::current_interval(storage)?.current_epoch_absolute_id(); - - let mixnode_rewarding = MixNodeRewarding::initialise_new(cost_params, &pledge, current_epoch)?; - let mixnode_bond = MixNodeBond::new(mix_id, owner, pledge, layer, mixnode, env.block.height); - - // save mixnode bond data - // note that this implicitly checks for uniqueness on identity key, sphinx key and owner - storage::mixnode_bonds().save(storage, mix_id, &mixnode_bond)?; - - // save rewarding data - rewards_storage::MIXNODE_REWARDING.save(storage, mix_id, &mixnode_rewarding)?; - - Ok((mix_id, layer)) -} - pub(crate) fn cleanup_post_unbond_mixnode_storage( storage: &mut dyn Storage, env: &Env, current_details: &MixNodeDetails, ) -> Result<(), MixnetContractError> { let mix_id = current_details.bond_information.mix_id; - // remove all bond information (we don't need it anymore + // remove all bond information since we don't need it anymore // note that "normal" remove is `may_load` followed by `replace` with a `None` // and we have already loaded the data from the storage storage::mixnode_bonds().replace( @@ -160,20 +131,17 @@ pub(crate) fn cleanup_post_unbond_mixnode_storage( unbonding_height: env.block.height, }, )?; - storage::decrement_layer_count(storage, current_details.bond_information.layer) + Ok(()) } #[cfg(test)] pub(crate) mod tests { use super::*; - use crate::support::tests::fixtures::{ - mix_node_cost_params_fixture, mix_node_fixture, TEST_COIN_DENOM, - }; use crate::support::tests::test_helpers::TestSetup; - use cosmwasm_std::{coin, Uint128}; + use cosmwasm_std::Uint128; pub(crate) struct DummyMixnode { - pub mix_id: MixId, + pub mix_id: NodeId, pub owner: Addr, pub identity: IdentityKey, } @@ -375,101 +343,12 @@ pub(crate) mod tests { assert!(res.is_none()); } - #[test] - fn saving_new_mixnode() { - let mut test = TestSetup::new(); - - // get some mixnodes in - test.add_dummy_mixnode("owner1", None); - test.add_dummy_mixnode("owner2", None); - test.add_dummy_mixnode("owner3", None); - test.add_dummy_mixnode("owner4", None); - test.add_dummy_mixnode("owner5", None); - - let env = test.env(); - let id_key = "identity-key"; - let sphinx_key = "sphinx-key"; - let mut mixnode = mix_node_fixture(); - mixnode.identity_key = id_key.into(); - mixnode.sphinx_key = sphinx_key.into(); - let cost_params = mix_node_cost_params_fixture(); - let owner = Addr::unchecked("mix-owner"); - let pledge = coin(100_000_000, TEST_COIN_DENOM); - - let (id, layer) = save_new_mixnode( - test.deps_mut().storage, - env.clone(), - mixnode, - cost_params.clone(), - owner.clone(), - pledge.clone(), - ) - .unwrap(); - assert_eq!(id, 6); - assert_eq!(layer, Layer::Three); - - assert_eq!( - storage::MIXNODE_ID_COUNTER - .load(test.deps().storage) - .unwrap(), - 6 - ); - assert_eq!(storage::LAYERS.load(test.deps().storage).unwrap().layer3, 2); - let mix_details = get_mixnode_details_by_id(test.deps().storage, id) - .unwrap() - .unwrap(); - assert_eq!(mix_details.mix_id(), id); - assert_eq!(mix_details.original_pledge(), &pledge); - assert_eq!( - mix_details.bond_information.bonding_height, - env.block.height - ); - - // try to add node with duplicate identity... - let mut mixnode = mix_node_fixture(); - mixnode.identity_key = id_key.into(); - let res = save_new_mixnode( - test.deps_mut().storage, - env.clone(), - mixnode, - cost_params.clone(), - Addr::unchecked("different-owner"), - pledge.clone(), - ); - assert!(res.is_err()); - - // and duplicate owner... - let mixnode = mix_node_fixture(); - let res = save_new_mixnode( - test.deps_mut().storage, - env.clone(), - mixnode, - cost_params.clone(), - owner, - pledge.clone(), - ); - assert!(res.is_err()); - - // and duplicate sphinx key... - let mut mixnode = mix_node_fixture(); - mixnode.sphinx_key = sphinx_key.into(); - let res = save_new_mixnode( - test.deps_mut().storage, - env, - mixnode, - cost_params, - Addr::unchecked("different-owner"), - pledge, - ); - assert!(res.is_err()); - } - #[test] fn cleaning_post_unbond_storage() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); - let mix_id_leftover = test.add_dummy_mixnode("mix-owner-leftover", None); + let mix_id = test.add_legacy_mixnode("mix-owner", None); + let mix_id_leftover = test.add_legacy_mixnode("mix-owner-leftover", None); // manually adjust delegation info as to indicate the rewarding information shouldnt get removed let mut rewarding_details = test.mix_rewarding(mix_id_leftover); @@ -516,9 +395,6 @@ pub(crate) mod tests { }; assert_eq!(unbonded_details, expected); - // layers are decremented - assert_eq!(storage::LAYERS.load(test.deps().storage).unwrap().layer1, 0); - let details2 = get_mixnode_details_by_id(test.deps().storage, mix_id_leftover) .unwrap() .unwrap(); @@ -549,8 +425,5 @@ pub(crate) mod tests { unbonding_height: env.block.height, }; assert_eq!(unbonded_details, expected); - - // layers are decremented - assert_eq!(storage::LAYERS.load(test.deps().storage).unwrap().layer2, 0); } } diff --git a/contracts/mixnet/src/mixnodes/mod.rs b/contracts/mixnet/src/mixnodes/mod.rs index ebff4a6ace..f3a3c9941f 100644 --- a/contracts/mixnet/src/mixnodes/mod.rs +++ b/contracts/mixnet/src/mixnodes/mod.rs @@ -3,6 +3,5 @@ pub mod helpers; pub mod queries; -pub mod signature_helpers; pub mod storage; pub mod transactions; diff --git a/contracts/mixnet/src/mixnodes/queries.rs b/contracts/mixnet/src/mixnodes/queries.rs index 5d7b61054d..8300edf5f1 100644 --- a/contracts/mixnet/src/mixnodes/queries.rs +++ b/contracts/mixnet/src/mixnodes/queries.rs @@ -15,17 +15,17 @@ use crate::rewards::storage as rewards_storage; use cosmwasm_std::{Deps, Order, StdResult, Storage}; use cw_storage_plus::Bound; use mixnet_contract_common::mixnode::{ - MixNodeBond, MixNodeDetails, MixnodeRewardingDetailsResponse, PagedMixnodesDetailsResponse, - PagedUnbondedMixnodesResponse, StakeSaturationResponse, UnbondedMixnodeResponse, + MixNodeBond, MixNodeDetails, MixStakeSaturationResponse, MixnodeRewardingDetailsResponse, + PagedMixnodesDetailsResponse, PagedUnbondedMixnodesResponse, UnbondedMixnodeResponse, }; use mixnet_contract_common::{ - IdentityKey, LayerDistribution, MixId, MixOwnershipResponse, MixnodeDetailsByIdentityResponse, - MixnodeDetailsResponse, PagedMixnodeBondsResponse, + IdentityKey, MixOwnershipResponse, MixnodeDetailsByIdentityResponse, MixnodeDetailsResponse, + NodeId, PagedMixnodeBondsResponse, }; pub fn query_mixnode_bonds_paged( deps: Deps<'_>, - start_after: Option, + start_after: Option, limit: Option, ) -> StdResult { let limit = limit @@ -51,7 +51,7 @@ pub fn query_mixnode_bonds_paged( fn attach_node_details( storage: &dyn Storage, - read_bond: StdResult<(MixId, MixNodeBond)>, + read_bond: StdResult<(NodeId, MixNodeBond)>, ) -> StdResult { match read_bond { Ok((_, bond)) => attach_mix_details(storage, bond), @@ -61,7 +61,7 @@ fn attach_node_details( pub fn query_mixnodes_details_paged( deps: Deps<'_>, - start_after: Option, + start_after: Option, limit: Option, ) -> StdResult { let limit = limit @@ -87,7 +87,7 @@ pub fn query_mixnodes_details_paged( pub fn query_unbonded_mixnodes_paged( deps: Deps<'_>, - start_after: Option, + start_after: Option, limit: Option, ) -> StdResult { let limit = limit @@ -113,7 +113,7 @@ pub fn query_unbonded_mixnodes_paged( pub fn query_unbonded_mixnodes_by_owner_paged( deps: Deps<'_>, owner: String, - start_after: Option, + start_after: Option, limit: Option, ) -> StdResult { let owner = deps.api.addr_validate(&owner)?; @@ -144,7 +144,7 @@ pub fn query_unbonded_mixnodes_by_owner_paged( pub fn query_unbonded_mixnodes_by_identity_paged( deps: Deps<'_>, identity_key: String, - start_after: Option, + start_after: Option, limit: Option, ) -> StdResult { let limit = limit @@ -180,7 +180,7 @@ pub fn query_owned_mixnode(deps: Deps<'_>, address: String) -> StdResult, mix_id: MixId) -> StdResult { +pub fn query_mixnode_details(deps: Deps<'_>, mix_id: NodeId) -> StdResult { let mixnode_details = get_mixnode_details_by_id(deps.storage, mix_id)?; Ok(MixnodeDetailsResponse { @@ -203,7 +203,7 @@ pub fn query_mixnode_details_by_identity( pub fn query_mixnode_rewarding_details( deps: Deps<'_>, - mix_id: MixId, + mix_id: NodeId, ) -> StdResult { let rewarding_details = rewards_storage::MIXNODE_REWARDING.may_load(deps.storage, mix_id)?; @@ -213,7 +213,10 @@ pub fn query_mixnode_rewarding_details( }) } -pub fn query_unbonded_mixnode(deps: Deps<'_>, mix_id: MixId) -> StdResult { +pub fn query_unbonded_mixnode( + deps: Deps<'_>, + mix_id: NodeId, +) -> StdResult { let unbonded_info = storage::unbonded_mixnodes().may_load(deps.storage, mix_id)?; Ok(UnbondedMixnodeResponse { @@ -222,11 +225,14 @@ pub fn query_unbonded_mixnode(deps: Deps<'_>, mix_id: MixId) -> StdResult, mix_id: MixId) -> StdResult { +pub fn query_stake_saturation( + deps: Deps<'_>, + mix_id: NodeId, +) -> StdResult { let mix_rewarding = match rewards_storage::MIXNODE_REWARDING.may_load(deps.storage, mix_id)? { Some(mix_rewarding) => mix_rewarding, None => { - return Ok(StakeSaturationResponse { + return Ok(MixStakeSaturationResponse { mix_id, current_saturation: None, uncapped_saturation: None, @@ -236,17 +242,13 @@ pub fn query_stake_saturation(deps: Deps<'_>, mix_id: MixId) -> StdResult) -> StdResult { - storage::LAYERS.load(deps.storage) -} - #[cfg(test)] pub(crate) mod tests { use super::*; @@ -303,7 +305,7 @@ pub(crate) mod tests { // as we add mixnodes, we're always inserting them in ascending manner due to monotonically increasing id let mut test = TestSetup::new(); - test.add_dummy_mixnode("addr1", None); + test.add_legacy_mixnode("addr1", None); let per_page = 2; let page1 = query_mixnode_bonds_paged(test.deps(), None, Some(per_page)).unwrap(); @@ -312,13 +314,13 @@ pub(crate) mod tests { assert_eq!(1, page1.nodes.len()); // save another - test.add_dummy_mixnode("addr2", None); + test.add_legacy_mixnode("addr2", None); // page1 should have 2 results on it let page1 = query_mixnode_bonds_paged(test.deps(), None, Some(per_page)).unwrap(); assert_eq!(2, page1.nodes.len()); - test.add_dummy_mixnode("addr3", None); + test.add_legacy_mixnode("addr3", None); // page1 still has the same 2 results let another_page1 = @@ -334,7 +336,7 @@ pub(crate) mod tests { assert_eq!(1, page2.nodes.len()); // save another one - test.add_dummy_mixnode("addr4", None); + test.add_legacy_mixnode("addr4", None); let page2 = query_mixnode_bonds_paged(test.deps(), Some(start_after), Some(per_page)).unwrap(); @@ -393,7 +395,7 @@ pub(crate) mod tests { // as we add mixnodes, we're always inserting them in ascending manner due to monotonically increasing id let mut test = TestSetup::new(); - test.add_dummy_mixnode("addr1", None); + test.add_legacy_mixnode("addr1", None); let per_page = 2; let page1 = query_mixnodes_details_paged(test.deps(), None, Some(per_page)).unwrap(); @@ -402,13 +404,13 @@ pub(crate) mod tests { assert_eq!(1, page1.nodes.len()); // save another - test.add_dummy_mixnode("addr2", None); + test.add_legacy_mixnode("addr2", None); // page1 should have 2 results on it let page1 = query_mixnodes_details_paged(test.deps(), None, Some(per_page)).unwrap(); assert_eq!(2, page1.nodes.len()); - test.add_dummy_mixnode("addr3", None); + test.add_legacy_mixnode("addr3", None); // page1 still has the same 2 results let another_page1 = @@ -425,7 +427,7 @@ pub(crate) mod tests { assert_eq!(1, page2.nodes.len()); // save another one - test.add_dummy_mixnode("addr4", None); + test.add_legacy_mixnode("addr4", None); let page2 = query_mixnodes_details_paged(test.deps(), Some(start_after), Some(per_page)) @@ -491,7 +493,7 @@ pub(crate) mod tests { #[test] fn pagination_works() { - fn add_unbonded(storage: &mut dyn Storage, id: MixId) { + fn add_unbonded(storage: &mut dyn Storage, id: NodeId) { storage::unbonded_mixnodes() .save( storage, @@ -557,7 +559,7 @@ pub(crate) mod tests { use cosmwasm_std::Addr; use mixnet_contract_common::mixnode::UnbondedMixnode; - fn add_unbonded_with_owner(storage: &mut dyn Storage, id: MixId, owner: &str) { + fn add_unbonded_with_owner(storage: &mut dyn Storage, id: NodeId, owner: &str) { storage::unbonded_mixnodes() .save( storage, @@ -789,7 +791,7 @@ pub(crate) mod tests { use cosmwasm_std::Addr; use mixnet_contract_common::mixnode::UnbondedMixnode; - fn add_unbonded_with_identity(storage: &mut dyn Storage, id: MixId, identity: &str) { + fn add_unbonded_with_identity(storage: &mut dyn Storage, id: NodeId, identity: &str) { storage::unbonded_mixnodes() .save( storage, @@ -1061,7 +1063,7 @@ pub(crate) mod tests { assert_eq!(address, res.address); // when it [fully] exists - let id = test.add_dummy_mixnode(&address, None); + let id = test.add_legacy_mixnode(&address, None); let res = query_owned_mixnode(test.deps(), address.clone()).unwrap(); let details = res.mixnode_details.unwrap(); assert_eq!(address, details.bond_information.owner); @@ -1098,7 +1100,7 @@ pub(crate) mod tests { assert_eq!(42, res.mix_id); // it exists - let mix_id = test.add_dummy_mixnode("foomp", None); + let mix_id = test.add_legacy_mixnode("foomp", None); let res = query_mixnode_details(test.deps(), mix_id).unwrap(); let details = res.mixnode_details.unwrap(); assert_eq!(mix_id, details.bond_information.mix_id); @@ -1120,7 +1122,7 @@ pub(crate) mod tests { assert!(res.is_none()); // it exists - let mix_id = test.add_dummy_mixnode("owner", None); + let mix_id = test.add_legacy_mixnode("owner", None); // this was already tested to be working : ) let expected = query_mixnode_details(test.deps(), mix_id) .unwrap() @@ -1143,13 +1145,10 @@ pub(crate) mod tests { assert!(res.rewarding_details.is_none()); assert_eq!(42, res.mix_id); - let mix_id = test.add_dummy_mixnode("foomp", None); + let mix_id = test.add_legacy_mixnode("foomp", None); let res = query_mixnode_rewarding_details(test.deps(), mix_id).unwrap(); let details = res.rewarding_details.unwrap(); - assert_eq!( - fixtures::mix_node_cost_params_fixture(), - details.cost_params - ); + assert_eq!(fixtures::node_cost_params_fixture(), details.cost_params); assert_eq!(mix_id, res.mix_id); } @@ -1165,7 +1164,7 @@ pub(crate) mod tests { assert_eq!(42, res.mix_id); // add and unbond the mixnode - let mix_id = test.add_dummy_mixnode(sender, None); + let mix_id = test.add_legacy_mixnode(sender, None); pending_events::unbond_mixnode(test.deps_mut(), &mock_env(), 123, mix_id).unwrap(); let res = query_unbonded_mixnode(test.deps(), mix_id).unwrap(); @@ -1188,7 +1187,7 @@ pub(crate) mod tests { .unwrap(); let saturation_point = rewarding_params.interval.stake_saturation_point; - let mix_id = test.add_dummy_mixnode("foomp", None); + let mix_id = test.add_legacy_mixnode("foomp", None); // below saturation point // there's only the base pledge without any delegation diff --git a/contracts/mixnet/src/mixnodes/signature_helpers.rs b/contracts/mixnet/src/mixnodes/signature_helpers.rs deleted file mode 100644 index 04942aaff5..0000000000 --- a/contracts/mixnet/src/mixnodes/signature_helpers.rs +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::signing::storage as signing_storage; -use crate::support::helpers::decode_ed25519_identity_key; -use cosmwasm_std::{Addr, Coin, Deps}; -use mixnet_contract_common::error::MixnetContractError; -use mixnet_contract_common::{ - construct_legacy_mixnode_bonding_sign_payload, construct_mixnode_bonding_sign_payload, MixNode, - MixNodeCostParams, -}; -use nym_contracts_common::signing::MessageSignature; -use nym_contracts_common::signing::Verifier; - -pub(crate) fn verify_mixnode_bonding_signature( - deps: Deps<'_>, - sender: Addr, - pledge: Coin, - mixnode: MixNode, - cost_params: MixNodeCostParams, - signature: MessageSignature, -) -> Result<(), MixnetContractError> { - // recover the public key - let public_key = decode_ed25519_identity_key(&mixnode.identity_key)?; - - // reconstruct the payload, first try the current format, then attempt legacy - let nonce = signing_storage::get_signing_nonce(deps.storage, sender.clone())?; - let msg = construct_mixnode_bonding_sign_payload( - nonce, - sender.clone(), - pledge.clone(), - mixnode.clone(), - cost_params.clone(), - ); - - if deps - .api - .verify_message(msg, signature.clone(), &public_key)? - { - Ok(()) - } else { - // attempt to use legacy - let msg_legacy = construct_legacy_mixnode_bonding_sign_payload( - nonce, - sender, - pledge, - mixnode, - cost_params, - ); - if deps - .api - .verify_message(msg_legacy, signature, &public_key)? - { - Ok(()) - } else { - Err(MixnetContractError::InvalidEd25519Signature) - } - } -} diff --git a/contracts/mixnet/src/mixnodes/storage.rs b/contracts/mixnet/src/mixnodes/storage.rs index 2007456463..1a68bbebfb 100644 --- a/contracts/mixnet/src/mixnodes/storage.rs +++ b/contracts/mixnet/src/mixnodes/storage.rs @@ -2,30 +2,26 @@ // SPDX-License-Identifier: Apache-2.0 use crate::constants::{ - LAYER_DISTRIBUTION_KEY, MIXNODES_IDENTITY_IDX_NAMESPACE, MIXNODES_OWNER_IDX_NAMESPACE, - MIXNODES_PK_NAMESPACE, MIXNODES_SPHINX_IDX_NAMESPACE, NODE_ID_COUNTER_KEY, - PENDING_MIXNODE_CHANGES_NAMESPACE, UNBONDED_MIXNODES_IDENTITY_IDX_NAMESPACE, - UNBONDED_MIXNODES_OWNER_IDX_NAMESPACE, UNBONDED_MIXNODES_PK_NAMESPACE, + MIXNODES_IDENTITY_IDX_NAMESPACE, MIXNODES_OWNER_IDX_NAMESPACE, MIXNODES_PK_NAMESPACE, + MIXNODES_SPHINX_IDX_NAMESPACE, PENDING_MIXNODE_CHANGES_NAMESPACE, + UNBONDED_MIXNODES_IDENTITY_IDX_NAMESPACE, UNBONDED_MIXNODES_OWNER_IDX_NAMESPACE, + UNBONDED_MIXNODES_PK_NAMESPACE, }; -use cosmwasm_std::{StdResult, Storage}; -use cw_storage_plus::{Index, IndexList, IndexedMap, Item, Map, MultiIndex, UniqueIndex}; -use mixnet_contract_common::error::MixnetContractError; +use cw_storage_plus::{Index, IndexList, IndexedMap, Map, MultiIndex, UniqueIndex}; use mixnet_contract_common::mixnode::{PendingMixNodeChanges, UnbondedMixnode}; use mixnet_contract_common::SphinxKey; -use mixnet_contract_common::{Addr, IdentityKey, Layer, LayerDistribution, MixId, MixNodeBond}; +use mixnet_contract_common::{Addr, IdentityKey, MixNodeBond, NodeId}; -pub const LAYERS: Item<'_, LayerDistribution> = Item::new(LAYER_DISTRIBUTION_KEY); -pub const MIXNODE_ID_COUNTER: Item = Item::new(NODE_ID_COUNTER_KEY); -pub const PENDING_MIXNODE_CHANGES: Map = +pub const PENDING_MIXNODE_CHANGES: Map = Map::new(PENDING_MIXNODE_CHANGES_NAMESPACE); // keeps track of `node_id -> IdentityKey, Owner, unbonding_height` so we'd known a bit more about past mixnodes // if we ever decide it's too bloaty, we can deprecate it and start removing all data in // subsequent migrations pub(crate) struct UnbondedMixnodeIndex<'a> { - pub(crate) owner: MultiIndex<'a, Addr, UnbondedMixnode, MixId>, + pub(crate) owner: MultiIndex<'a, Addr, UnbondedMixnode, NodeId>, - pub(crate) identity_key: MultiIndex<'a, IdentityKey, UnbondedMixnode, MixId>, + pub(crate) identity_key: MultiIndex<'a, IdentityKey, UnbondedMixnode, NodeId>, } impl<'a> IndexList for UnbondedMixnodeIndex<'a> { @@ -36,7 +32,7 @@ impl<'a> IndexList for UnbondedMixnodeIndex<'a> { } pub(crate) fn unbonded_mixnodes<'a>( -) -> IndexedMap<'a, MixId, UnbondedMixnode, UnbondedMixnodeIndex<'a>> { +) -> IndexedMap<'a, NodeId, UnbondedMixnode, UnbondedMixnodeIndex<'a>> { let indexes = UnbondedMixnodeIndex { owner: MultiIndex::new( |_pk, d| d.owner.clone(), @@ -71,7 +67,7 @@ impl<'a> IndexList for MixnodeBondIndex<'a> { } // mixnode_bonds() is the storage access function. -pub(crate) fn mixnode_bonds<'a>() -> IndexedMap<'a, MixId, MixNodeBond, MixnodeBondIndex<'a>> { +pub(crate) fn mixnode_bonds<'a>() -> IndexedMap<'a, NodeId, MixNodeBond, MixnodeBondIndex<'a>> { let indexes = MixnodeBondIndex { owner: UniqueIndex::new(|d| d.owner.clone(), MIXNODES_OWNER_IDX_NAMESPACE), identity_key: UniqueIndex::new( @@ -85,130 +81,3 @@ pub(crate) fn mixnode_bonds<'a>() -> IndexedMap<'a, MixId, MixNodeBond, MixnodeB }; IndexedMap::new(MIXNODES_PK_NAMESPACE, indexes) } - -pub fn decrement_layer_count( - storage: &mut dyn Storage, - layer: Layer, -) -> Result<(), MixnetContractError> { - let mut layers = LAYERS.load(storage)?; - layers.decrement_layer_count(layer)?; - Ok(LAYERS.save(storage, &layers)?) -} - -pub(crate) fn assign_layer(store: &mut dyn Storage) -> StdResult { - // load current distribution - let mut layers = LAYERS.load(store)?; - - // choose the one with fewest nodes - let fewest = layers.choose_with_fewest(); - - // increment the existing count - layers.increment_layer_count(fewest); - - // and resave it - LAYERS.save(store, &layers)?; - Ok(fewest) -} - -pub(crate) fn next_mixnode_id_counter(store: &mut dyn Storage) -> StdResult { - let id: MixId = MIXNODE_ID_COUNTER.may_load(store)?.unwrap_or_default() + 1; - MIXNODE_ID_COUNTER.save(store, &id)?; - Ok(id) -} - -pub(crate) fn initialise_storage(storage: &mut dyn Storage) -> StdResult<()> { - LAYERS.save(storage, &LayerDistribution::default()) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::support::tests::test_helpers; - use cosmwasm_std::testing::mock_dependencies; - - #[test] - fn decrementing_layer() { - let mut deps = test_helpers::init_contract(); - - // we never underflow, if it were to happen we're going to return an error instead - assert_eq!( - Err(MixnetContractError::OverflowSubtraction { - minuend: 0, - subtrahend: 1 - }), - decrement_layer_count(deps.as_mut().storage, Layer::One) - ); - - LAYERS - .save( - deps.as_mut().storage, - &LayerDistribution { - layer1: 3, - layer2: 2, - layer3: 1, - }, - ) - .unwrap(); - - assert!(decrement_layer_count(deps.as_mut().storage, Layer::One).is_ok()); - assert!(decrement_layer_count(deps.as_mut().storage, Layer::Two).is_ok()); - assert!(decrement_layer_count(deps.as_mut().storage, Layer::Three).is_ok()); - - assert!(decrement_layer_count(deps.as_mut().storage, Layer::One).is_ok()); - assert!(decrement_layer_count(deps.as_mut().storage, Layer::Two).is_ok()); - assert!(decrement_layer_count(deps.as_mut().storage, Layer::Three).is_err()); - - assert!(decrement_layer_count(deps.as_mut().storage, Layer::One).is_ok()); - assert!(decrement_layer_count(deps.as_mut().storage, Layer::Two).is_err()); - assert!(decrement_layer_count(deps.as_mut().storage, Layer::Three).is_err()); - - assert!(decrement_layer_count(deps.as_mut().storage, Layer::One).is_err()); - assert!(decrement_layer_count(deps.as_mut().storage, Layer::Two).is_err()); - assert!(decrement_layer_count(deps.as_mut().storage, Layer::Three).is_err()); - } - - #[test] - fn assigning_layer() { - let mut deps = test_helpers::init_contract(); - - let layers = LayerDistribution { - layer1: 3, - layer2: 2, - layer3: 1, - }; - LAYERS.save(deps.as_mut().storage, &layers).unwrap(); - - // always assigns layer with fewest nodes - assert_eq!(Layer::Three, assign_layer(deps.as_mut().storage).unwrap()); - assert_eq!(2, LAYERS.load(deps.as_ref().storage).unwrap().layer3); - - // we have 3, 2, 2, so the 2nd layer should get chosen now - assert_eq!(Layer::Two, assign_layer(deps.as_mut().storage).unwrap()); - assert_eq!(3, LAYERS.load(deps.as_ref().storage).unwrap().layer2); - - // 3, 3, 2, so 3rd one again - assert_eq!(Layer::Three, assign_layer(deps.as_mut().storage).unwrap()); - assert_eq!(3, LAYERS.load(deps.as_ref().storage).unwrap().layer3); - } - - #[test] - fn next_id() { - let mut deps = test_helpers::init_contract(); - - for i in 1u32..1000 { - assert_eq!(i, next_mixnode_id_counter(deps.as_mut().storage).unwrap()); - } - } - - #[test] - fn initialising() { - let mut deps = mock_dependencies(); - assert!(LAYERS.load(deps.as_ref().storage).is_err()); - - initialise_storage(deps.as_mut().storage).unwrap(); - assert_eq!( - LayerDistribution::default(), - LAYERS.load(deps.as_ref().storage).unwrap() - ); - } -} diff --git a/contracts/mixnet/src/mixnodes/transactions.rs b/contracts/mixnet/src/mixnodes/transactions.rs index 59ebc432c0..fea26bc24e 100644 --- a/contracts/mixnet/src/mixnodes/transactions.rs +++ b/contracts/mixnet/src/mixnodes/transactions.rs @@ -1,149 +1,73 @@ // Copyright 2021-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use cosmwasm_std::{coin, Coin, DepsMut, Env, MessageInfo, Response, Storage}; -use mixnet_contract_common::error::MixnetContractError; -use mixnet_contract_common::events::{ - new_mixnode_bonding_event, new_mixnode_config_update_event, - new_mixnode_pending_cost_params_update_event, new_pending_mixnode_unbonding_event, - new_pending_pledge_decrease_event, new_pending_pledge_increase_event, +use super::storage; +use crate::compat::helpers::{ + ensure_can_decrease_pledge, ensure_can_increase_pledge, ensure_can_modify_cost_params, }; -use mixnet_contract_common::mixnode::{MixNodeConfigUpdate, MixNodeCostParams}; -use mixnet_contract_common::pending_events::{PendingEpochEventKind, PendingIntervalEventKind}; -use mixnet_contract_common::{Layer, MixId, MixNode}; -use nym_contracts_common::signing::MessageSignature; - use crate::interval::storage as interval_storage; use crate::interval::storage::push_new_interval_event; -use crate::mixnet_contract_settings::storage as mixnet_params_storage; -use crate::mixnet_contract_settings::storage::rewarding_denom; -use crate::mixnodes::helpers::{ - get_mixnode_details_by_owner, must_get_mixnode_bond_by_owner, save_new_mixnode, -}; -use crate::mixnodes::signature_helpers::verify_mixnode_bonding_signature; -use crate::signing::storage as signing_storage; +use crate::mixnodes::helpers::{get_mixnode_details_by_owner, must_get_mixnode_bond_by_owner}; +use crate::nodes::storage as nymnodes_storage; +use crate::nodes::transactions::add_nym_node_inner; use crate::support::helpers::{ - ensure_bonded, ensure_epoch_in_progress_state, ensure_is_authorized, ensure_no_existing_bond, - ensure_no_pending_pledge_changes, ensure_operating_cost_within_range, - ensure_profit_margin_within_range, validate_pledge, + ensure_bonded, ensure_epoch_in_progress_state, ensure_no_pending_params_changes, + ensure_no_pending_pledge_changes, validate_pledge, }; +use cosmwasm_std::{coin, Coin, DepsMut, Env, MessageInfo, Response}; +use mixnet_contract_common::error::MixnetContractError; +use mixnet_contract_common::events::{ + new_migrated_mixnode_event, new_mixnode_config_update_event, + new_pending_cost_params_update_event, new_pending_mixnode_unbonding_event, + new_pending_pledge_decrease_event, new_pending_pledge_increase_event, +}; +use mixnet_contract_common::mixnode::{MixNodeConfigUpdate, NodeCostParams}; +use mixnet_contract_common::pending_events::{PendingEpochEventKind, PendingIntervalEventKind}; +use mixnet_contract_common::{ + MixNode, MixNodeDetails, MixnodeBondingPayload, NymNodeBond, PendingNodeChanges, +}; +use nym_contracts_common::signing::MessageSignature; -use super::storage; - -pub(crate) fn update_mixnode_layer( - mix_id: MixId, - layer: Layer, - storage: &mut dyn Storage, -) -> Result<(), MixnetContractError> { - let bond = if let Some(bond_information) = storage::mixnode_bonds().may_load(storage, mix_id)? { - bond_information - } else { - return Err(MixnetContractError::MixNodeBondNotFound { mix_id }); - }; - let mut updated_bond = bond.clone(); - updated_bond.layer = layer; - - storage::mixnode_bonds().replace(storage, bond.mix_id, Some(&updated_bond), Some(&bond))?; - Ok(()) -} - -pub fn assign_mixnode_layer( - deps: DepsMut<'_>, - info: MessageInfo, - mix_id: MixId, - layer: Layer, -) -> Result { - ensure_is_authorized(&info.sender, deps.storage)?; - - update_mixnode_layer(mix_id, layer, deps.storage)?; - - Ok(Response::default()) -} - -// TODO: perhaps also require the user to explicitly provide what it thinks is the current nonce -// so that we could return a better error message if it doesn't match? -pub(crate) fn try_add_mixnode( +pub fn try_add_mixnode( deps: DepsMut<'_>, env: Env, info: MessageInfo, - mixnode: MixNode, - cost_params: MixNodeCostParams, + mix_node: MixNode, + cost_params: NodeCostParams, owner_signature: MessageSignature, ) -> Result { - // ensure the profit margin is within the defined range - ensure_profit_margin_within_range(deps.storage, cost_params.profit_margin_percent)?; + let signed_payload = MixnodeBondingPayload::new(mix_node.clone(), cost_params.clone()); - // ensure the operating cost is within the defined range - ensure_operating_cost_within_range(deps.storage, &cost_params.interval_operating_cost)?; - - // check if the pledge contains any funds of the appropriate denomination - let minimum_pledge = mixnet_params_storage::minimum_mixnode_pledge(deps.storage)?; - let pledge = validate_pledge(info.funds, minimum_pledge)?; - - // if the client has an active bonded mixnode or gateway, don't allow bonding - // note that this has to be done explicitly as `UniqueIndex` constraint would not protect us - // against attempting to use different node types (i.e. gateways and mixnodes) - ensure_no_existing_bond(&info.sender, deps.storage)?; - - // there's no need to explicitly check whether there already exists mixnode with the same - // identity or sphinx keys as this is going to be done implicitly when attempting to save - // the bond information due to `UniqueIndex` constraint defined on those fields. - - // check if this sender actually owns the mixnode by checking the signature - verify_mixnode_bonding_signature( - deps.as_ref(), - info.sender.clone(), - pledge.clone(), - mixnode.clone(), - cost_params.clone(), - owner_signature, - )?; - - // update the signing nonce associated with this sender so that the future signature would be made on the new value - signing_storage::increment_signing_nonce(deps.storage, info.sender.clone())?; - - let node_identity = mixnode.identity_key.clone(); - let (node_id, layer) = save_new_mixnode( - deps.storage, + // any mixnode added via 'BondMixnode' endpoint should get added as a NymNode + add_nym_node_inner( + deps, env, - mixnode, + info, + mix_node.into(), cost_params, - info.sender.clone(), - pledge.clone(), - )?; - - Ok(Response::new().add_event(new_mixnode_bonding_event( - &info.sender, - &pledge, - &node_identity, - node_id, - layer, - ))) + owner_signature, + signed_payload, + ) } -pub fn try_increase_pledge( +pub fn try_increase_mixnode_pledge( deps: DepsMut<'_>, env: Env, - info: MessageInfo, + increase: Vec, + mix_details: MixNodeDetails, ) -> Result { - let mix_details = get_mixnode_details_by_owner(deps.storage, info.sender.clone())? - .ok_or(MixnetContractError::NoAssociatedMixNodeBond { owner: info.sender })?; let mut pending_changes = mix_details.pending_changes; let mix_id = mix_details.mix_id(); - // increasing pledge is only allowed if the epoch is currently not in the process of being advanced - ensure_epoch_in_progress_state(deps.storage)?; + ensure_can_increase_pledge(deps.storage, &mix_details)?; - ensure_bonded(&mix_details.bond_information)?; - ensure_no_pending_pledge_changes(&pending_changes)?; - - let rewarding_denom = rewarding_denom(deps.storage)?; - let pledge_increase = validate_pledge(info.funds, coin(1, rewarding_denom))?; + let rewarding_denom = &mix_details.original_pledge().denom; + let pledge_increase = validate_pledge(increase, coin(1, rewarding_denom))?; let cosmos_event = new_pending_pledge_increase_event(mix_id, &pledge_increase); // push the event to execute it at the end of the epoch - let epoch_event = PendingEpochEventKind::PledgeMore { + let epoch_event = PendingEpochEventKind::MixnodePledgeMore { mix_id, amount: pledge_increase, }; @@ -154,57 +78,21 @@ pub fn try_increase_pledge( Ok(Response::new().add_event(cosmos_event)) } -pub fn try_decrease_pledge( +pub fn try_decrease_mixnode_pledge( deps: DepsMut<'_>, env: Env, - info: MessageInfo, decrease_by: Coin, + mix_details: MixNodeDetails, ) -> Result { - let mix_details = get_mixnode_details_by_owner(deps.storage, info.sender.clone())? - .ok_or(MixnetContractError::NoAssociatedMixNodeBond { owner: info.sender })?; let mut pending_changes = mix_details.pending_changes; let mix_id = mix_details.mix_id(); - // decreasing pledge is only allowed if the epoch is currently not in the process of being advanced - ensure_epoch_in_progress_state(deps.storage)?; - - ensure_bonded(&mix_details.bond_information)?; - ensure_no_pending_pledge_changes(&pending_changes)?; - - let minimum_pledge = mixnet_params_storage::minimum_mixnode_pledge(deps.storage)?; - - // check that the denomination is correct - if decrease_by.denom != minimum_pledge.denom { - return Err(MixnetContractError::WrongDenom { - received: decrease_by.denom, - expected: minimum_pledge.denom, - }); - } - - // also check if the request contains non-zero amount - // (otherwise it's a no-op and we should we waste gas when resolving events?) - if decrease_by.amount.is_zero() { - return Err(MixnetContractError::ZeroCoinAmount); - } - - // decreasing pledge can't result in the new pledge being lower than the minimum amount - let new_pledge_amount = mix_details - .original_pledge() - .amount - .saturating_sub(decrease_by.amount); - if new_pledge_amount < minimum_pledge.amount { - return Err(MixnetContractError::InvalidPledgeReduction { - current: mix_details.original_pledge().amount, - decrease_by: decrease_by.amount, - minimum: minimum_pledge.amount, - denom: minimum_pledge.denom, - }); - } + ensure_can_decrease_pledge(deps.storage, &mix_details, &decrease_by)?; let cosmos_event = new_pending_pledge_decrease_event(mix_id, &decrease_by); // push the event to execute it at the end of the epoch - let epoch_event = PendingEpochEventKind::DecreasePledge { + let epoch_event = PendingEpochEventKind::MixnodeDecreasePledge { mix_id, decrease_by, }; @@ -289,57 +177,102 @@ pub(crate) fn try_update_mixnode_config( } pub(crate) fn try_update_mixnode_cost_params( - deps: DepsMut<'_>, + deps: DepsMut, env: Env, - info: MessageInfo, - new_costs: MixNodeCostParams, + new_costs: NodeCostParams, + mix_details: MixNodeDetails, ) -> Result { - // see if the node still exists - let existing_bond = must_get_mixnode_bond_by_owner(deps.storage, &info.sender)?; + let mut pending_changes = mix_details.pending_changes; + let mix_id = mix_details.mix_id(); - // changing cost params is only allowed if the epoch is currently not in the process of being advanced - ensure_epoch_in_progress_state(deps.storage)?; + ensure_can_modify_cost_params(deps.storage, &mix_details)?; - ensure_bonded(&existing_bond)?; - - // ensure the profit margin is within the defined range - ensure_profit_margin_within_range(deps.storage, new_costs.profit_margin_percent)?; - - // ensure the operating cost is within the defined range - ensure_operating_cost_within_range(deps.storage, &new_costs.interval_operating_cost)?; - - let cosmos_event = new_mixnode_pending_cost_params_update_event( - existing_bond.mix_id, - &info.sender, - &new_costs, - ); + let cosmos_event = new_pending_cost_params_update_event(mix_id, &new_costs); // push the interval event - let interval_event = PendingIntervalEventKind::ChangeMixCostParams { - mix_id: existing_bond.mix_id, - new_costs, - }; - push_new_interval_event(deps.storage, &env, interval_event)?; + let interval_event = PendingIntervalEventKind::ChangeMixCostParams { mix_id, new_costs }; + let interval_event_id = push_new_interval_event(deps.storage, &env, interval_event)?; + pending_changes.cost_params_change = Some(interval_event_id); + storage::PENDING_MIXNODE_CHANGES.save(deps.storage, mix_id, &pending_changes)?; Ok(Response::new().add_event(cosmos_event)) } +pub fn try_migrate_to_nymnode( + deps: DepsMut, + info: MessageInfo, +) -> Result { + let mix_details = get_mixnode_details_by_owner(deps.storage, info.sender.clone())?.ok_or( + MixnetContractError::NoAssociatedMixNodeBond { + owner: info.sender.clone(), + }, + )?; + let node_id = mix_details.mix_id(); + let pending_changes = mix_details.pending_changes; + let mixnode_bond = mix_details.bond_information; + + if mixnode_bond.proxy.is_some() { + return Err(MixnetContractError::VestingNodeMigration); + } + + ensure_epoch_in_progress_state(deps.storage)?; + ensure_no_pending_pledge_changes(&pending_changes)?; + ensure_no_pending_params_changes(&pending_changes)?; + ensure_bonded(&mixnode_bond)?; + + let mixnode_identity = mixnode_bond.mix_node.identity_key.clone(); + + // remove mixnode bond data + storage::mixnode_bonds().replace(deps.storage, node_id, None, Some(&mixnode_bond))?; + + // NOTE: nothing happens to rewarding data as its structure hasn't changed, and it's accessible under `node_id` key + + // create nym-node entry + // note: since the starting value of nymnode counter was the same one as the final value of mixnode counter, + // we know there's definitely nothing under this key saved. + let nym_node_bond = NymNodeBond::new( + node_id, + mixnode_bond.owner, + mixnode_bond.original_pledge, + mixnode_bond.mix_node, + mixnode_bond.bonding_height, + ); + nymnodes_storage::nym_nodes().save(deps.storage, node_id, &nym_node_bond)?; + + // move pending changes + // TODO: what if node has pending PM change? + storage::PENDING_MIXNODE_CHANGES.remove(deps.storage, node_id); + nymnodes_storage::PENDING_NYMNODE_CHANGES.save( + deps.storage, + node_id, + &PendingNodeChanges::new_empty(), + )?; + + Ok(Response::new().add_event(new_migrated_mixnode_event( + &info.sender, + &mixnode_identity, + node_id, + ))) +} #[cfg(test)] pub mod tests { - use cosmwasm_std::testing::mock_info; - use cosmwasm_std::{Addr, Order, StdResult, Uint128}; - - use mixnet_contract_common::mixnode::PendingMixNodeChanges; - use mixnet_contract_common::{EpochState, EpochStatus, ExecuteMsg, LayerDistribution, Percent}; - + use super::*; + use crate::compat::transactions::{ + try_decrease_pledge, try_increase_pledge, try_update_cost_params, + }; use crate::contract::execute; - use crate::mixnet_contract_settings::storage::minimum_mixnode_pledge; + use crate::mixnet_contract_settings::storage::minimum_node_pledge; use crate::mixnodes::helpers::get_mixnode_details_by_id; + use crate::nodes::helpers::{get_node_details_by_identity, must_get_node_bond_by_owner}; + use crate::signing::storage as signing_storage; use crate::support::tests::fixtures::{good_mixnode_pledge, TEST_COIN_DENOM}; use crate::support::tests::test_helpers::TestSetup; use crate::support::tests::{fixtures, test_helpers}; - - use super::*; + use cosmwasm_std::testing::mock_info; + use cosmwasm_std::{Addr, Order, StdResult, Uint128}; + use mixnet_contract_common::mixnode::PendingMixNodeChanges; + use mixnet_contract_common::nym_node::Role; + use mixnet_contract_common::{EpochState, EpochStatus, ExecuteMsg, Percent}; #[test] fn mixnode_add() { @@ -347,7 +280,7 @@ pub mod tests { let env = test.env(); let sender = "alice"; - let minimum_pledge = minimum_mixnode_pledge(test.deps().storage).unwrap(); + let minimum_pledge = minimum_node_pledge(test.deps().storage).unwrap(); let mut insufficient_pledge = minimum_pledge.clone(); insufficient_pledge.amount -= Uint128::new(1000); @@ -355,7 +288,7 @@ pub mod tests { let info = mock_info(sender, &[insufficient_pledge.clone()]); let (mixnode, sig, _) = test.mixnode_with_signature(sender, Some(vec![insufficient_pledge.clone()])); - let cost_params = fixtures::mix_node_cost_params_fixture(); + let cost_params = fixtures::node_cost_params_fixture(); // we are informed that we didn't send enough funds let result = try_add_mixnode( @@ -378,7 +311,7 @@ pub mod tests { let info = mock_info(sender, &[minimum_pledge]); // if there was already a mixnode bonded by particular user - test.add_dummy_mixnode(sender, None); + test.add_legacy_mixnode(sender, None); // it fails let result = try_add_mixnode( @@ -394,7 +327,7 @@ pub mod tests { // the same holds if the user already owns a gateway let sender2 = "gateway-owner"; - test.add_dummy_gateway(sender2, None); + test.add_legacy_gateway(sender2, None); let info = mock_info(sender2, &tests::fixtures::good_mixnode_pledge()); let (mixnode, sig, _) = test.mixnode_with_signature(sender2, None); @@ -413,24 +346,26 @@ pub mod tests { let msg = ExecuteMsg::UnbondGateway {}; execute(test.deps_mut(), env.clone(), info.clone(), msg).unwrap(); - let result = try_add_mixnode(test.deps_mut(), env, info, mixnode, cost_params, sig); + let result = try_add_mixnode( + test.deps_mut(), + env, + info.clone(), + mixnode.clone(), + cost_params, + sig, + ); assert!(result.is_ok()); + // and the node has been added as a nym-node + let nym_node = get_node_details_by_identity(test.deps().storage, mixnode.identity_key) + .unwrap() + .unwrap(); + assert_eq!(nym_node.bond_information.owner, info.sender); + // make sure we got assigned the next id (note: we have already bonded a mixnode before in this test) let bond = - must_get_mixnode_bond_by_owner(test.deps().storage, &Addr::unchecked(sender2)).unwrap(); - assert_eq!(2, bond.mix_id); - - // and make sure we're on layer 2 (because it was the next empty one) - assert_eq!(Layer::Two, bond.layer); - - // and see if the layer distribution matches our expectation - let expected = LayerDistribution { - layer1: 1, - layer2: 1, - layer3: 0, - }; - assert_eq!(expected, storage::LAYERS.load(test.deps().storage).unwrap()) + must_get_node_bond_by_owner(test.deps().storage, &Addr::unchecked(sender2)).unwrap(); + assert_eq!(2, bond.node_id); } #[test] @@ -444,7 +379,7 @@ pub mod tests { let (mixnode, signature, _) = test.mixnode_with_signature(sender, Some(pledge.clone())); // the above using cost params fixture - let cost_params = fixtures::mix_node_cost_params_fixture(); + let cost_params = fixtures::node_cost_params_fixture(); // using different parameters than what the signature was made on let mut modified_mixnode = mixnode.clone(); @@ -505,7 +440,7 @@ pub mod tests { .unwrap(); assert_eq!(1, updated_nonce); - test.immediately_unbond_mixnode(1); + test.immediately_unbond_node(1); let res = try_add_mixnode(test.deps_mut(), env, info, mixnode, cost_params, signature); assert_eq!(res, Err(MixnetContractError::InvalidEd25519Signature)); } @@ -518,7 +453,9 @@ pub mod tests { final_node_id: 0, }, EpochState::ReconcilingEvents, - EpochState::AdvancingEpoch, + EpochState::RoleAssignment { + next: Role::first(), + }, ]; for bad_state in bad_states { @@ -527,7 +464,7 @@ pub mod tests { let owner = "alice"; let info = mock_info(owner, &[]); - test.add_dummy_mixnode(owner, None); + test.add_legacy_mixnode(owner, None); let mut status = EpochStatus::new(test.rewarding_validator().sender); status.state = bad_state; @@ -558,7 +495,7 @@ pub mod tests { }) ); - let mix_id = test.add_dummy_mixnode(owner, None); + let mix_id = test.add_legacy_mixnode(owner, None); // "normal" unbonding succeeds and unbonding event is pushed to the pending epoch events let res = try_remove_mixnode(test.deps_mut(), env.clone(), info.clone()); @@ -587,7 +524,7 @@ pub mod tests { // prior increase let owner = "mix-owner1"; - test.add_dummy_mixnode(owner, None); + test.add_legacy_mixnode(owner, None); let sender = mock_info(owner, &[test.coin(1000)]); try_increase_pledge(test.deps_mut(), env.clone(), sender.clone()).unwrap(); @@ -601,7 +538,7 @@ pub mod tests { // prior decrease let owner = "mix-owner2"; - test.add_dummy_mixnode(owner, Some(Uint128::new(10000000000))); + test.add_legacy_mixnode(owner, Some(Uint128::new(10000000000))); let sender = mock_info(owner, &[]); let amount = test.coin(1000); try_decrease_pledge(test.deps_mut(), env.clone(), sender, amount).unwrap(); @@ -617,9 +554,10 @@ pub mod tests { // artificial event let owner = "mix-owner3"; - let mix_id = test.add_dummy_mixnode(owner, None); + let mix_id = test.add_legacy_mixnode(owner, None); let pending_change = PendingMixNodeChanges { pledge_change: Some(1234), + cost_params_change: None, }; storage::PENDING_MIXNODE_CHANGES .save(test.deps_mut().storage, mix_id, &pending_change) @@ -659,7 +597,7 @@ pub mod tests { }) ); - let mix_id = test.add_dummy_mixnode(owner, None); + let mix_id = test.add_legacy_mixnode(owner, None); // "normal" update succeeds let res = try_update_mixnode_config(test.deps_mut(), info.clone(), update.clone()); @@ -688,10 +626,12 @@ pub mod tests { final_node_id: 0, }, EpochState::ReconcilingEvents, - EpochState::AdvancingEpoch, + EpochState::RoleAssignment { + next: Role::first(), + }, ]; - let update = MixNodeCostParams { + let update = NodeCostParams { profit_margin_percent: Percent::from_percentage_value(42).unwrap(), interval_operating_cost: Coin::new(12345678, TEST_COIN_DENOM), }; @@ -702,14 +642,13 @@ pub mod tests { let owner = "alice"; let info = mock_info(owner, &[]); - test.add_dummy_mixnode(owner, None); + test.add_legacy_mixnode(owner, None); let mut status = EpochStatus::new(test.rewarding_validator().sender); status.state = bad_state; interval_storage::save_current_epoch_status(test.deps_mut().storage, &status).unwrap(); - let res = - try_update_mixnode_cost_params(test.deps_mut(), env.clone(), info, update.clone()); + let res = try_update_cost_params(test.deps_mut(), env.clone(), info, update.clone()); assert!(matches!( res, Err(MixnetContractError::EpochAdvancementInProgress { .. }) @@ -724,34 +663,26 @@ pub mod tests { let owner = "alice"; let info = mock_info(owner, &[]); - let update = MixNodeCostParams { + let update = NodeCostParams { profit_margin_percent: Percent::from_percentage_value(42).unwrap(), interval_operating_cost: Coin::new(12345678, TEST_COIN_DENOM), }; - // try updating a non existing mixnode bond - let res = try_update_mixnode_cost_params( - test.deps_mut(), - env.clone(), - info.clone(), - update.clone(), - ); + // try updating a non-existing mixnode bond + let res = + try_update_cost_params(test.deps_mut(), env.clone(), info.clone(), update.clone()); assert_eq!( res, - Err(MixnetContractError::NoAssociatedMixNodeBond { + Err(MixnetContractError::NoAssociatedNodeBond { owner: Addr::unchecked(owner) }) ); - let mix_id = test.add_dummy_mixnode(owner, None); + let mix_id = test.add_legacy_mixnode(owner, None); // "normal" update succeeds - let res = try_update_mixnode_cost_params( - test.deps_mut(), - env.clone(), - info.clone(), - update.clone(), - ); + let res = + try_update_cost_params(test.deps_mut(), env.clone(), info.clone(), update.clone()); assert!(res.is_ok()); // see if the event has been pushed onto the queue @@ -781,8 +712,11 @@ pub mod tests { // but we cannot perform any updates whilst the mixnode is already unbonding try_remove_mixnode(test.deps_mut(), env.clone(), info.clone()).unwrap(); - let res = try_update_mixnode_cost_params(test.deps_mut(), env, info, update); - assert_eq!(res, Err(MixnetContractError::MixnodeIsUnbonding { mix_id })) + let res = try_update_cost_params(test.deps_mut(), env, info, update); + assert_eq!( + res, + Err(MixnetContractError::NodeIsUnbonding { node_id: mix_id }) + ) } #[test] @@ -793,7 +727,7 @@ pub mod tests { let keypair1 = nym_crypto::asymmetric::identity::KeyPair::new(&mut test.rng); let keypair2 = nym_crypto::asymmetric::identity::KeyPair::new(&mut test.rng); - let cost_params = fixtures::mix_node_cost_params_fixture(); + let cost_params = fixtures::node_cost_params_fixture(); let mixnode1 = MixNode { host: "1.2.3.4".to_string(), mix_port: 1234, @@ -852,7 +786,9 @@ pub mod tests { final_node_id: 0, }, EpochState::ReconcilingEvents, - EpochState::AdvancingEpoch, + EpochState::RoleAssignment { + next: Role::first(), + }, ]; for bad_state in bad_states { @@ -860,7 +796,7 @@ pub mod tests { let env = test.env(); let owner = "mix-owner"; - test.add_dummy_mixnode(owner, None); + test.add_legacy_mixnode(owner, None); let mut status = EpochStatus::new(test.rewarding_validator().sender); status.state = bad_state; @@ -886,7 +822,7 @@ pub mod tests { let res = try_increase_pledge(test.deps_mut(), env, sender); assert_eq!( res, - Err(MixnetContractError::NoAssociatedMixNodeBond { + Err(MixnetContractError::NoAssociatedNodeBond { owner: Addr::unchecked("not-mix-owner") }) ) @@ -913,8 +849,8 @@ pub mod tests { ); assert_eq!( res, - Err(MixnetContractError::MixnodeIsUnbonding { - mix_id: mix_id_unbonding + Err(MixnetContractError::NodeIsUnbonding { + node_id: mix_id_unbonding }) ); @@ -927,7 +863,7 @@ pub mod tests { ); assert_eq!( res, - Err(MixnetContractError::NoAssociatedMixNodeBond { + Err(MixnetContractError::NoAssociatedNodeBond { owner: owner_unbonded_leftover }) ); @@ -939,7 +875,7 @@ pub mod tests { ); assert_eq!( res, - Err(MixnetContractError::NoAssociatedMixNodeBond { + Err(MixnetContractError::NoAssociatedNodeBond { owner: owner_unbonded }) ) @@ -951,7 +887,7 @@ pub mod tests { let env = test.env(); let owner = "mix-owner"; - test.add_dummy_mixnode(owner, None); + test.add_legacy_mixnode(owner, None); let sender_empty = mock_info(owner, &[]); let res = try_increase_pledge(test.deps_mut(), env.clone(), sender_empty); @@ -975,7 +911,7 @@ pub mod tests { // prior increase let owner = "mix-owner1"; - test.add_dummy_mixnode(owner, None); + test.add_legacy_mixnode(owner, None); let sender = mock_info(owner, &[test.coin(1000)]); try_increase_pledge(test.deps_mut(), env.clone(), sender.clone()).unwrap(); @@ -989,7 +925,7 @@ pub mod tests { // prior decrease let owner = "mix-owner2"; - test.add_dummy_mixnode(owner, Some(Uint128::new(10000000000))); + test.add_legacy_mixnode(owner, Some(Uint128::new(10000000000))); let sender = mock_info(owner, &[]); let amount = test.coin(1000); try_decrease_pledge(test.deps_mut(), env.clone(), sender, amount).unwrap(); @@ -1005,9 +941,10 @@ pub mod tests { // artificial event let owner = "mix-owner3"; - let mix_id = test.add_dummy_mixnode(owner, None); + let mix_id = test.add_legacy_mixnode(owner, None); let pending_change = PendingMixNodeChanges { pledge_change: Some(1234), + cost_params_change: None, }; storage::PENDING_MIXNODE_CHANGES .save(test.deps_mut().storage, mix_id, &pending_change) @@ -1028,7 +965,7 @@ pub mod tests { let mut test = TestSetup::new(); let env = test.env(); let owner = "mix-owner"; - let mix_id = test.add_dummy_mixnode(owner, None); + let mix_id = test.add_legacy_mixnode(owner, None); let events = test.pending_epoch_events(); assert!(events.is_empty()); @@ -1040,7 +977,7 @@ pub mod tests { assert_eq!( events[0].kind, - PendingEpochEventKind::PledgeMore { + PendingEpochEventKind::MixnodePledgeMore { mix_id, amount: test.coin(1000), } @@ -1064,7 +1001,9 @@ pub mod tests { final_node_id: 0, }, EpochState::ReconcilingEvents, - EpochState::AdvancingEpoch, + EpochState::RoleAssignment { + next: Role::first(), + }, ]; for bad_state in bad_states { @@ -1073,7 +1012,7 @@ pub mod tests { let owner = "mix-owner"; let decrease = test.coin(1000); - test.add_dummy_mixnode(owner, Some(Uint128::new(100_000_000_000))); + test.add_legacy_mixnode(owner, Some(Uint128::new(100_000_000_000))); let mut status = EpochStatus::new(test.rewarding_validator().sender); status.state = bad_state; @@ -1100,7 +1039,7 @@ pub mod tests { let res = try_decrease_pledge(test.deps_mut(), env, sender, decrease); assert_eq!( res, - Err(MixnetContractError::NoAssociatedMixNodeBond { + Err(MixnetContractError::NoAssociatedNodeBond { owner: Addr::unchecked("not-mix-owner") }) ) @@ -1132,8 +1071,8 @@ pub mod tests { ); assert_eq!( res, - Err(MixnetContractError::MixnodeIsUnbonding { - mix_id: mix_id_unbonding + Err(MixnetContractError::NodeIsUnbonding { + node_id: mix_id_unbonding }) ); @@ -1147,7 +1086,7 @@ pub mod tests { ); assert_eq!( res, - Err(MixnetContractError::NoAssociatedMixNodeBond { + Err(MixnetContractError::NoAssociatedNodeBond { owner: owner_unbonded_leftover }) ); @@ -1160,7 +1099,7 @@ pub mod tests { ); assert_eq!( res, - Err(MixnetContractError::NoAssociatedMixNodeBond { + Err(MixnetContractError::NoAssociatedNodeBond { owner: owner_unbonded }) ) @@ -1172,10 +1111,10 @@ pub mod tests { let env = test.env(); let owner = "mix-owner"; - let minimum_pledge = minimum_mixnode_pledge(test.deps().storage).unwrap(); + let minimum_pledge = minimum_node_pledge(test.deps().storage).unwrap(); let pledge_amount = minimum_pledge.amount + Uint128::new(100); let pledged = test.coin(pledge_amount.u128()); - test.add_dummy_mixnode(owner, Some(pledge_amount)); + test.add_legacy_mixnode(owner, Some(pledge_amount)); let invalid_decrease = test.coin(150); let valid_decrease = test.coin(50); @@ -1210,7 +1149,7 @@ pub mod tests { let decrease = test.coin(0); let owner = "mix-owner"; - test.add_dummy_mixnode(owner, Some(stake)); + test.add_legacy_mixnode(owner, Some(stake)); let sender = mock_info(owner, &[]); let res = try_decrease_pledge(test.deps_mut(), env, sender, decrease); @@ -1226,7 +1165,7 @@ pub mod tests { // prior increase let owner = "mix-owner1"; - test.add_dummy_mixnode(owner, Some(stake)); + test.add_legacy_mixnode(owner, Some(stake)); let sender = mock_info(owner, &[test.coin(1000)]); try_increase_pledge(test.deps_mut(), env.clone(), sender.clone()).unwrap(); @@ -1240,7 +1179,7 @@ pub mod tests { // prior decrease let owner = "mix-owner2"; - test.add_dummy_mixnode(owner, Some(stake)); + test.add_legacy_mixnode(owner, Some(stake)); let sender = mock_info(owner, &[]); let amount = test.coin(1000); try_decrease_pledge(test.deps_mut(), env.clone(), sender, amount).unwrap(); @@ -1256,9 +1195,10 @@ pub mod tests { // artificial event let owner = "mix-owner3"; - let mix_id = test.add_dummy_mixnode(owner, Some(stake)); + let mix_id = test.add_legacy_mixnode(owner, Some(stake)); let pending_change = PendingMixNodeChanges { pledge_change: Some(1234), + cost_params_change: None, }; storage::PENDING_MIXNODE_CHANGES .save(test.deps_mut().storage, mix_id, &pending_change) @@ -1284,7 +1224,7 @@ pub mod tests { let decrease = test.coin(1000); let owner = "mix-owner"; - let mix_id = test.add_dummy_mixnode(owner, Some(stake)); + let mix_id = test.add_legacy_mixnode(owner, Some(stake)); let events = test.pending_epoch_events(); assert!(events.is_empty()); @@ -1296,7 +1236,7 @@ pub mod tests { assert_eq!( events[0].kind, - PendingEpochEventKind::DecreasePledge { + PendingEpochEventKind::MixnodeDecreasePledge { mix_id, decrease_by: decrease, } diff --git a/contracts/mixnet/src/nodes/helpers.rs b/contracts/mixnet/src/nodes/helpers.rs new file mode 100644 index 0000000000..b9b2706380 --- /dev/null +++ b/contracts/mixnet/src/nodes/helpers.rs @@ -0,0 +1,183 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::interval::storage as interval_storage; +use crate::nodes::storage; +use crate::nodes::storage::next_nymnode_id_counter; +use crate::rewards::storage as rewards_storage; +use cosmwasm_std::{Addr, Coin, Env, StdResult, Storage}; +use mixnet_contract_common::error::MixnetContractError; +use mixnet_contract_common::nym_node::UnbondedNymNode; +use mixnet_contract_common::{ + NodeCostParams, NodeId, NodeRewarding, NymNode, NymNodeBond, NymNodeDetails, PendingNodeChanges, +}; +use nym_contracts_common::IdentityKey; + +pub(crate) fn save_new_nymnode( + storage: &mut dyn Storage, + bonding_height: u64, + node: NymNode, + cost_params: NodeCostParams, + owner: Addr, + pledge: Coin, +) -> Result { + let node_id = next_nymnode_id_counter(storage)?; + save_new_nymnode_with_id( + storage, + node_id, + bonding_height, + node, + cost_params, + owner, + pledge, + )?; + + Ok(node_id) +} + +pub(crate) fn save_new_nymnode_with_id( + storage: &mut dyn Storage, + node_id: NodeId, + bonding_height: u64, + node: NymNode, + cost_params: NodeCostParams, + owner: Addr, + pledge: Coin, +) -> Result<(), MixnetContractError> { + let current_epoch = interval_storage::current_interval(storage)?.current_epoch_absolute_id(); + + let node_rewarding = NodeRewarding::initialise_new(cost_params, &pledge, current_epoch)?; + let node_bond = NymNodeBond::new(node_id, owner, pledge, node, bonding_height); + + // save node bond data + // note that this implicitly checks for uniqueness on identity key and owner + storage::nym_nodes().save(storage, node_id, &node_bond)?; + + // save rewarding data + rewards_storage::NYMNODE_REWARDING.save(storage, node_id, &node_rewarding)?; + + // initialise pending changes + storage::PENDING_NYMNODE_CHANGES.save(storage, node_id, &PendingNodeChanges::new_empty())?; + + Ok(()) +} + +pub(crate) fn attach_nym_node_details( + store: &dyn Storage, + bond_information: NymNodeBond, +) -> StdResult { + // if bond exists, rewarding details MUST also exist + let rewarding_details = + rewards_storage::NYMNODE_REWARDING.load(store, bond_information.node_id)?; + + // the same is true for the pending changes + let pending_changes = storage::PENDING_NYMNODE_CHANGES.load(store, bond_information.node_id)?; + + Ok(NymNodeDetails::new( + bond_information, + rewarding_details, + pending_changes, + )) +} + +pub(crate) fn get_node_details_by_id( + store: &dyn Storage, + mix_id: NodeId, +) -> StdResult> { + if let Some(bond_information) = storage::nym_nodes().may_load(store, mix_id)? { + attach_nym_node_details(store, bond_information).map(Some) + } else { + Ok(None) + } +} + +pub(crate) fn get_node_details_by_owner( + store: &dyn Storage, + address: Addr, +) -> StdResult> { + if let Some(bond_information) = storage::nym_nodes() + .idx + .owner + .item(store, address)? + .map(|record| record.1) + { + attach_nym_node_details(store, bond_information).map(Some) + } else { + Ok(None) + } +} + +pub(crate) fn get_node_details_by_identity( + store: &dyn Storage, + identity: IdentityKey, +) -> StdResult> { + if let Some(bond_information) = storage::nym_nodes() + .idx + .identity_key + .item(store, identity)? + .map(|record| record.1) + { + attach_nym_node_details(store, bond_information).map(Some) + } else { + Ok(None) + } +} + +pub(crate) fn must_get_node_bond_by_owner( + store: &dyn Storage, + owner: &Addr, +) -> Result { + Ok(storage::nym_nodes() + .idx + .owner + .item(store, owner.clone())? + .ok_or(MixnetContractError::NoAssociatedNodeBond { + owner: owner.clone(), + })? + .1) +} + +pub(crate) fn cleanup_post_unbond_nym_node_storage( + storage: &mut dyn Storage, + env: &Env, + current_details: &NymNodeDetails, +) -> Result<(), MixnetContractError> { + let node_id = current_details.bond_information.node_id; + // remove all bond information since we don't need it anymore + // note that "normal" remove is `may_load` followed by `replace` with a `None` + // and we have already loaded the data from the storage + storage::nym_nodes().replace( + storage, + node_id, + None, + Some(¤t_details.bond_information), + )?; + + // if there are no pending delegations to return, we can also + // purge all information regarding rewarding parameters + if current_details.rewarding_details.unique_delegations == 0 { + rewards_storage::NYMNODE_REWARDING.remove(storage, node_id); + } else { + // otherwise just set operator's tokens to zero as to indicate they have unbonded + // and already claimed those + let zeroed = current_details.rewarding_details.clear_operator(); + rewards_storage::NYMNODE_REWARDING.save(storage, node_id, &zeroed)?; + } + + let identity_key = current_details.bond_information.identity().to_owned(); + let owner = current_details.bond_information.owner.clone(); + + // save minimal information about this node + storage::unbonded_nym_nodes().save( + storage, + node_id, + &UnbondedNymNode { + identity_key, + node_id, + owner, + unbonding_height: env.block.height, + }, + )?; + + Ok(()) +} diff --git a/contracts/mixnet/src/nodes/mod.rs b/contracts/mixnet/src/nodes/mod.rs new file mode 100644 index 0000000000..7707e3b780 --- /dev/null +++ b/contracts/mixnet/src/nodes/mod.rs @@ -0,0 +1,8 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod helpers; +pub mod queries; +mod signature_helpers; +pub mod storage; +pub mod transactions; diff --git a/contracts/mixnet/src/nodes/queries.rs b/contracts/mixnet/src/nodes/queries.rs new file mode 100644 index 0000000000..6283d21d75 --- /dev/null +++ b/contracts/mixnet/src/nodes/queries.rs @@ -0,0 +1,259 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::constants::{ + NYM_NODE_BOND_DEFAULT_RETRIEVAL_LIMIT, NYM_NODE_BOND_MAX_RETRIEVAL_LIMIT, + NYM_NODE_DETAILS_DEFAULT_RETRIEVAL_LIMIT, NYM_NODE_DETAILS_MAX_RETRIEVAL_LIMIT, + UNBONDED_NYM_NODES_DEFAULT_RETRIEVAL_LIMIT, UNBONDED_NYM_NODES_MAX_RETRIEVAL_LIMIT, +}; +use crate::nodes::helpers::{ + attach_nym_node_details, get_node_details_by_id, get_node_details_by_identity, + get_node_details_by_owner, +}; +use crate::nodes::storage; +use crate::rewards::storage as rewards_storage; +use cosmwasm_std::{Deps, Order, StdResult, Storage}; +use cw_storage_plus::Bound; +use mixnet_contract_common::error::MixnetContractError; +use mixnet_contract_common::nym_node::{ + EpochAssignmentResponse, NodeDetailsByIdentityResponse, NodeDetailsResponse, + NodeOwnershipResponse, NodeRewardingDetailsResponse, PagedNymNodeBondsResponse, + PagedNymNodeDetailsResponse, PagedUnbondedNymNodesResponse, Role, RolesMetadataResponse, + StakeSaturationResponse, UnbondedNodeResponse, +}; +use mixnet_contract_common::{NodeId, NymNodeBond, NymNodeDetails}; +use nym_contracts_common::IdentityKey; + +pub(crate) fn query_nymnode_bonds_paged( + deps: Deps<'_>, + start_after: Option, + limit: Option, +) -> StdResult { + let limit = limit + .unwrap_or(NYM_NODE_BOND_DEFAULT_RETRIEVAL_LIMIT) + .min(NYM_NODE_BOND_MAX_RETRIEVAL_LIMIT) as usize; + + let start = start_after.map(Bound::exclusive); + + let nodes = storage::nym_nodes() + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|res| res.map(|item| item.1)) + .collect::>>()?; + + let start_next_after = nodes.last().map(|node| node.node_id); + + Ok(PagedNymNodeBondsResponse { + nodes, + start_next_after, + }) +} + +pub(crate) fn query_rewarded_set_metadata( + deps: Deps<'_>, +) -> Result { + let metadata = storage::read_rewarded_set_metadata(deps.storage)?; + Ok(RolesMetadataResponse { metadata }) +} + +pub(crate) fn query_epoch_assignment( + deps: Deps<'_>, + role: Role, +) -> Result { + let metadata = storage::read_rewarded_set_metadata(deps.storage)?; + let nodes = storage::read_assigned_roles(deps.storage, role)?; + Ok(EpochAssignmentResponse { + epoch_id: metadata.epoch_id, + nodes, + }) +} + +fn attach_node_details( + storage: &dyn Storage, + read_bond: StdResult<(NodeId, NymNodeBond)>, +) -> StdResult { + match read_bond { + Ok((_, bond)) => attach_nym_node_details(storage, bond), + Err(err) => Err(err), + } +} + +pub fn query_nymnodes_details_paged( + deps: Deps<'_>, + start_after: Option, + limit: Option, +) -> StdResult { + let limit = limit + .unwrap_or(NYM_NODE_DETAILS_DEFAULT_RETRIEVAL_LIMIT) + .min(NYM_NODE_DETAILS_MAX_RETRIEVAL_LIMIT) as usize; + + let start = start_after.map(Bound::exclusive); + + let nodes = storage::nym_nodes() + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|res| attach_node_details(deps.storage, res)) + .collect::>>()?; + + let start_next_after = nodes.last().map(|details| details.node_id()); + + Ok(PagedNymNodeDetailsResponse { + nodes, + start_next_after, + }) +} + +pub fn query_unbonded_nymnodes_paged( + deps: Deps<'_>, + start_after: Option, + limit: Option, +) -> StdResult { + let limit = limit + .unwrap_or(UNBONDED_NYM_NODES_DEFAULT_RETRIEVAL_LIMIT) + .min(UNBONDED_NYM_NODES_MAX_RETRIEVAL_LIMIT) as usize; + + let start = start_after.map(Bound::exclusive); + + let nodes = storage::unbonded_nym_nodes() + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|res| res.map(|item| item.1)) + .collect::>>()?; + + let start_next_after = nodes.last().map(|res| res.node_id); + + Ok(PagedUnbondedNymNodesResponse { + nodes, + start_next_after, + }) +} + +pub fn query_unbonded_nymnodes_by_owner_paged( + deps: Deps<'_>, + owner: String, + start_after: Option, + limit: Option, +) -> StdResult { + let owner = deps.api.addr_validate(&owner)?; + + let limit = limit + .unwrap_or(UNBONDED_NYM_NODES_DEFAULT_RETRIEVAL_LIMIT) + .min(UNBONDED_NYM_NODES_MAX_RETRIEVAL_LIMIT) as usize; + + let start = start_after.map(Bound::exclusive); + + let nodes = storage::unbonded_nym_nodes() + .idx + .owner + .prefix(owner) + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|r| r.map(|r| r.1)) + .collect::>>()?; + + let start_next_after = nodes.last().map(|res| res.node_id); + + Ok(PagedUnbondedNymNodesResponse { + nodes, + start_next_after, + }) +} + +pub fn query_unbonded_nymnodes_by_identity_paged( + deps: Deps<'_>, + identity_key: String, + start_after: Option, + limit: Option, +) -> StdResult { + let limit = limit + .unwrap_or(UNBONDED_NYM_NODES_DEFAULT_RETRIEVAL_LIMIT) + .min(UNBONDED_NYM_NODES_MAX_RETRIEVAL_LIMIT) as usize; + + let start = start_after.map(Bound::exclusive); + + let nodes = storage::unbonded_nym_nodes() + .idx + .identity_key + .prefix(identity_key) + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|r| r.map(|r| r.1)) + .collect::>>()?; + + let start_next_after = nodes.last().map(|res| res.node_id); + + Ok(PagedUnbondedNymNodesResponse { + nodes, + start_next_after, + }) +} + +pub fn query_owned_nymnode(deps: Deps<'_>, address: String) -> StdResult { + let validated_addr = deps.api.addr_validate(&address)?; + + let details = get_node_details_by_owner(deps.storage, validated_addr.clone())?; + Ok(NodeOwnershipResponse { + address: validated_addr, + details, + }) +} + +pub fn query_nymnode_details(deps: Deps<'_>, node_id: NodeId) -> StdResult { + let details = get_node_details_by_id(deps.storage, node_id)?; + + Ok(NodeDetailsResponse { node_id, details }) +} + +pub fn query_nymnode_details_by_identity( + deps: Deps<'_>, + identity_key: IdentityKey, +) -> StdResult { + let details = get_node_details_by_identity(deps.storage, identity_key.clone())?; + + Ok(NodeDetailsByIdentityResponse { + identity_key, + details, + }) +} + +pub fn query_nymnode_rewarding_details( + deps: Deps<'_>, + node_id: NodeId, +) -> StdResult { + let rewarding_details = rewards_storage::MIXNODE_REWARDING.may_load(deps.storage, node_id)?; + + Ok(NodeRewardingDetailsResponse { + node_id, + rewarding_details, + }) +} + +pub fn query_unbonded_nymnode(deps: Deps<'_>, node_id: NodeId) -> StdResult { + let details = storage::unbonded_nym_nodes().may_load(deps.storage, node_id)?; + + Ok(UnbondedNodeResponse { node_id, details }) +} + +pub fn query_stake_saturation( + deps: Deps<'_>, + node_id: NodeId, +) -> StdResult { + let node_rewarding = match rewards_storage::NYMNODE_REWARDING.may_load(deps.storage, node_id)? { + Some(node_rewarding) => node_rewarding, + None => { + return Ok(StakeSaturationResponse { + node_id, + current_saturation: None, + uncapped_saturation: None, + }) + } + }; + + let rewarding_params = rewards_storage::REWARDING_PARAMS.load(deps.storage)?; + + Ok(StakeSaturationResponse { + node_id, + current_saturation: Some(node_rewarding.bond_saturation(&rewarding_params)), + uncapped_saturation: Some(node_rewarding.uncapped_bond_saturation(&rewarding_params)), + }) +} diff --git a/contracts/mixnet/src/nodes/signature_helpers.rs b/contracts/mixnet/src/nodes/signature_helpers.rs new file mode 100644 index 0000000000..db2e074540 --- /dev/null +++ b/contracts/mixnet/src/nodes/signature_helpers.rs @@ -0,0 +1,38 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::signing::storage as signing_storage; +use crate::support::helpers::decode_ed25519_identity_key; +use cosmwasm_std::{Addr, Coin, Deps}; +use mixnet_contract_common::construct_generic_node_bonding_payload; +use mixnet_contract_common::error::MixnetContractError; +use nym_contracts_common::signing::Verifier; +use nym_contracts_common::signing::{MessageSignature, SigningPurpose}; +use nym_contracts_common::IdentityKeyRef; +use serde::Serialize; + +/// Verifies the bonding signature on either a legacy mixnode, legacy gateway or a nym-node. +pub(crate) fn verify_bonding_signature( + deps: Deps<'_>, + sender: Addr, + identity_key: IdentityKeyRef, + pledge: Coin, + message: T, + signature: MessageSignature, +) -> Result<(), MixnetContractError> +where + T: SigningPurpose + Serialize, +{ + // recover the public key + let public_key = decode_ed25519_identity_key(identity_key)?; + + // reconstruct the payload + let nonce = signing_storage::get_signing_nonce(deps.storage, sender.clone())?; + let msg = construct_generic_node_bonding_payload(nonce, sender, pledge, message); + + if deps.api.verify_message(msg, signature, &public_key)? { + Ok(()) + } else { + Err(MixnetContractError::InvalidEd25519Signature) + } +} diff --git a/contracts/mixnet/src/nodes/storage/helpers.rs b/contracts/mixnet/src/nodes/storage/helpers.rs new file mode 100644 index 0000000000..ab59cd2617 --- /dev/null +++ b/contracts/mixnet/src/nodes/storage/helpers.rs @@ -0,0 +1,152 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::nodes::storage::rewarded_set::{ACTIVE_ROLES_BUCKET, ROLES, ROLES_METADATA}; +use crate::nodes::storage::{nym_nodes, NYMNODE_ID_COUNTER}; +use cosmwasm_std::{StdResult, Storage}; +use mixnet_contract_common::error::MixnetContractError; +use mixnet_contract_common::nym_node::{RewardedSetMetadata, Role}; +use mixnet_contract_common::{EpochId, NodeId, NymNodeBond, RoleAssignment}; +use serde::{Deserialize, Serialize}; + +#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, Eq, PartialEq)] +#[repr(u8)] +pub enum RoleStorageBucket { + #[default] + A = 0, + B = 1, +} + +impl RoleStorageBucket { + pub fn other(&self) -> Self { + match self { + RoleStorageBucket::A => RoleStorageBucket::B, + RoleStorageBucket::B => RoleStorageBucket::A, + } + } + + pub fn swap(&self) -> Self { + self.other() + } +} + +pub(crate) fn reset_inactive_metadata( + storage: &mut dyn Storage, + epoch_id: EpochId, +) -> Result<(), MixnetContractError> { + let active_bucket = ACTIVE_ROLES_BUCKET.load(storage)?; + let inactive = active_bucket.other() as u8; + + ROLES_METADATA.save(storage, inactive, &RewardedSetMetadata::new(epoch_id))?; + Ok(()) +} + +pub(crate) fn save_assignment( + storage: &mut dyn Storage, + assignment: RoleAssignment, +) -> Result<(), MixnetContractError> { + let active_bucket = ACTIVE_ROLES_BUCKET.load(storage)?; + + // we're always assigning to the INACTIVE bucket, because it's still being built + let inactive = active_bucket.other() as u8; + + // update metadata + let mut metadata = ROLES_METADATA.load(storage, inactive)?; + let last = assignment.nodes.last().copied().unwrap_or_default(); + metadata.set_highest_id(last, assignment.role); + metadata.set_role_count(assignment.role, assignment.nodes.len() as u32); + if assignment.is_final_assignment() { + metadata.fully_assigned = true + } + ROLES_METADATA.save(storage, inactive, &metadata)?; + + // set the actual roles + Ok(ROLES.save(storage, (inactive, assignment.role), &assignment.nodes)?) +} + +pub(crate) fn read_rewarded_set_metadata( + storage: &dyn Storage, +) -> Result { + let active_bucket = ACTIVE_ROLES_BUCKET.load(storage)?; + Ok(ROLES_METADATA.load(storage, active_bucket as u8)?) +} + +pub(crate) fn read_assigned_roles( + storage: &dyn Storage, + role: Role, +) -> Result, MixnetContractError> { + let active_bucket = ACTIVE_ROLES_BUCKET.load(storage)?; + // we're always reading from the ACTIVE bucket + Ok(ROLES.load(storage, (active_bucket as u8, role))?) +} + +pub(crate) fn swap_active_role_bucket( + storage: &mut dyn Storage, +) -> Result<(), MixnetContractError> { + let active_bucket = ACTIVE_ROLES_BUCKET.load(storage)?; + Ok(ACTIVE_ROLES_BUCKET.save(storage, &active_bucket.swap())?) +} + +pub(crate) fn set_unbonding( + storage: &mut dyn Storage, + bond: &NymNodeBond, +) -> Result<(), MixnetContractError> { + let mut updated_bond = bond.clone(); + updated_bond.is_unbonding = true; + nym_nodes().replace(storage, bond.node_id, Some(&updated_bond), Some(bond))?; + Ok(()) +} + +pub(crate) fn next_nymnode_id_counter(store: &mut dyn Storage) -> StdResult { + let id: NodeId = NYMNODE_ID_COUNTER.may_load(store)?.unwrap_or_default() + 1; + NYMNODE_ID_COUNTER.save(store, &id)?; + Ok(id) +} + +pub(crate) fn initialise_storage(storage: &mut dyn Storage) -> Result<(), MixnetContractError> { + ACTIVE_ROLES_BUCKET.save(storage, &RoleStorageBucket::default())?; + let roles = vec![ + Role::Layer1, + Role::Layer2, + Role::Layer3, + Role::EntryGateway, + Role::ExitGateway, + Role::Standby, + ]; + for role in roles { + ROLES.save(storage, (RoleStorageBucket::default() as u8, role), &vec![])?; + ROLES.save( + storage, + (RoleStorageBucket::default().other() as u8, role), + &vec![], + )? + } + + ROLES_METADATA.save( + storage, + RoleStorageBucket::default() as u8, + &Default::default(), + )?; + ROLES_METADATA.save( + storage, + RoleStorageBucket::default().other() as u8, + &Default::default(), + )?; + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::support::tests::test_helpers; + + #[test] + fn next_id() { + let mut deps = test_helpers::init_contract(); + + for i in 1u32..1000 { + assert_eq!(i, next_nymnode_id_counter(deps.as_mut().storage).unwrap()); + } + } +} diff --git a/contracts/mixnet/src/nodes/storage/mod.rs b/contracts/mixnet/src/nodes/storage/mod.rs new file mode 100644 index 0000000000..c431e4af08 --- /dev/null +++ b/contracts/mixnet/src/nodes/storage/mod.rs @@ -0,0 +1,128 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::constants::{ + NODE_ID_COUNTER_KEY, NYMNODE_ACTIVE_ROLE_ASSIGNMENT_KEY, NYMNODE_IDENTITY_IDX_NAMESPACE, + NYMNODE_OWNER_IDX_NAMESPACE, NYMNODE_PK_NAMESPACE, NYMNODE_REWARDED_SET_METADATA_NAMESPACE, + NYMNODE_ROLES_ASSIGNMENT_NAMESPACE, PENDING_NYMNODE_CHANGES_NAMESPACE, + UNBONDED_NYMNODE_IDENTITY_IDX_NAMESPACE, UNBONDED_NYMNODE_OWNER_IDX_NAMESPACE, + UNBONDED_NYMNODE_PK_NAMESPACE, +}; +use crate::nodes::storage::helpers::RoleStorageBucket; +use cosmwasm_std::Addr; +use cw_storage_plus::{Index, IndexList, IndexedMap, Item, Map, MultiIndex, UniqueIndex}; +use mixnet_contract_common::nym_node::{NymNodeBond, RewardedSetMetadata, Role, UnbondedNymNode}; +use mixnet_contract_common::{NodeId, PendingNodeChanges}; +use nym_contracts_common::IdentityKey; + +pub(crate) mod helpers; + +pub(crate) use helpers::*; + +// IMPORTANT NOTE: we're using the same storage key as we had for MIXNODE_ID_COUNTER, +// so that we could start from the old values +pub const NYMNODE_ID_COUNTER: Item = Item::new(NODE_ID_COUNTER_KEY); + +// each nym-node has 3 storage buckets: +// - `NymNodeBondIndex` to keep track of the actual node information +// - `PENDING_NYMNODE_CHANGES` to keep track of current params/pledge changes +// - `rewards_storage::NYMNODE_REWARDING` to keep track of data needed for reward calculation + +pub const PENDING_NYMNODE_CHANGES: Map = + Map::new(PENDING_NYMNODE_CHANGES_NAMESPACE); + +pub mod rewarded_set { + use super::*; + + // role assignment period is an awkward time for querying for up-to-date data + // for example if we have assigned layer1 and layer2 but not yet touched layer3, + // the state would be inconsistent since it'd have data of layer3 from previous epoch + // + // thus we just toggle the virtual pointer between 2 buckets + // since we also don't want to keep state for all epochs. + // + // general rule of thumb: we're always READING from the active bucket, + // but we're WRITING to the inactive bucket (because it's still being built) + /// Item keeping track of the current active node assignment + pub const ACTIVE_ROLES_BUCKET: Item = + Item::new(NYMNODE_ACTIVE_ROLE_ASSIGNMENT_KEY); + + // NOTES FOR FUTURE IMPLEMENTATION: + // to implement pre-announcement of nodes, you don't have to do much. literally almost nothing at all, + // you'd just have to expose the current inactive bucket and make sure to correctly invalidate it when being written to + + // it feels more efficient to have a single bulk read/write operation per role + // as opposed to storing everything under separate keys. + // however, the drawback is a potentially huge writing cost, but I don't think + // we're going to have 1k+ nodes per layer any time soon for it to be a problem + // + // note: the actual resolution of which node id corresponds to which ip/identity + // is left to up the caller + // + // storage note: we use `u8` rather than `RoleStorageBucket` in the composite key + // to avoid having to derive all required traits + /// Storage map of `(RoleStorageBucket, Role)` => set of nodes with that assigned role + pub const ROLES: Map<(u8, Role), Vec> = Map::new(NYMNODE_ROLES_ASSIGNMENT_NAMESPACE); + + /// Storage map of metadata associated with particular `RoleStorageBucket` + pub const ROLES_METADATA: Map = + Map::new(NYMNODE_REWARDED_SET_METADATA_NAMESPACE); +} + +pub(crate) struct NymNodeBondIndex<'a> { + pub(crate) owner: UniqueIndex<'a, Addr, NymNodeBond>, + + pub(crate) identity_key: UniqueIndex<'a, IdentityKey, NymNodeBond>, +} + +impl<'a> IndexList for NymNodeBondIndex<'a> { + fn get_indexes(&'_ self) -> Box> + '_> { + let v: Vec<&dyn Index> = vec![&self.owner, &self.identity_key]; + Box::new(v.into_iter()) + } +} + +// nym_nodes() is the storage access function. +pub(crate) fn nym_nodes<'a>() -> IndexedMap<'a, NodeId, NymNodeBond, NymNodeBondIndex<'a>> { + let indexes = NymNodeBondIndex { + owner: UniqueIndex::new(|d| d.owner.clone(), NYMNODE_OWNER_IDX_NAMESPACE), + identity_key: UniqueIndex::new( + |d| d.node.identity_key.clone(), + NYMNODE_IDENTITY_IDX_NAMESPACE, + ), + }; + IndexedMap::new(NYMNODE_PK_NAMESPACE, indexes) +} + +// keeps track of `node_id -> IdentityKey, Owner, unbonding_height` so we'd known a bit more about past nodes +// if we ever decide it's too bloaty, we can deprecate it and start removing all data in +// subsequent migrations +pub(crate) struct UnbondedNymNodeIndex<'a> { + pub(crate) owner: MultiIndex<'a, Addr, UnbondedNymNode, NodeId>, + + pub(crate) identity_key: MultiIndex<'a, IdentityKey, UnbondedNymNode, NodeId>, +} + +impl<'a> IndexList for UnbondedNymNodeIndex<'a> { + fn get_indexes(&'_ self) -> Box> + '_> { + let v: Vec<&dyn Index> = vec![&self.owner, &self.identity_key]; + Box::new(v.into_iter()) + } +} + +pub(crate) fn unbonded_nym_nodes<'a>( +) -> IndexedMap<'a, NodeId, UnbondedNymNode, UnbondedNymNodeIndex<'a>> { + let indexes = UnbondedNymNodeIndex { + owner: MultiIndex::new( + |_pk, d| d.owner.clone(), + UNBONDED_NYMNODE_PK_NAMESPACE, + UNBONDED_NYMNODE_OWNER_IDX_NAMESPACE, + ), + identity_key: MultiIndex::new( + |_pk, d| d.identity_key.clone(), + UNBONDED_NYMNODE_PK_NAMESPACE, + UNBONDED_NYMNODE_IDENTITY_IDX_NAMESPACE, + ), + }; + IndexedMap::new(UNBONDED_NYMNODE_PK_NAMESPACE, indexes) +} diff --git a/contracts/mixnet/src/nodes/transactions.rs b/contracts/mixnet/src/nodes/transactions.rs new file mode 100644 index 0000000000..646bd48dd9 --- /dev/null +++ b/contracts/mixnet/src/nodes/transactions.rs @@ -0,0 +1,264 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::compat::helpers::{ + ensure_can_decrease_pledge, ensure_can_increase_pledge, ensure_can_modify_cost_params, +}; +use crate::interval::storage as interval_storage; +use crate::interval::storage::push_new_interval_event; +use crate::mixnet_contract_settings::storage as mixnet_params_storage; +use crate::nodes::helpers::{must_get_node_bond_by_owner, save_new_nymnode}; +use crate::nodes::signature_helpers::verify_bonding_signature; +use crate::nodes::storage; +use crate::nodes::storage::set_unbonding; +use crate::signing::storage as signing_storage; +use crate::support::helpers::{ + ensure_epoch_in_progress_state, ensure_no_existing_bond, ensure_operating_cost_within_range, + ensure_profit_margin_within_range, validate_pledge, +}; +use cosmwasm_std::{coin, Coin, DepsMut, Env, MessageInfo, Response}; +use mixnet_contract_common::error::MixnetContractError; +use mixnet_contract_common::events::{ + new_nym_node_bonding_event, new_pending_cost_params_update_event, + new_pending_nym_node_unbonding_event, new_pending_pledge_decrease_event, + new_pending_pledge_increase_event, +}; +use mixnet_contract_common::nym_node::{NodeConfigUpdate, NymNode}; +use mixnet_contract_common::{ + NodeCostParams, NymNodeBondingPayload, NymNodeDetails, PendingEpochEventKind, + PendingIntervalEventKind, +}; +use nym_contracts_common::signing::{MessageSignature, SigningPurpose}; +use serde::Serialize; + +pub fn try_add_nym_node( + deps: DepsMut<'_>, + env: Env, + info: MessageInfo, + nym_node: NymNode, + cost_params: NodeCostParams, + owner_signature: MessageSignature, +) -> Result { + // TODO: here be backwards compatibility checks for making sure there's no pre-existing mixnode/gateway + + add_nym_node_inner( + deps, + env, + info, + nym_node.clone(), + cost_params.clone(), + owner_signature, + NymNodeBondingPayload::new(nym_node, cost_params), + ) +} + +// allow bonding nym-node through mixnode/gateway entry points for backwards compatibility, +// and make sure to check correct signatures +pub(crate) fn add_nym_node_inner( + deps: DepsMut<'_>, + env: Env, + info: MessageInfo, + nym_node: NymNode, + cost_params: NodeCostParams, + owner_signature: MessageSignature, + signed_message_payload: T, +) -> Result +where + T: SigningPurpose + Serialize, +{ + // ensure the provided values for host and public key are not insane + nym_node.ensure_host_in_range()?; + nym_node.naive_ensure_valid_pubkey()?; + + // check if the pledge contains any funds of the appropriate denomination + let minimum_pledge = mixnet_params_storage::minimum_node_pledge(deps.storage)?; + let pledge = validate_pledge(info.funds, minimum_pledge)?; + + // ensure the profit margin is within the defined range + ensure_profit_margin_within_range(deps.storage, cost_params.profit_margin_percent)?; + + // ensure the operating cost is within the defined range + ensure_operating_cost_within_range(deps.storage, &cost_params.interval_operating_cost)?; + + // if the client has an active bonded [legacy] mixnode, [legacy] gateway or a nym-node, don't allow bonding + // note that this has to be done explicitly as `UniqueIndex` constraint would not protect us + // against attempting to use different node types (i.e. gateways and mixnodes) + ensure_no_existing_bond(&info.sender, deps.storage)?; + + // there's no need to explicitly check whether there already exists nymnode with the same + // identity as this is going to be done implicitly when attempting to save + // the bond information due to `UniqueIndex` constraint defined on that field. + + // check if this sender actually owns the node by checking the signature + verify_bonding_signature( + deps.as_ref(), + info.sender.clone(), + &nym_node.identity_key, + pledge.clone(), + signed_message_payload, + owner_signature, + )?; + + // update the signing nonce associated with this sender so that the future signature would be made on the new value + signing_storage::increment_signing_nonce(deps.storage, info.sender.clone())?; + + let node_identity = nym_node.identity_key.clone(); + let node_id = save_new_nymnode( + deps.storage, + env.block.height, + nym_node, + cost_params, + info.sender.clone(), + pledge.clone(), + )?; + + Ok(Response::new().add_event(new_nym_node_bonding_event( + &info.sender, + &pledge, + &node_identity, + node_id, + ))) +} + +pub(crate) fn try_remove_nym_node( + deps: DepsMut<'_>, + env: Env, + info: MessageInfo, +) -> Result { + let existing_bond = must_get_node_bond_by_owner(deps.storage, &info.sender)?; + let pending_changes = + storage::PENDING_NYMNODE_CHANGES.load(deps.storage, existing_bond.node_id)?; + + // unbonding is only allowed if the epoch is currently not in the process of being advanced + ensure_epoch_in_progress_state(deps.storage)?; + + // see if the proxy matches + existing_bond.ensure_bonded()?; + + // if there are any pending requests to change the pledge, wait for them to resolve before allowing the unbonding + pending_changes.ensure_no_pending_pledge_changes()?; + + // set `is_unbonding` field + set_unbonding(deps.storage, &existing_bond)?; + + // push the event to execute it at the end of the epoch + let epoch_event = PendingEpochEventKind::UnbondNymNode { + node_id: existing_bond.node_id, + }; + interval_storage::push_new_epoch_event(deps.storage, &env, epoch_event)?; + + Ok( + Response::new().add_event(new_pending_nym_node_unbonding_event( + &existing_bond.owner, + existing_bond.identity(), + existing_bond.node_id, + )), + ) +} + +pub(crate) fn try_update_node_config( + deps: DepsMut<'_>, + info: MessageInfo, + update: NodeConfigUpdate, +) -> Result { + let existing_bond = must_get_node_bond_by_owner(deps.storage, &info.sender)?; + existing_bond.ensure_bonded()?; + + let mut updated_bond = existing_bond.clone(); + + if let Some(updated_host) = update.host { + updated_bond.node.host = updated_host; + } + + if let Some(updated_custom_http_port) = update.custom_http_port { + updated_bond.node.custom_http_port = Some(updated_custom_http_port); + } + + if update.restore_default_http_port { + updated_bond.node.custom_http_port = None + } + + storage::nym_nodes().replace( + deps.storage, + existing_bond.node_id, + Some(&updated_bond), + Some(&existing_bond), + )?; + + Ok(Response::new()) +} + +pub(crate) fn try_increase_nym_node_pledge( + deps: DepsMut<'_>, + env: Env, + increase: Vec, + node_details: NymNodeDetails, +) -> Result { + let mut pending_changes = node_details.pending_changes; + let node_id = node_details.node_id(); + + ensure_can_increase_pledge(deps.storage, &node_details)?; + + let rewarding_denom = &node_details.original_pledge().denom; + let pledge_increase = validate_pledge(increase, coin(1, rewarding_denom))?; + + let cosmos_event = new_pending_pledge_increase_event(node_id, &pledge_increase); + + // push the event to execute it at the end of the epoch + let epoch_event = PendingEpochEventKind::NymNodePledgeMore { + node_id, + amount: pledge_increase, + }; + let epoch_event_id = interval_storage::push_new_epoch_event(deps.storage, &env, epoch_event)?; + pending_changes.pledge_change = Some(epoch_event_id); + storage::PENDING_NYMNODE_CHANGES.save(deps.storage, node_id, &pending_changes)?; + + Ok(Response::new().add_event(cosmos_event)) +} + +pub(crate) fn try_decrease_nym_node_pledge( + deps: DepsMut<'_>, + env: Env, + decrease_by: Coin, + node_details: NymNodeDetails, +) -> Result { + let mut pending_changes = node_details.pending_changes; + let node_id = node_details.node_id(); + + ensure_can_decrease_pledge(deps.storage, &node_details, &decrease_by)?; + + let cosmos_event = new_pending_pledge_decrease_event(node_id, &decrease_by); + + // push the event to execute it at the end of the epoch + let epoch_event = PendingEpochEventKind::NymNodeDecreasePledge { + node_id, + decrease_by, + }; + let epoch_event_id = interval_storage::push_new_epoch_event(deps.storage, &env, epoch_event)?; + pending_changes.pledge_change = Some(epoch_event_id); + storage::PENDING_NYMNODE_CHANGES.save(deps.storage, node_id, &pending_changes)?; + + Ok(Response::new().add_event(cosmos_event)) +} + +pub(crate) fn try_update_nym_node_cost_params( + deps: DepsMut, + env: Env, + new_costs: NodeCostParams, + node_details: NymNodeDetails, +) -> Result { + let mut pending_changes = node_details.pending_changes; + let node_id = node_details.node_id(); + + ensure_can_modify_cost_params(deps.storage, &node_details)?; + + let cosmos_event = new_pending_cost_params_update_event(node_id, &new_costs); + + // push the interval event + let interval_event = PendingIntervalEventKind::ChangeNymNodeCostParams { node_id, new_costs }; + let interval_event_id = push_new_interval_event(deps.storage, &env, interval_event)?; + pending_changes.cost_params_change = Some(interval_event_id); + storage::PENDING_NYMNODE_CHANGES.save(deps.storage, node_id, &pending_changes)?; + + Ok(Response::new().add_event(cosmos_event)) +} diff --git a/contracts/mixnet/src/queued_migrations.rs b/contracts/mixnet/src/queued_migrations.rs index a0273196be..9521172bcb 100644 --- a/contracts/mixnet/src/queued_migrations.rs +++ b/contracts/mixnet/src/queued_migrations.rs @@ -1,2 +1,303 @@ // Copyright 2022-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 + +mod families_purge { + use cosmwasm_std::{DepsMut, Order, StdResult}; + use cw_storage_plus::{Index, IndexList, IndexedMap, Map, UniqueIndex}; + use mixnet_contract_common::error::MixnetContractError; + use nym_contracts_common::IdentityKey; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + pub const FAMILIES_INDEX_NAMESPACE: &str = "faml2"; + pub const FAMILIES_MAP_NAMESPACE: &str = "fam2"; + pub const MEMBERS_MAP_NAMESPACE: &str = "memb2"; + + type FamilyHeadKey = IdentityKey; + + #[derive(Serialize, Deserialize, Clone)] + pub struct Family { + /// Owner of this family. + head: FamilyHead, + + /// Optional proxy (i.e. vesting contract address) used when creating the family. + proxy: Option, + + /// Human readable label for this family. + label: String, + } + + #[derive(Debug, Clone, Eq, PartialEq)] + pub struct FamilyHead(IdentityKey); + + impl Serialize for FamilyHead { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + self.0.serialize(serializer) + } + } + + impl<'de> Deserialize<'de> for FamilyHead { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let inner = IdentityKey::deserialize(deserializer)?; + Ok(FamilyHead(inner)) + } + } + + pub struct FamilyIndex<'a> { + pub label: UniqueIndex<'a, FamilyHeadKey, Family>, + } + + impl<'a> IndexList for FamilyIndex<'a> { + fn get_indexes(&'_ self) -> Box> + '_> { + let v: Vec<&dyn Index> = vec![&self.label]; + Box::new(v.into_iter()) + } + } + + pub fn families<'a>() -> IndexedMap<'a, FamilyHeadKey, Family, FamilyIndex<'a>> { + let indexes = FamilyIndex { + label: UniqueIndex::new(|d| d.label.to_string(), FAMILIES_INDEX_NAMESPACE), + }; + IndexedMap::new(FAMILIES_MAP_NAMESPACE, indexes) + } + + pub const MEMBERS: Map = Map::new(MEMBERS_MAP_NAMESPACE); + + pub(crate) fn families_purge(deps: DepsMut) -> Result<(), MixnetContractError> { + // we don't care about values, we are only concerned with keys + let family_keys = families() + .keys(deps.storage, None, None, Order::Ascending) + .collect::>>()?; + for family in family_keys { + families().remove(deps.storage, family)?; + } + + let member_keys = MEMBERS + .keys(deps.storage, None, None, Order::Ascending) + .collect::>>()?; + for member in member_keys { + MEMBERS.remove(deps.storage, member); + } + + Ok(()) + } +} + +mod nym_nodes_usage { + use crate::constants::{CONTRACT_STATE_KEY, REWARDING_PARAMS_KEY}; + use crate::mixnet_contract_settings::storage::CONTRACT_STATE; + use crate::rewards::storage::RewardingStorage; + use crate::support::helpers::ensure_epoch_in_progress_state; + use cosmwasm_std::{Addr, Coin, DepsMut, Order, StdResult, Storage}; + use cw_storage_plus::{Item, Map}; + use mixnet_contract_common::error::MixnetContractError; + use mixnet_contract_common::reward_params::RewardedSetParams; + use mixnet_contract_common::{ + ContractState, ContractStateParams, IntervalRewardParams, MigrateMsg, NodeId, + OperatingCostRange, PendingIntervalEvent, PendingIntervalEventKind, ProfitMarginRange, + RewardingParams, + }; + use serde::{Deserialize, Serialize}; + + fn migrate_contract_state(storage: &mut dyn Storage) -> Result<(), MixnetContractError> { + #[derive(Serialize, Deserialize)] + struct OldContractState { + owner: Option, + rewarding_validator_address: Addr, + vesting_contract_address: Addr, + rewarding_denom: String, + params: OldContractStateParams, + } + + #[derive(Serialize, Deserialize)] + struct OldContractStateParams { + minimum_mixnode_delegation: Option, + minimum_mixnode_pledge: Coin, + minimum_gateway_pledge: Coin, + #[serde(default)] + profit_margin: ProfitMarginRange, + #[serde(default)] + interval_operating_cost: OperatingCostRange, + } + + let old_state_entry = Item::new(CONTRACT_STATE_KEY); + let old_state: OldContractState = old_state_entry.load(storage)?; + + #[allow(deprecated)] + CONTRACT_STATE.save( + storage, + &ContractState { + owner: old_state.owner, + rewarding_validator_address: old_state.rewarding_validator_address, + vesting_contract_address: old_state.vesting_contract_address, + rewarding_denom: old_state.rewarding_denom, + params: ContractStateParams { + minimum_delegation: old_state.params.minimum_mixnode_delegation, + // just use the same value for nym-node pledge as we have for mixnodes + minimum_pledge: old_state.params.minimum_mixnode_pledge, + profit_margin: old_state.params.profit_margin, + interval_operating_cost: old_state.params.interval_operating_cost, + }, + }, + )?; + + Ok(()) + } + + fn migrate_pending_interval_changes( + storage: &mut dyn Storage, + ) -> Result<(), MixnetContractError> { + // at the time of writing this migration there were just 15 pending interval events, + // so if we stay within this order of magnitude, it's quite safe to just grab all of them + let events = crate::interval::storage::PENDING_INTERVAL_EVENTS + .range(storage, None, None, Order::Ascending) + .map(|res| res.map(|row| row.into())) + .collect::>>()?; + + for event in events { + if let PendingIntervalEventKind::ChangeMixCostParams { mix_id, .. } = event.event.kind { + let mut pending = crate::mixnodes::storage::PENDING_MIXNODE_CHANGES + .may_load(storage, mix_id)? + .unwrap_or_default(); + pending.cost_params_change = Some(event.id); + crate::mixnodes::storage::PENDING_MIXNODE_CHANGES + .save(storage, mix_id, &pending)?; + } + } + + Ok(()) + } + + fn preassign_gateway_ids(storage: &mut dyn Storage) -> Result<(), MixnetContractError> { + // that one is a big if. we have ~100 gateways so we **might** be able to fit it within migration. + // if not, then we'll have to do it in batches/change our approach + + let gateways = crate::gateways::storage::gateways() + .range(storage, None, None, Order::Ascending) + .map(|res| res.map(|row| row.1)) + .collect::>>()?; + + for gateway in gateways { + let id = crate::nodes::storage::next_nymnode_id_counter(storage)?; + crate::gateways::storage::PREASSIGNED_LEGACY_IDS.save( + storage, + gateway.gateway.identity_key, + &id, + )?; + } + + Ok(()) + } + + fn cleanup_legacy_storage(storage: &mut dyn Storage) -> Result<(), MixnetContractError> { + #[derive(Copy, Clone, Default, Serialize, Deserialize)] + pub struct LayerDistribution { + pub layer1: u64, + pub layer2: u64, + pub layer3: u64, + } + pub const LAYERS: Item<'_, LayerDistribution> = Item::new("layers"); + + #[derive(Copy, Clone, Serialize, Deserialize)] + #[serde(deny_unknown_fields, rename_all = "snake_case")] + pub enum RewardedSetNodeStatus { + /// Node that is currently active, i.e. is expected to be used by clients for mixing packets. + #[serde(alias = "Active")] + Active, + + /// Node that is currently in standby, i.e. it's present in the rewarded set but is not active. + #[serde(alias = "Standby")] + Standby, + } + pub(crate) const REWARDED_SET: Map = Map::new("rs"); + + // remove explicit layer assignment -> got replaced with role assignment + LAYERS.remove(storage); + + // remove every node from the legacy rewarded set + let rewarded_ids = REWARDED_SET + .keys(storage, None, None, Order::Ascending) + .collect::, _>>()?; + + for node_id in rewarded_ids { + REWARDED_SET.remove(storage, node_id) + } + + Ok(()) + } + + fn migrate_rewarded_set_params(storage: &mut dyn Storage) -> Result<(), MixnetContractError> { + #[derive(Copy, Clone, Serialize, Deserialize)] + pub struct LegacyRewardingParams { + pub interval: IntervalRewardParams, + pub rewarded_set_size: u32, + pub active_set_size: u32, + } + pub(crate) const REWARDING_PARAMS: Item<'_, LegacyRewardingParams> = + Item::new(REWARDING_PARAMS_KEY); + + let legacy = REWARDING_PARAMS.load(storage)?; + + // our mainnet assumption. we could work around it, + // but what's the point of the extra logic if we might not need it? + if legacy.rewarded_set_size != 240 || legacy.active_set_size != 240 { + return Err(MixnetContractError::FailedMigration { + comment: "the current active or rewarded set size is not 240 (the expected value for mainnet)".to_string(), + }); + } + + let updated = RewardingParams { + interval: legacy.interval, + rewarded_set: RewardedSetParams { + entry_gateways: 50, + exit_gateways: 70, + mixnodes: 120, + standby: 0, + }, + }; + + RewardingStorage::load() + .global_rewarding_params + .save(storage, &updated)?; + + Ok(()) + } + + pub(crate) fn migrate_to_nym_nodes_usage( + deps: DepsMut<'_>, + _msg: &MigrateMsg, + ) -> Result<(), MixnetContractError> { + // ensure we're not migrating mid-epoch progression, or we're gonna have bad time + ensure_epoch_in_progress_state(deps.storage)?; + + // update the contract state structure (remove separate mixnode/gateway pledge amount) + migrate_contract_state(deps.storage)?; + + // make sure to assign pending cost params changes to mixnodes so those nodes couldn't be migrated + // to nym-nodes until the events are resolved + migrate_pending_interval_changes(deps.storage)?; + + // pre-assign NodeId to all gateways (that will be applied during nym-node migration) + // to simplify all other code during the intermediate period + preassign_gateway_ids(deps.storage)?; + + // initialise all the storage structures required by nym-nodes + crate::nodes::storage::initialise_storage(deps.storage)?; + + // update the simple active/rewarded set sizes to actually contain the distribution of roles + migrate_rewarded_set_params(deps.storage)?; + + // remove all redundant storage items + cleanup_legacy_storage(deps.storage)?; + + Ok(()) + } +} + +pub(crate) use families_purge::families_purge; +pub(crate) use nym_nodes_usage::migrate_to_nym_nodes_usage; diff --git a/contracts/mixnet/src/rewards/helpers.rs b/contracts/mixnet/src/rewards/helpers.rs index 689c263efb..dfd07956ce 100644 --- a/contracts/mixnet/src/rewards/helpers.rs +++ b/contracts/mixnet/src/rewards/helpers.rs @@ -4,16 +4,18 @@ use super::storage; use crate::delegations::storage as delegations_storage; use crate::interval::storage as interval_storage; +use crate::nodes::storage::read_assigned_roles; use cosmwasm_std::{Coin, Storage}; use mixnet_contract_common::error::MixnetContractError; -use mixnet_contract_common::helpers::IntoBaseDecimal; -use mixnet_contract_common::mixnode::{MixNodeDetails, MixNodeRewarding}; -use mixnet_contract_common::{Delegation, EpochState, EpochStatus, MixId}; +use mixnet_contract_common::helpers::{IntoBaseDecimal, NodeBond, NodeDetails}; +use mixnet_contract_common::mixnode::NodeRewarding; +use mixnet_contract_common::nym_node::Role; +use mixnet_contract_common::{Delegation, EpochState, EpochStatus, NodeId}; pub(crate) fn update_and_save_last_rewarded( storage: &mut dyn Storage, mut current_epoch_status: EpochStatus, - new_last_rewarded: MixId, + new_last_rewarded: NodeId, ) -> Result<(), MixnetContractError> { let is_done = current_epoch_status.update_last_rewarded(new_last_rewarded)?; if is_done { @@ -39,7 +41,7 @@ pub(crate) fn apply_reward_pool_changes( let epoch_reward_budget = reward_pool / interval.epochs_in_interval().into_base_decimal()? * rewarding_params.interval.interval_pool_emission; let stake_saturation_point = - staking_supply / rewarding_params.rewarded_set_size.into_base_decimal()?; + staking_supply / rewarding_params.rewarded_set_size().into_base_decimal()?; rewarding_params.interval.reward_pool = reward_pool; rewarding_params.interval.staking_supply = staking_supply; @@ -52,26 +54,29 @@ pub(crate) fn apply_reward_pool_changes( Ok(()) } -pub(crate) fn withdraw_operator_reward( +pub(crate) fn withdraw_operator_reward( store: &mut dyn Storage, - mix_details: MixNodeDetails, -) -> Result { - let mix_id = mix_details.mix_id(); - let mut mix_rewarding = mix_details.rewarding_details; - let original_pledge = mix_details.bond_information.original_pledge; - let reward = mix_rewarding.withdraw_operator_reward(&original_pledge)?; + node_details: D, +) -> Result +where + D: NodeDetails, +{ + let (bond_info, mut node_rewarding, _) = node_details.split(); + let node_id = bond_info.node_id(); + let original_pledge = bond_info.original_pledge(); + let reward = node_rewarding.withdraw_operator_reward(original_pledge)?; // save updated rewarding info - storage::MIXNODE_REWARDING.save(store, mix_id, &mix_rewarding)?; + storage::NYMNODE_REWARDING.save(store, node_id, &node_rewarding)?; Ok(reward) } pub(crate) fn withdraw_delegator_reward( store: &mut dyn Storage, delegation: Delegation, - mut mix_rewarding: MixNodeRewarding, + mut mix_rewarding: NodeRewarding, ) -> Result { - let mix_id = delegation.mix_id; + let mix_id = delegation.node_id; let mut updated_delegation = delegation.clone(); let reward = mix_rewarding.withdraw_delegator_reward(&mut updated_delegation)?; @@ -86,6 +91,47 @@ pub(crate) fn withdraw_delegator_reward( Ok(reward) } +pub(crate) fn ensure_assignment( + storage: &dyn Storage, + node_id: NodeId, + role: Role, +) -> Result<(), MixnetContractError> { + // that's a bit expensive to read the whole thing each time, but I'm not sure if there's a much better way + // (creating a reverse map would be more expensive in the long run due to writes being more costly than reads) + let assignment = read_assigned_roles(storage, role)?; + if !assignment.contains(&node_id) { + return Err(MixnetContractError::IncorrectEpochRole { node_id, role }); + } + Ok(()) +} + +// this is **ONLY** to be used in queries +// unless a better way can be figured out +pub(crate) fn expensive_role_lookup( + storage: &dyn Storage, + node_id: NodeId, +) -> Result, MixnetContractError> { + if ensure_assignment(storage, node_id, Role::EntryGateway).is_ok() { + return Ok(Some(Role::EntryGateway)); + } + if ensure_assignment(storage, node_id, Role::ExitGateway).is_ok() { + return Ok(Some(Role::ExitGateway)); + } + if ensure_assignment(storage, node_id, Role::Layer1).is_ok() { + return Ok(Some(Role::Layer1)); + } + if ensure_assignment(storage, node_id, Role::Layer2).is_ok() { + return Ok(Some(Role::Layer2)); + } + if ensure_assignment(storage, node_id, Role::Layer3).is_ok() { + return Ok(Some(Role::Layer3)); + } + if ensure_assignment(storage, node_id, Role::Standby).is_ok() { + return Ok(Some(Role::Standby)); + } + Ok(None) +} + #[cfg(test)] mod tests { use super::*; @@ -145,10 +191,7 @@ mod tests { assert_eq!( updated_rewarding_params.interval.stake_saturation_point, updated_rewarding_params.interval.staking_supply - / updated_rewarding_params - .rewarded_set_size - .into_base_decimal() - .unwrap() + / updated_rewarding_params.dec_rewarded_set_size() ); // resets changes back to 0 @@ -197,10 +240,7 @@ mod tests { assert_eq!( updated_rewarding_params2.interval.stake_saturation_point, updated_rewarding_params2.interval.staking_supply - / updated_rewarding_params2 - .rewarded_set_size - .into_base_decimal() - .unwrap() + / updated_rewarding_params2.dec_rewarded_set_size() ); // resets changes back to 0 @@ -218,7 +258,7 @@ mod tests { let pledge = Uint128::new(250_000_000); let pledge_dec = 250_000_000u32.into_base_decimal().unwrap(); - let mix_id = test.add_dummy_mixnode("mix-owner", Some(pledge)); + let mix_id = test.add_legacy_mixnode("mix-owner", Some(pledge)); // no rewards let mix_details = get_mixnode_details_by_id(test.deps().storage, mix_id) @@ -229,13 +269,16 @@ mod tests { test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![mix_id]); - let dist1 = test.reward_with_distribution_with_state_bypass(mix_id, performance(100.0)); + let dist1 = + test.legacy_reward_with_distribution_with_state_bypass(mix_id, performance(100.0)); test.skip_to_next_epoch_end(); - let dist2 = test.reward_with_distribution_with_state_bypass(mix_id, performance(100.0)); + let dist2 = + test.legacy_reward_with_distribution_with_state_bypass(mix_id, performance(100.0)); test.skip_to_next_epoch_end(); - let dist3 = test.reward_with_distribution_with_state_bypass(mix_id, performance(100.0)); + let dist3 = + test.legacy_reward_with_distribution_with_state_bypass(mix_id, performance(100.0)); let mix_details = get_mixnode_details_by_id(test.deps().storage, mix_id) .unwrap() @@ -255,7 +298,7 @@ mod tests { let delegation_amount = Uint128::new(2_500_000_000); let delegation_dec = 2_500_000_000_u32.into_base_decimal().unwrap(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_legacy_mixnode("mix-owner", None); let delegator = "delegator"; test.add_immediate_delegation(delegator, delegation_amount, mix_id); @@ -268,13 +311,16 @@ mod tests { test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![mix_id]); - let dist1 = test.reward_with_distribution_with_state_bypass(mix_id, performance(100.0)); + let dist1 = + test.legacy_reward_with_distribution_with_state_bypass(mix_id, performance(100.0)); test.skip_to_next_epoch_end(); - let dist2 = test.reward_with_distribution_with_state_bypass(mix_id, performance(100.0)); + let dist2 = + test.legacy_reward_with_distribution_with_state_bypass(mix_id, performance(100.0)); test.skip_to_next_epoch_end(); - let dist3 = test.reward_with_distribution_with_state_bypass(mix_id, performance(100.0)); + let dist3 = + test.legacy_reward_with_distribution_with_state_bypass(mix_id, performance(100.0)); let delegation_pre = test.delegation(mix_id, delegator, &None); let mix_rewarding = test.mix_rewarding(mix_id); diff --git a/contracts/mixnet/src/rewards/queries.rs b/contracts/mixnet/src/rewards/queries.rs index 87df0f9fc8..53b7ab9f99 100644 --- a/contracts/mixnet/src/rewards/queries.rs +++ b/contracts/mixnet/src/rewards/queries.rs @@ -1,39 +1,28 @@ // Copyright 2021-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use super::storage; +use super::{helpers, storage}; +use crate::compat; +use crate::compat::helpers::may_get_bond; use crate::delegations::storage as delegations_storage; use crate::interval::storage as interval_storage; -use crate::mixnodes; -use crate::mixnodes::storage as mixnodes_storage; use cosmwasm_std::{coin, Coin, Decimal, Deps, StdResult}; +use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::helpers::into_base_decimal; -use mixnet_contract_common::mixnode::MixNodeDetails; -use mixnet_contract_common::reward_params::{NodeRewardParams, Performance, RewardingParams}; +use mixnet_contract_common::nym_node::Role; +use mixnet_contract_common::reward_params::{ + NodeRewardingParameters, Performance, RewardingParams, WorkFactor, +}; use mixnet_contract_common::rewarding::helpers::truncate_reward; use mixnet_contract_common::rewarding::{ EstimatedCurrentEpochRewardResponse, PendingRewardResponse, }; -use mixnet_contract_common::{Delegation, MixId}; +use mixnet_contract_common::{Delegation, NodeId}; pub(crate) fn query_rewarding_params(deps: Deps<'_>) -> StdResult { storage::REWARDING_PARAMS.load(deps.storage) } -fn pending_operator_reward( - mix_details: Option, -) -> StdResult { - Ok(match mix_details { - Some(mix_details) => PendingRewardResponse { - amount_staked: Some(mix_details.original_pledge().clone()), - amount_earned: Some(mix_details.pending_operator_reward()), - amount_earned_detailed: Some(mix_details.pending_detailed_operator_reward()?), - mixnode_still_fully_bonded: !mix_details.is_unbonding(), - }, - None => PendingRewardResponse::default(), - }) -} - pub fn query_pending_operator_reward( deps: Deps, owner: String, @@ -41,24 +30,20 @@ pub fn query_pending_operator_reward( let owner_address = deps.api.addr_validate(&owner)?; // in order to determine operator's reward we need to know its original pledge and thus // we have to load the entire thing - let mix_details = mixnodes::helpers::get_mixnode_details_by_owner(deps.storage, owner_address)?; - pending_operator_reward(mix_details) + compat::queries::rewards::pending_operator_reward(deps, owner_address) } pub fn query_pending_mixnode_operator_reward( deps: Deps, - mix_id: MixId, + node_id: NodeId, ) -> StdResult { - // in order to determine operator's reward we need to know its original pledge and thus - // we have to load the entire thing - let mix_details = mixnodes::helpers::get_mixnode_details_by_id(deps.storage, mix_id)?; - pending_operator_reward(mix_details) + compat::queries::rewards::pending_operator_reward_by_id(deps, node_id) } pub fn query_pending_delegator_reward( deps: Deps, owner: String, - mix_id: MixId, + node_id: NodeId, proxy: Option, ) -> StdResult { let owner_address = deps.api.addr_validate(&owner)?; @@ -66,28 +51,32 @@ pub fn query_pending_delegator_reward( .map(|proxy| deps.api.addr_validate(&proxy)) .transpose()?; - let mix_rewarding = match storage::MIXNODE_REWARDING.may_load(deps.storage, mix_id)? { + let node_rewarding = match storage::NYMNODE_REWARDING.may_load(deps.storage, node_id)? { Some(mix_rewarding) => mix_rewarding, None => return Ok(PendingRewardResponse::default()), }; - let storage_key = Delegation::generate_storage_key(mix_id, &owner_address, proxy.as_ref()); + let storage_key = Delegation::generate_storage_key(node_id, &owner_address, proxy.as_ref()); let delegation = match delegations_storage::delegations().may_load(deps.storage, storage_key)? { Some(delegation) => delegation, None => return Ok(PendingRewardResponse::default()), }; - let detailed_reward = mix_rewarding.determine_delegation_reward(&delegation)?; - let delegator_reward = mix_rewarding.pending_delegator_reward(&delegation)?; + let detailed_reward = node_rewarding.determine_delegation_reward(&delegation)?; + let delegator_reward = node_rewarding.pending_delegator_reward(&delegation)?; - // check if the mixnode isnt in the process of unbonding (or has already unbonded) - let is_bonded = matches!(mixnodes_storage::mixnode_bonds().may_load(deps.storage, mix_id)?, Some(mix_bond) if !mix_bond.is_unbonding); + // check if the node isnt in the process of unbonding (or has already unbonded) + let is_bonded = may_get_bond(deps.storage, node_id)? + .map(|b| !b.is_unbonding()) + .unwrap_or_default(); + #[allow(deprecated)] Ok(PendingRewardResponse { amount_staked: Some(delegation.amount), amount_earned: Some(delegator_reward), amount_earned_detailed: Some(detailed_reward), mixnode_still_fully_bonded: is_bonded, + node_still_fully_bonded: is_bonded, }) } @@ -106,40 +95,57 @@ fn zero_reward( pub(crate) fn query_estimated_current_epoch_operator_reward( deps: Deps<'_>, - mix_id: MixId, + node_id: NodeId, estimated_performance: Performance, -) -> StdResult { - let mix_details = match mixnodes::helpers::get_mixnode_details_by_id(deps.storage, mix_id)? { + estimated_work: Option, +) -> Result { + let rewarding_details = match storage::NYMNODE_REWARDING.may_load(deps.storage, node_id)? { None => return Ok(EstimatedCurrentEpochRewardResponse::empty_response()), - Some(mix_details) => mix_details, + Some(info) => info, }; - let amount_staked = mix_details.original_pledge().clone(); - let mix_rewarding = mix_details.rewarding_details; - let current_value = mix_rewarding.operator; + let bond = compat::helpers::get_bond(deps.storage, node_id)?; + + let amount_staked = bond.original_pledge().clone(); + let current_value = rewarding_details.operator; // if node is currently not in the rewarded set, the performance is 0, // or the node has either unbonded or is in the process of unbonding, // the calculations are trivial - the rewards are 0 - if mix_details.bond_information.is_unbonding { + if bond.is_unbonding() { return Ok(zero_reward(amount_staked, current_value)); } - let node_status = match interval_storage::REWARDED_SET.may_load(deps.storage, mix_id)? { - None => return Ok(zero_reward(amount_staked, current_value)), - Some(node_status) => node_status, - }; - if estimated_performance.is_zero() { return Ok(zero_reward(amount_staked, current_value)); } + let rewarding_params = storage::REWARDING_PARAMS.load(deps.storage)?; + + let work_factor = if let Some(work_factor) = estimated_work { + work_factor + } else { + let Some(role) = helpers::expensive_role_lookup(deps.storage, node_id)? else { + return Ok(zero_reward(amount_staked, current_value)); + }; + match role { + Role::EntryGateway | Role::Layer1 | Role::Layer2 | Role::Layer3 | Role::ExitGateway => { + rewarding_params.active_node_work() + } + Role::Standby => rewarding_params.standby_node_work(), + } + }; + + let node_reward_params = NodeRewardingParameters { + performance: estimated_performance, + work_factor, + }; + let rewarding_params = storage::REWARDING_PARAMS.load(deps.storage)?; let interval = interval_storage::current_interval(deps.storage)?; - let node_reward_params = NodeRewardParams::new(estimated_performance, node_status.is_active()); - let node_reward = mix_rewarding.node_reward(&rewarding_params, node_reward_params); - let reward_distribution = mix_rewarding.determine_reward_split( + let node_reward = rewarding_details.node_reward(&rewarding_params, node_reward_params); + let reward_distribution = rewarding_details.determine_reward_split( node_reward, estimated_performance, interval.epochs_in_interval(), @@ -160,55 +166,69 @@ pub(crate) fn query_estimated_current_epoch_operator_reward( pub(crate) fn query_estimated_current_epoch_delegator_reward( deps: Deps<'_>, owner: String, - mix_id: MixId, - proxy: Option, + node_id: NodeId, estimated_performance: Performance, -) -> StdResult { + estimated_work: Option, +) -> Result { let owner_address = deps.api.addr_validate(&owner)?; - let proxy = proxy - .map(|proxy| deps.api.addr_validate(&proxy)) - .transpose()?; - let mix_rewarding = match storage::MIXNODE_REWARDING.may_load(deps.storage, mix_id)? { - Some(mix_rewarding) => mix_rewarding, + let rewarding_details = match storage::NYMNODE_REWARDING.may_load(deps.storage, node_id)? { None => return Ok(EstimatedCurrentEpochRewardResponse::empty_response()), + Some(info) => info, }; - let storage_key = Delegation::generate_storage_key(mix_id, &owner_address, proxy.as_ref()); + let storage_key = Delegation::generate_storage_key(node_id, &owner_address, None); let delegation = match delegations_storage::delegations().may_load(deps.storage, storage_key)? { Some(delegation) => delegation, None => return Ok(EstimatedCurrentEpochRewardResponse::empty_response()), }; let staked_dec = into_base_decimal(delegation.amount.amount)?; - let current_value = staked_dec + mix_rewarding.determine_delegation_reward(&delegation)?; + let current_value = staked_dec + rewarding_details.determine_delegation_reward(&delegation)?; let amount_staked = delegation.amount; - // check if the mixnode isnt in the process of unbonding (or has already unbonded) - let is_bonded = matches!(mixnodes_storage::mixnode_bonds().may_load(deps.storage, mix_id)?, Some(mix_bond) if !mix_bond.is_unbonding); - - if !is_bonded { + if estimated_performance.is_zero() { return Ok(zero_reward(amount_staked, current_value)); } - // if node is currently not in the rewarded set, the performance is 0, - // or the node has either unbonded or is in the process of unbonding, - // the calculations are trivial - the rewards are 0 - let node_status = match interval_storage::REWARDED_SET.may_load(deps.storage, mix_id)? { - None => return Ok(zero_reward(amount_staked, current_value)), - Some(node_status) => node_status, + // check if the node isnt in the process of unbonding (or has already unbonded) + let Ok(bond) = compat::helpers::get_bond(deps.storage, node_id) else { + return Ok(zero_reward(amount_staked, current_value)); }; + if bond.is_unbonding() { + return Ok(zero_reward(amount_staked, current_value)); + } + if estimated_performance.is_zero() { return Ok(zero_reward(amount_staked, current_value)); } let rewarding_params = storage::REWARDING_PARAMS.load(deps.storage)?; + + let work_factor = if let Some(work_factor) = estimated_work { + work_factor + } else { + let Some(role) = helpers::expensive_role_lookup(deps.storage, node_id)? else { + return Ok(zero_reward(amount_staked, current_value)); + }; + match role { + Role::EntryGateway | Role::Layer1 | Role::Layer2 | Role::Layer3 | Role::ExitGateway => { + rewarding_params.active_node_work() + } + Role::Standby => rewarding_params.standby_node_work(), + } + }; + + let node_reward_params = NodeRewardingParameters { + performance: estimated_performance, + work_factor, + }; + let interval = interval_storage::current_interval(deps.storage)?; - let node_reward_params = NodeRewardParams::new(estimated_performance, node_status.is_active()); - let node_reward = mix_rewarding.node_reward(&rewarding_params, node_reward_params); - let reward_distribution = mix_rewarding.determine_reward_split( + let node_reward = rewarding_details.node_reward(&rewarding_params, node_reward_params); + let reward_distribution = rewarding_details.determine_reward_split( node_reward, estimated_performance, interval.epochs_in_interval(), @@ -218,7 +238,7 @@ pub(crate) fn query_estimated_current_epoch_delegator_reward( return Ok(zero_reward(amount_staked, current_value)); } - let reward_share = current_value / mix_rewarding.delegates * reward_distribution.delegates; + let reward_share = current_value / rewarding_details.delegates * reward_distribution.delegates; Ok(EstimatedCurrentEpochRewardResponse { estimation: Some(truncate_reward(reward_share, &amount_staked.denom)), @@ -273,7 +293,7 @@ mod tests { let owner = "mix-owner"; let initial_stake = Uint128::new(1_000_000_000_000); - let mix_id = test.add_dummy_mixnode(owner, Some(initial_stake)); + let mix_id = test.add_rewarded_legacy_mixnode(owner, Some(initial_stake)); let res = query_pending_operator_reward(test.deps(), owner.into()).unwrap(); let res2 = query_pending_mixnode_operator_reward(test.deps(), mix_id).unwrap(); @@ -292,13 +312,13 @@ mod tests { let mut test = TestSetup::new(); let owner = "mix-owner"; let initial_stake = Uint128::new(1_000_000_000_000); - let mix_id = test.add_dummy_mixnode(owner, Some(initial_stake)); + let mix_id = test.add_rewarded_legacy_mixnode(owner, Some(initial_stake)); test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![mix_id]); let mut total_earned = Decimal::zero(); - let dist = test.reward_with_distribution_with_state_bypass( + let dist = test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(100.0), ); @@ -318,7 +338,7 @@ mod tests { // reward it few more times for good measure for _ in 0..10 { test.skip_to_next_epoch_end(); - let dist = test.reward_with_distribution_with_state_bypass( + let dist = test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(100.0), ); @@ -342,13 +362,13 @@ mod tests { let mut test = TestSetup::new(); let owner = "mix-owner"; let initial_stake = Uint128::new(1_000_000_000_000); - let mix_id = test.add_dummy_mixnode(owner, Some(initial_stake)); + let mix_id = test.add_rewarded_legacy_mixnode(owner, Some(initial_stake)); test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![mix_id]); let mut total_earned = Decimal::zero(); - let dist = test.reward_with_distribution_with_state_bypass( + let dist = test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(100.0), ); @@ -374,12 +394,12 @@ mod tests { let mut test = TestSetup::new(); let owner = "mix-owner"; let initial_stake = Uint128::new(1_000_000_000_000); - let mix_id = test.add_dummy_mixnode(owner, Some(initial_stake)); + let mix_id = test.add_rewarded_legacy_mixnode(owner, Some(initial_stake)); test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![mix_id]); - test.reward_with_distribution_with_state_bypass( + test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(100.0), ); @@ -431,7 +451,8 @@ mod tests { let owner = "delegator"; let initial_stake = Uint128::new(100_000_000); - let mix_id = test.add_dummy_mixnode("mix-owner", Some(Uint128::new(1_000_000_000_000))); + let mix_id = test + .add_rewarded_legacy_mixnode("mix-owner", Some(Uint128::new(1_000_000_000_000))); test.add_immediate_delegation(owner, initial_stake, mix_id); let res = @@ -451,14 +472,15 @@ mod tests { let owner = "delegator"; let initial_stake = Uint128::new(100_000_000); - let mix_id = test.add_dummy_mixnode("mix-owner", Some(Uint128::new(1_000_000_000_000))); + let mix_id = test + .add_rewarded_legacy_mixnode("mix-owner", Some(Uint128::new(1_000_000_000_000))); test.add_immediate_delegation(owner, initial_stake, mix_id); test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![mix_id]); let mut total_earned = Decimal::zero(); - let dist = test.reward_with_distribution_with_state_bypass( + let dist = test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(100.0), ); @@ -477,7 +499,7 @@ mod tests { // reward it few more times for good measure for _ in 0..10 { test.skip_to_next_epoch_end(); - let dist = test.reward_with_distribution_with_state_bypass( + let dist = test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(100.0), ); @@ -501,14 +523,15 @@ mod tests { let owner = "delegator"; let initial_stake = Uint128::new(100_000_000); - let mix_id = test.add_dummy_mixnode("mix-owner", Some(Uint128::new(1_000_000_000_000))); + let mix_id = test + .add_rewarded_legacy_mixnode("mix-owner", Some(Uint128::new(1_000_000_000_000))); test.add_immediate_delegation(owner, initial_stake, mix_id); test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![mix_id]); let mut total_earned = Decimal::zero(); - let dist = test.reward_with_distribution_with_state_bypass( + let dist = test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(100.0), ); @@ -534,14 +557,15 @@ mod tests { let owner = "delegator"; let initial_stake = Uint128::new(100_000_000); - let mix_id = test.add_dummy_mixnode("mix-owner", Some(Uint128::new(1_000_000_000_000))); + let mix_id = test + .add_rewarded_legacy_mixnode("mix-owner", Some(Uint128::new(1_000_000_000_000))); test.add_immediate_delegation(owner, initial_stake, mix_id); test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![mix_id]); let mut total_earned = Decimal::zero(); - let dist = test.reward_with_distribution_with_state_bypass( + let dist = test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(100.0), ); @@ -572,7 +596,8 @@ mod tests { let del3 = "delegator3"; let del4 = "delegator4"; - let mix_id = test.add_dummy_mixnode("mix-owner", Some(Uint128::new(1_000_000_000_000))); + let mix_id = test + .add_rewarded_legacy_mixnode("mix-owner", Some(Uint128::new(1_000_000_000_000))); test.add_immediate_delegation(del1, 123_456_789u32, mix_id); test.add_immediate_delegation(del2, 150_000_000u32, mix_id); @@ -580,52 +605,55 @@ mod tests { test.force_change_rewarded_set(vec![mix_id]); test.skip_to_next_epoch_end(); - test.reward_with_distribution_with_state_bypass( + test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(100.0), ); test.skip_to_next_epoch_end(); - test.reward_with_distribution_with_state_bypass( + test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(100.0), ); test.add_immediate_delegation(del3, 500_000_000u32, mix_id); test.skip_to_next_epoch_end(); - test.reward_with_distribution_with_state_bypass( + test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(85.0), ); test.skip_to_next_epoch_end(); - test.reward_with_distribution_with_state_bypass(mix_id, test_helpers::performance(5.0)); + test.legacy_reward_with_distribution_with_state_bypass( + mix_id, + test_helpers::performance(5.0), + ); test.add_immediate_delegation(del4, 5_000_000u32, mix_id); test.skip_to_next_epoch_end(); - test.reward_with_distribution_with_state_bypass( + test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(100.0), ); test.add_immediate_delegation(del2, 250_000_000u32, mix_id); test.skip_to_next_epoch_end(); - test.reward_with_distribution_with_state_bypass( + test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(98.0), ); test.skip_to_next_epoch_end(); - test.reward_with_distribution_with_state_bypass( + test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(100.0), ); test.remove_immediate_delegation(del3, mix_id); test.skip_to_next_epoch_end(); - test.reward_with_distribution_with_state_bypass( + test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(98.0), ); test.skip_to_next_epoch_end(); - test.reward_with_distribution_with_state_bypass( + test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(100.0), ); @@ -671,7 +699,7 @@ mod tests { fn expected_current_operator( test: &TestSetup, - mix_id: MixId, + mix_id: NodeId, initial_stake: Uint128, ) -> EstimatedCurrentEpochRewardResponse { let mix_rewarding = test.mix_rewarding(mix_id); @@ -691,6 +719,7 @@ mod tests { test.deps(), 42, test_helpers::performance(100.0), + None, ) .unwrap(); assert_eq!(res, EstimatedCurrentEpochRewardResponse::empty_response()) @@ -701,11 +730,11 @@ mod tests { let mut test = TestSetup::new(); let initial_stake = Uint128::new(1_000_000_000_000); let owner = "mix-owner"; - let mix_id = test.add_dummy_mixnode(owner, Some(initial_stake)); + let mix_id = test.add_rewarded_legacy_mixnode(owner, Some(initial_stake)); test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![mix_id]); - test.reward_with_distribution_with_state_bypass( + test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(100.0), ); @@ -718,6 +747,7 @@ mod tests { test.deps(), mix_id, test_helpers::performance(100.0), + None, ) .unwrap(); @@ -730,11 +760,11 @@ mod tests { let mut test = TestSetup::new(); let initial_stake = Uint128::new(1_000_000_000_000); let owner = "mix-owner"; - let mix_id = test.add_dummy_mixnode(owner, Some(initial_stake)); + let mix_id = test.add_rewarded_legacy_mixnode(owner, Some(initial_stake)); test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![mix_id]); - test.reward_with_distribution_with_state_bypass( + test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(100.0), ); @@ -748,6 +778,7 @@ mod tests { test.deps(), mix_id, test_helpers::performance(100.0), + None, ) .unwrap(); assert_eq!(res, EstimatedCurrentEpochRewardResponse::empty_response()) @@ -757,11 +788,11 @@ mod tests { fn when_node_is_not_in_the_rewarded_set() { let mut test = TestSetup::new(); let initial_stake = Uint128::new(1_000_000_000_000); - let mix_id = test.add_dummy_mixnode("mix-owner", Some(initial_stake)); + let mix_id = test.add_rewarded_legacy_mixnode("mix-owner", Some(initial_stake)); test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![mix_id]); - test.reward_with_distribution_with_state_bypass( + test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(100.0), ); @@ -771,6 +802,7 @@ mod tests { test.deps(), mix_id, test_helpers::performance(100.0), + None, ) .unwrap(); @@ -782,11 +814,11 @@ mod tests { fn when_estimated_performance_is_zero() { let mut test = TestSetup::new(); let initial_stake = Uint128::new(1_000_000_000_000); - let mix_id = test.add_dummy_mixnode("mix-owner", Some(initial_stake)); + let mix_id = test.add_rewarded_legacy_mixnode("mix-owner", Some(initial_stake)); test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![mix_id]); - test.reward_with_distribution_with_state_bypass( + test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(100.0), ); @@ -795,6 +827,7 @@ mod tests { test.deps(), mix_id, test_helpers::performance(0.0), + None, ) .unwrap(); @@ -806,11 +839,11 @@ mod tests { fn with_correct_parameters_matches_actual_distribution() { let mut test = TestSetup::new(); let initial_stake = Uint128::new(1_000_000_000_000); - let mix_id = test.add_dummy_mixnode("mix-owner", Some(initial_stake)); + let mix_id = test.add_rewarded_legacy_mixnode("mix-owner", Some(initial_stake)); test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![mix_id]); - test.reward_with_distribution_with_state_bypass( + test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(100.0), ); @@ -820,11 +853,12 @@ mod tests { test.deps(), mix_id, test_helpers::performance(95.0), + None, ) .unwrap(); test.skip_to_next_epoch_end(); - let dist = test.reward_with_distribution_with_state_bypass( + let dist = test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(95.0), ); @@ -850,7 +884,7 @@ mod tests { fn expected_current_delegator( test: &TestSetup, - mix_id: MixId, + mix_id: NodeId, owner: &str, ) -> EstimatedCurrentEpochRewardResponse { let mix_rewarding = test.mix_rewarding(mix_id); @@ -875,11 +909,11 @@ mod tests { #[test] fn when_delegation_doesnt_exist() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_rewarded_legacy_mixnode("mix-owner", None); test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![mix_id]); - test.reward_with_distribution_with_state_bypass( + test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(100.0), ); @@ -888,8 +922,8 @@ mod tests { test.deps(), "foomper".into(), mix_id, - None, test_helpers::performance(100.0), + None, ) .unwrap(); @@ -899,7 +933,7 @@ mod tests { #[test] fn when_node_is_unbonding() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_rewarded_legacy_mixnode("mix-owner", None); let initial_stake = Uint128::new(1_000_000_000); let owner = "delegator"; @@ -907,7 +941,7 @@ mod tests { test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![mix_id]); - test.reward_with_distribution_with_state_bypass( + test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(100.0), ); @@ -920,8 +954,8 @@ mod tests { test.deps(), owner.into(), mix_id, - None, test_helpers::performance(100.0), + None, ) .unwrap(); @@ -932,7 +966,7 @@ mod tests { #[test] fn when_node_has_already_unbonded() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_rewarded_legacy_mixnode("mix-owner", None); let initial_stake = Uint128::new(1_000_000_000); let owner = "delegator"; @@ -940,7 +974,7 @@ mod tests { test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![mix_id]); - test.reward_with_distribution_with_state_bypass( + test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(100.0), ); @@ -954,8 +988,8 @@ mod tests { test.deps(), owner.into(), mix_id, - None, test_helpers::performance(100.0), + None, ) .unwrap(); @@ -966,7 +1000,7 @@ mod tests { #[test] fn when_node_is_not_in_the_rewarded_set() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_rewarded_legacy_mixnode("mix-owner", None); let initial_stake = Uint128::new(1_000_000_000); let owner = "delegator"; @@ -974,7 +1008,7 @@ mod tests { test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![mix_id]); - test.reward_with_distribution_with_state_bypass( + test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(100.0), ); @@ -984,8 +1018,8 @@ mod tests { test.deps(), owner.into(), mix_id, - None, test_helpers::performance(100.0), + None, ) .unwrap(); @@ -996,7 +1030,7 @@ mod tests { #[test] fn when_estimated_performance_is_zero() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_rewarded_legacy_mixnode("mix-owner", None); let initial_stake = Uint128::new(1_000_000_000); let owner = "delegator"; @@ -1004,7 +1038,7 @@ mod tests { test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![mix_id]); - test.reward_with_distribution_with_state_bypass( + test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(100.0), ); @@ -1013,8 +1047,8 @@ mod tests { test.deps(), owner.into(), mix_id, - None, test_helpers::performance(0.0), + None, ) .unwrap(); @@ -1025,7 +1059,7 @@ mod tests { #[test] fn with_correct_parameters_matches_actual_distribution_for_single_delegator() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_rewarded_legacy_mixnode("mix-owner", None); let initial_stake = Uint128::new(1_000_000_000); let owner = "delegator"; @@ -1039,13 +1073,13 @@ mod tests { test.deps(), owner.into(), mix_id, - None, test_helpers::performance(95.0), + None, ) .unwrap(); test.skip_to_next_epoch_end(); - let dist = test.reward_with_distribution_with_state_bypass( + let dist = test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(95.0), ); @@ -1067,7 +1101,7 @@ mod tests { #[test] fn with_correct_parameters_matches_actual_distribution_for_three_delegators() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_rewarded_legacy_mixnode("mix-owner", None); let initial_stake1 = Uint128::new(1_000_000_000); let initial_stake2 = Uint128::new(45_000_000_000); @@ -1084,14 +1118,14 @@ mod tests { test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![mix_id]); - test.reward_with_distribution_with_state_bypass( + test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(95.0), ); test.add_immediate_delegation(del3, initial_stake3, mix_id); test.skip_to_next_epoch_end(); - test.reward_with_distribution_with_state_bypass( + test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(85.0), ); @@ -1105,8 +1139,8 @@ mod tests { test.deps(), owner.to_string(), mix_id, - None, test_helpers::performance(95.0), + None, ) .unwrap() }) @@ -1131,7 +1165,7 @@ mod tests { let cur3 = initial_stake3_dec + est3; test.skip_to_next_epoch_end(); - let dist = test.reward_with_distribution_with_state_bypass( + let dist = test.legacy_reward_with_distribution_with_state_bypass( mix_id, test_helpers::performance(95.0), ); diff --git a/contracts/mixnet/src/rewards/storage.rs b/contracts/mixnet/src/rewards/storage.rs index e773e67d5d..37574f99ee 100644 --- a/contracts/mixnet/src/rewards/storage.rs +++ b/contracts/mixnet/src/rewards/storage.rs @@ -2,38 +2,126 @@ // SPDX-License-Identifier: Apache-2.0 use crate::constants::{ - MIXNODES_REWARDING_PK_NAMESPACE, PENDING_REWARD_POOL_KEY, REWARDING_PARAMS_KEY, + CUMULATIVE_EPOCH_WORK_KEY, MIXNODES_REWARDING_PK_NAMESPACE, PENDING_REWARD_POOL_KEY, + REWARDING_PARAMS_KEY, }; use crate::rewards::models::RewardPoolChange; use cosmwasm_std::{Decimal, StdResult, Storage}; use cw_storage_plus::{Item, Map}; use mixnet_contract_common::error::MixnetContractError; -use mixnet_contract_common::mixnode::MixNodeRewarding; -use mixnet_contract_common::reward_params::RewardingParams; -use mixnet_contract_common::MixId; +use mixnet_contract_common::mixnode::NodeRewarding; +use mixnet_contract_common::reward_params::{RewardingParams, WorkFactor}; +use mixnet_contract_common::NodeId; + +// LEGACY CONSTANTS: // current parameters used for rewarding purposes pub(crate) const REWARDING_PARAMS: Item<'_, RewardingParams> = Item::new(REWARDING_PARAMS_KEY); pub(crate) const PENDING_REWARD_POOL_CHANGE: Item<'_, RewardPoolChange> = Item::new(PENDING_REWARD_POOL_KEY); -pub const MIXNODE_REWARDING: Map = - Map::new(MIXNODES_REWARDING_PK_NAMESPACE); +pub const MIXNODE_REWARDING: Map = Map::new(MIXNODES_REWARDING_PK_NAMESPACE); -pub fn reward_accounting( - storage: &mut dyn Storage, - amount: Decimal, -) -> Result<(), MixnetContractError> { - let mut pending_changes = PENDING_REWARD_POOL_CHANGE.load(storage)?; - pending_changes.removed += amount; +// we're using the same underlying key to allow seamless delegation migration +pub const NYMNODE_REWARDING: Map = MIXNODE_REWARDING; - Ok(PENDING_REWARD_POOL_CHANGE.save(storage, &pending_changes)?) +pub struct RewardingStorage<'a> { + /// Global parameters used for reward calculation, such as the current reward pool, the active set size, etc. + pub global_rewarding_params: Item<'a, RewardingParams>, + + /// All the changes to the rewarding pool that should get applied upon the **interval** finishing. + pub pending_reward_pool_change: Item<'a, RewardPoolChange>, + + /// Information associated with all nym-nodes (and legacy-mixnodes) required for reward calculation + // important note: this is using **EXACTLY** the same underlying key (and structure) as legacy mixnode rewarding + pub nym_node_rewarding_data: Map<'a, NodeId, NodeRewarding>, + + /// keeps track of total cumulative work submitted for this rewarding epoch to make sure it never goes above 1 + pub cumulative_epoch_work: Item<'a, WorkFactor>, } -pub(crate) fn initialise_storage( - storage: &mut dyn Storage, - reward_params: RewardingParams, -) -> StdResult<()> { - REWARDING_PARAMS.save(storage, &reward_params)?; - PENDING_REWARD_POOL_CHANGE.save(storage, &RewardPoolChange::default()) +impl<'a> RewardingStorage<'a> { + pub const fn new() -> RewardingStorage<'a> { + RewardingStorage { + global_rewarding_params: REWARDING_PARAMS, + pending_reward_pool_change: PENDING_REWARD_POOL_CHANGE, + nym_node_rewarding_data: NYMNODE_REWARDING, + cumulative_epoch_work: Item::new(CUMULATIVE_EPOCH_WORK_KEY), + } + } + + // an 'alias' because a `new` method might be a bit misleading since it'd suggest a brand new storage is created + // as opposed to using the same underlying data as before + pub const fn load() -> RewardingStorage<'a> { + Self::new() + } + + pub fn initialise( + &self, + storage: &mut dyn Storage, + reward_params: RewardingParams, + ) -> StdResult<()> { + self.global_rewarding_params.save(storage, &reward_params)?; + self.pending_reward_pool_change + .save(storage, &RewardPoolChange::default())?; + self.cumulative_epoch_work + .save(storage, &WorkFactor::zero())?; + + Ok(()) + } + + pub fn reset_cumulative_epoch_work( + &self, + storage: &mut dyn Storage, + ) -> Result<(), MixnetContractError> { + self.cumulative_epoch_work + .save(storage, &WorkFactor::zero())?; + Ok(()) + } + + pub fn update_cumulative_epoch_work( + &self, + storage: &mut dyn Storage, + work: Decimal, + ) -> Result<(), MixnetContractError> { + // we use a default in case this is the first run in the new contract since that value hasn't existed before + let current = self + .cumulative_epoch_work + .may_load(storage)? + .unwrap_or(WorkFactor::zero()); + let updated = current + work; + if updated > WorkFactor::one() { + return Err(MixnetContractError::TotalWorkAboveOne); + } + self.cumulative_epoch_work.save(storage, &updated)?; + Ok(()) + } + + pub fn add_pending_pool_changes( + &self, + storage: &mut dyn Storage, + amount: Decimal, + ) -> Result<(), MixnetContractError> { + let mut pending_changes = self.pending_reward_pool_change.load(storage)?; + pending_changes.removed += amount; + self.pending_reward_pool_change + .save(storage, &pending_changes)?; + Ok(()) + } + + pub fn try_persist_node_reward( + &self, + storage: &mut dyn Storage, + node: NodeId, + updated_data: NodeRewarding, + reward: Decimal, + work: WorkFactor, + ) -> Result<(), MixnetContractError> { + self.nym_node_rewarding_data + .save(storage, node, &updated_data)?; + self.add_pending_pool_changes(storage, reward)?; + self.update_cumulative_epoch_work(storage, work)?; + + Ok(()) + } } diff --git a/contracts/mixnet/src/rewards/transactions.rs b/contracts/mixnet/src/rewards/transactions.rs index f772dca800..c5cc5c64d4 100644 --- a/contracts/mixnet/src/rewards/transactions.rs +++ b/contracts/mixnet/src/rewards/transactions.rs @@ -2,186 +2,194 @@ // SPDX-License-Identifier: Apache-2.0 use super::storage; +use crate::compat::helpers::ensure_can_withdraw_rewards; use crate::delegations::storage as delegations_storage; use crate::interval::storage as interval_storage; use crate::interval::storage::{push_new_epoch_event, push_new_interval_event}; use crate::mixnet_contract_settings::storage as mixnet_params_storage; use crate::mixnet_contract_settings::storage::ADMIN; -use crate::mixnodes::helpers::get_mixnode_details_by_owner; -use crate::mixnodes::storage as mixnodes_storage; use crate::rewards::helpers; use crate::rewards::helpers::update_and_save_last_rewarded; +use crate::rewards::storage::RewardingStorage; use crate::support::helpers::{ - ensure_bonded, ensure_can_advance_epoch, ensure_epoch_in_progress_state, AttachSendTokens, + ensure_any_node_bonded, ensure_can_advance_epoch, ensure_epoch_in_progress_state, + AttachSendTokens, }; use cosmwasm_std::{DepsMut, Env, MessageInfo, Response}; use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::events::{ new_active_set_update_event, new_mix_rewarding_event, - new_not_found_mix_operator_rewarding_event, new_pending_active_set_update_event, + new_not_found_node_operator_rewarding_event, new_pending_active_set_update_event, new_pending_rewarding_params_update_event, new_rewarding_params_update_event, new_withdraw_delegator_reward_event, new_withdraw_operator_reward_event, new_zero_uptime_mix_operator_rewarding_event, }; use mixnet_contract_common::pending_events::{PendingEpochEventKind, PendingIntervalEventKind}; use mixnet_contract_common::reward_params::{ - IntervalRewardingParamsUpdate, NodeRewardParams, Performance, + ActiveSetUpdate, IntervalRewardingParamsUpdate, NodeRewardingParameters, }; -use mixnet_contract_common::{Delegation, EpochState, MixId}; +use mixnet_contract_common::{Delegation, EpochState, MixNodeDetails, NodeId, NymNodeDetails}; -pub(crate) fn try_reward_mixnode( +pub(crate) fn try_reward_node( deps: DepsMut<'_>, env: Env, info: MessageInfo, - mix_id: MixId, - node_performance: Performance, + node_id: NodeId, + node_rewarding_params: NodeRewardingParameters, ) -> Result { + let rewarding_storage = RewardingStorage::load(); + // check whether this `info.sender` is the same one as set in `epoch_status.being_advanced_by` // if so, return `epoch_status` so we could avoid having to perform extra read from the storage let current_epoch_status = ensure_can_advance_epoch(&info.sender, deps.storage)?; // see if the epoch has finished let interval = interval_storage::current_interval(deps.storage)?; - if !interval.is_current_epoch_over(&env) { - return Err(MixnetContractError::EpochInProgress { - current_block_time: env.block.time.seconds(), - epoch_start: interval.current_epoch_start_unix_timestamp(), - epoch_end: interval.current_epoch_end_unix_timestamp(), - }); - } + interval.ensure_current_epoch_is_over(&env)?; + let absolute_epoch_id = interval.current_epoch_absolute_id(); - if matches!(current_epoch_status.state, EpochState::Rewarding {last_rewarded, ..} if last_rewarded == mix_id) - { - return Err(MixnetContractError::MixnodeAlreadyRewarded { - mix_id, - absolute_epoch_id, - }); + if let EpochState::Rewarding { last_rewarded, .. } = current_epoch_status.state { + if last_rewarded >= node_id { + return Err(MixnetContractError::NodeAlreadyRewarded { + node_id, + absolute_epoch_id, + }); + } } // update the epoch state with this node as being rewarded most recently - // (if the transaction fails down the line, it will be reverted) - update_and_save_last_rewarded(deps.storage, current_epoch_status, mix_id)?; + // (if the transaction fails down the line, this storage write will be reverted) + update_and_save_last_rewarded(deps.storage, current_epoch_status, node_id)?; - // there's a chance of this failing to load the details if the mixnode unbonded before rewards + // there's a chance of this failing to load the details if the node unbonded before rewards // were distributed and all of its delegators are also gone - let mut mix_rewarding = match storage::MIXNODE_REWARDING.may_load(deps.storage, mix_id)? { - Some(mix_rewarding) if mix_rewarding.still_bonded() => mix_rewarding, - // don't fail if the node has unbonded as we don't want to fail the underlying transaction + + // NOTE: legacy mixnode rewarding are stored under the same storage key + // and have the same rewarding structure thus they'd also be loaded here + let mut rewarding_info = match storage::NYMNODE_REWARDING.may_load(deps.storage, node_id)? { + Some(rewarding_info) if rewarding_info.still_bonded() => rewarding_info, + // don't fail if the node has unbonded (or it's a legacy gateway) as we don't want to fail the underlying transaction _ => { - return Ok(Response::new() - .add_event(new_not_found_mix_operator_rewarding_event(interval, mix_id))); + return Ok( + Response::new().add_event(new_not_found_node_operator_rewarding_event( + interval, node_id, + )), + ); } }; - let prior_delegates = mix_rewarding.delegates; - let prior_unit_reward = mix_rewarding.full_reward_ratio(); + let prior_delegates = rewarding_info.delegates; + let prior_unit_reward = rewarding_info.full_reward_ratio(); // check if this node has already been rewarded for the current epoch. // unlike the previous check, this one should be a hard error since this cannot be // influenced by users actions (note that previous epoch state checks should actually already guard us against it) - if absolute_epoch_id == mix_rewarding.last_rewarded_epoch { - return Err(MixnetContractError::MixnodeAlreadyRewarded { - mix_id, + if absolute_epoch_id == rewarding_info.last_rewarded_epoch { + return Err(MixnetContractError::NodeAlreadyRewarded { + node_id, absolute_epoch_id, }); } - // again a hard error since the rewarding validator should have known not to reward this node - let node_status = interval_storage::REWARDED_SET - .load(deps.storage, mix_id) - .map_err(|_| MixnetContractError::MixnodeNotInRewardedSet { - mix_id, - absolute_epoch_id, - })?; - // no need to calculate anything as rewards are going to be 0 for everything // however, we still need to update last_rewarded_epoch field - if node_performance.is_zero() { - mix_rewarding.last_rewarded_epoch = absolute_epoch_id; - storage::MIXNODE_REWARDING.save(deps.storage, mix_id, &mix_rewarding)?; + if node_rewarding_params.is_zero() { + rewarding_info.last_rewarded_epoch = absolute_epoch_id; + storage::NYMNODE_REWARDING.save(deps.storage, node_id, &rewarding_info)?; return Ok( Response::new().add_event(new_zero_uptime_mix_operator_rewarding_event( - interval, mix_id, + interval, node_id, )), ); } - // make sure node's profit margin is within the allowed range, + // make sure node's cost function is within the allowed range, // if not adjust it accordingly - let params = mixnet_params_storage::CONTRACT_STATE - .load(deps.storage)? - .params; - mix_rewarding.normalise_profit_margin(params.profit_margin); - mix_rewarding.normalise_operating_cost(params.interval_operating_cost); + let params = mixnet_params_storage::state_params(deps.storage)?; + rewarding_info.normalise_cost_function(params.profit_margin, params.interval_operating_cost); - let rewarding_params = storage::REWARDING_PARAMS.load(deps.storage)?; - let node_reward_params = NodeRewardParams::new(node_performance, node_status.is_active()); + let global_rewarding_params = rewarding_storage + .global_rewarding_params + .load(deps.storage)?; - // calculate each step separate for easier accounting - let node_reward = mix_rewarding.node_reward(&rewarding_params, node_reward_params); - let reward_distribution = mix_rewarding.determine_reward_split( + // calculate each step separately for easier accounting + // + // total node reward, i.e. owner + delegates + let node_reward = rewarding_info.node_reward(&global_rewarding_params, node_rewarding_params); + + // the actual split between owner and its delegates + let reward_distribution = rewarding_info.determine_reward_split( node_reward, - node_performance, + node_rewarding_params.performance, interval.epochs_in_interval(), ); - mix_rewarding.distribute_rewards(reward_distribution, absolute_epoch_id); + // update internal accounting with the new values + rewarding_info.distribute_rewards(reward_distribution, absolute_epoch_id); - // persist changes happened to the storage - storage::MIXNODE_REWARDING.save(deps.storage, mix_id, &mix_rewarding)?; - storage::reward_accounting(deps.storage, node_reward)?; + // persist the changes to the storage + rewarding_storage.try_persist_node_reward( + deps.storage, + node_id, + rewarding_info, + node_reward, + node_rewarding_params.work_factor, + )?; Ok(Response::new().add_event(new_mix_rewarding_event( interval, - mix_id, + node_id, reward_distribution, prior_delegates, prior_unit_reward, ))) } -pub(crate) fn try_withdraw_operator_reward( +pub(crate) fn try_withdraw_nym_node_operator_reward( deps: DepsMut<'_>, - info: MessageInfo, + node_details: NymNodeDetails, ) -> Result { - // we need to grab all of the node's details, so we'd known original pledge alongside - // all the earned rewards (and obviously to know if this node even exists and is still - // in the bonded state) - let mix_details = get_mixnode_details_by_owner(deps.storage, info.sender.clone())?.ok_or( - MixnetContractError::NoAssociatedMixNodeBond { - owner: info.sender.clone(), - }, - )?; - let mix_id = mix_details.mix_id(); + ensure_can_withdraw_rewards(&node_details)?; - ensure_bonded(&mix_details.bond_information)?; + let node_id = node_details.node_id(); + let owner = node_details.bond_information.owner.clone(); + + let reward = helpers::withdraw_operator_reward(deps.storage, node_details)?; + + Ok(Response::new().add_event(new_withdraw_operator_reward_event(&owner, reward, node_id))) +} + +pub(crate) fn try_withdraw_mixnode_operator_reward( + deps: DepsMut<'_>, + mix_details: MixNodeDetails, +) -> Result { + let node_id = mix_details.mix_id(); + let owner = mix_details.bond_information.owner.clone(); + + ensure_can_withdraw_rewards(&mix_details)?; let reward = helpers::withdraw_operator_reward(deps.storage, mix_details)?; let mut response = Response::new(); // if the reward is zero, don't track or send anything - there's no point if !reward.amount.is_zero() { - response = response.send_tokens(&info.sender, reward.clone()) + response = response.send_tokens(&owner, reward.clone()) } - Ok(response.add_event(new_withdraw_operator_reward_event( - &info.sender, - reward, - mix_id, - ))) + Ok(response.add_event(new_withdraw_operator_reward_event(&owner, reward, node_id))) } pub(crate) fn try_withdraw_delegator_reward( deps: DepsMut<'_>, info: MessageInfo, - mix_id: MixId, + node_id: NodeId, ) -> Result { // see if the delegation even exists - let storage_key = Delegation::generate_storage_key(mix_id, &info.sender, None); + let storage_key = Delegation::generate_storage_key(node_id, &info.sender, None); let delegation = match delegations_storage::delegations().may_load(deps.storage, storage_key)? { None => { - return Err(MixnetContractError::NoMixnodeDelegationFound { - mix_id, + return Err(MixnetContractError::NodeDelegationNotFound { + node_id, address: info.sender.into_string(), proxy: None, }); @@ -189,21 +197,15 @@ pub(crate) fn try_withdraw_delegator_reward( Some(delegation) => delegation, }; - // grab associated mixnode rewarding details + // grab associated node rewarding details let mix_rewarding = - storage::MIXNODE_REWARDING.may_load(deps.storage, mix_id)?.ok_or(MixnetContractError::inconsistent_state( - "mixnode rewarding got removed from the storage whilst there's still an existing delegation" + storage::NYMNODE_REWARDING.may_load(deps.storage, node_id)?.ok_or(MixnetContractError::inconsistent_state( + "nym-node/legacy mixnode rewarding got removed from the storage whilst there's still an existing delegation" ))?; // see if the mixnode is not in the process of unbonding or whether it has already unbonded // (in that case the expected path of getting your tokens back is via undelegation) - match mixnodes_storage::mixnode_bonds().may_load(deps.storage, mix_id)? { - Some(mix_bond) if mix_bond.is_unbonding => { - return Err(MixnetContractError::MixnodeIsUnbonding { mix_id }); - } - None => return Err(MixnetContractError::MixnodeHasUnbonded { mix_id }), - _ => (), - }; + ensure_any_node_bonded(deps.storage, node_id)?; let reward = helpers::withdraw_delegator_reward(deps.storage, delegation, mix_rewarding)?; let mut response = Response::new(); @@ -216,54 +218,46 @@ pub(crate) fn try_withdraw_delegator_reward( Ok(response.add_event(new_withdraw_delegator_reward_event( &info.sender, reward, - mix_id, + node_id, ))) } -pub(crate) fn try_update_active_set_size( +pub(crate) fn try_update_active_set_distribution( deps: DepsMut<'_>, env: Env, info: MessageInfo, - active_set_size: u32, + update: ActiveSetUpdate, force_immediately: bool, ) -> Result { ADMIN.assert_admin(deps.as_ref(), &info.sender)?; - let mut rewarding_params = storage::REWARDING_PARAMS.load(deps.storage)?; - if active_set_size == 0 { - return Err(MixnetContractError::ZeroActiveSet); - } + let mut rewarding_params = RewardingStorage::load() + .global_rewarding_params + .load(deps.storage)?; - if active_set_size > rewarding_params.rewarded_set_size { - return Err(MixnetContractError::InvalidActiveSetSize); - } + // make sure the values could theoretically be applied in the current context + rewarding_params.validate_active_set_update(update)?; let interval = interval_storage::current_interval(deps.storage)?; - if force_immediately || interval.is_current_epoch_over(&env) { - rewarding_params.try_change_active_set_size(active_set_size)?; - storage::REWARDING_PARAMS.save(deps.storage, &rewarding_params)?; - Ok(Response::new().add_event(new_active_set_update_event( - env.block.height, - active_set_size, - ))) - } else { - // updating active sety size is only allowed if the epoch is currently not in the process of being advanced - // (unless the force flag was used) - ensure_epoch_in_progress_state(deps.storage)?; - // push the epoch event - let epoch_event = PendingEpochEventKind::UpdateActiveSetSize { - new_size: active_set_size, - }; - push_new_epoch_event(deps.storage, &env, epoch_event)?; - let time_left = interval.secs_until_current_interval_end(&env); - Ok( - Response::new().add_event(new_pending_active_set_update_event( - active_set_size, - time_left, - )), - ) + // perform the change immediately + if force_immediately || interval.is_current_epoch_over(&env) { + rewarding_params.try_change_active_set(update)?; + storage::REWARDING_PARAMS.save(deps.storage, &rewarding_params)?; + return Ok(Response::new().add_event(new_active_set_update_event(env.block.height, update))); } + + // otherwise push the event onto the queue to get executed when the epoch concludes + + // updating active set is only allowed if the epoch is currently not in the process of being advanced + // (unless the force flag was used) + ensure_epoch_in_progress_state(deps.storage)?; + + // push the epoch event + let epoch_event = PendingEpochEventKind::UpdateActiveSet { update }; + push_new_epoch_event(deps.storage, &env, epoch_event)?; + let time_left = interval.secs_until_current_interval_end(&env); + Ok(Response::new().add_event(new_pending_active_set_update_event(update, time_left))) } pub(crate) fn try_update_rewarding_params( @@ -311,29 +305,83 @@ pub(crate) fn try_update_rewarding_params( #[cfg(test)] pub mod tests { - use cosmwasm_std::testing::mock_info; - - use crate::mixnodes::storage as mixnodes_storage; - use crate::support::tests::test_helpers; - use super::*; + use crate::mixnodes::storage as mixnodes_storage; + use crate::support::tests::fixtures::active_set_update_fixture; + use crate::support::tests::test_helpers; + use cosmwasm_std::testing::mock_info; #[cfg(test)] mod mixnode_rewarding { + use super::*; + use crate::interval::pending_events; + use crate::support::tests::test_helpers::{find_attribute, FindAttribute, TestSetup}; use cosmwasm_std::{Decimal, Uint128}; - use mixnet_contract_common::events::{ MixnetEventType, BOND_NOT_FOUND_VALUE, DELEGATES_REWARD_KEY, NO_REWARD_REASON_KEY, OPERATOR_REWARD_KEY, PRIOR_DELEGATES_KEY, PRIOR_UNIT_REWARD_KEY, ZERO_PERFORMANCE_VALUE, }; use mixnet_contract_common::helpers::compare_decimals; - use mixnet_contract_common::{EpochStatus, RewardedSetNodeStatus}; + use mixnet_contract_common::nym_node::Role; + use mixnet_contract_common::reward_params::Performance; + use mixnet_contract_common::EpochStatus; - use crate::interval::pending_events; - use crate::support::tests::test_helpers::{find_attribute, TestSetup}; + // a simple wrapper to streamline checking for rewarding results + trait TestRewarding { + #[deprecated] + fn legacy_rewarding( + &mut self, + node_id: NodeId, + performance: Performance, + ) -> Result; - use super::*; + fn execute_rewarding( + &mut self, + node_id: NodeId, + rewarding_params: NodeRewardingParameters, + ) -> Result; + + fn assert_rewarding(&mut self, node_id: NodeId, performance: Performance) -> Response; + } + + impl TestRewarding for TestSetup { + fn legacy_rewarding( + &mut self, + node_id: NodeId, + performance: Performance, + ) -> Result { + // this is rather temporary (I hope...) + let work_factor = self.get_legacy_rewarding_node_work_factor(node_id); + + self.execute_rewarding( + node_id, + NodeRewardingParameters { + performance, + work_factor, + }, + ) + } + + fn execute_rewarding( + &mut self, + node_id: NodeId, + rewarding_params: NodeRewardingParameters, + ) -> Result { + let sender = self.rewarding_validator(); + self.execute_fn( + |deps, env, info| try_reward_node(deps, env, info, node_id, rewarding_params), + sender, + ) + } + + #[track_caller] + fn assert_rewarding(&mut self, node_id: NodeId, performance: Performance) -> Response { + let caller = std::panic::Location::caller(); + self.legacy_rewarding(node_id, performance) + .unwrap_or_else(|err| panic!("{caller} failed with: '{err}' ({err:?})")) + } + } #[cfg(test)] mod epoch_state_is_correctly_updated { @@ -342,15 +390,15 @@ pub mod tests { #[test] fn when_target_mixnode_unbonded() { let mut test = TestSetup::new(); - let mix_id_unbonded = test.add_dummy_mixnode("mix-owner-unbonded", None); - let mix_id_unbonded_leftover = - test.add_dummy_mixnode("mix-owner-unbonded-leftover", None); - let mix_id_never_existed = 42; + let node_id_unbonded = test.add_rewarded_legacy_mixnode("mix-owner-unbonded", None); + let node_id_unbonded_leftover = + test.add_rewarded_legacy_mixnode("mix-owner-unbonded-leftover", None); + let node_id_never_existed = 42; test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![ - mix_id_unbonded, - mix_id_unbonded_leftover, - mix_id_never_existed, + node_id_unbonded, + node_id_unbonded_leftover, + node_id_never_existed, ]); test.start_epoch_transition(); @@ -360,77 +408,54 @@ pub mod tests { // since before performing the nym-api should clear out the event queue // manually adjust delegation info as to indicate the rewarding information shouldnt get removed - let mut rewarding_details = storage::MIXNODE_REWARDING - .load(test.deps().storage, mix_id_unbonded_leftover) + let mut rewarding_details = storage::NYMNODE_REWARDING + .load(test.deps().storage, node_id_unbonded_leftover) .unwrap(); rewarding_details.delegates = Decimal::raw(12345); rewarding_details.unique_delegations = 1; - storage::MIXNODE_REWARDING + storage::NYMNODE_REWARDING .save( test.deps_mut().storage, - mix_id_unbonded_leftover, + node_id_unbonded_leftover, &rewarding_details, ) .unwrap(); - pending_events::unbond_mixnode(test.deps_mut(), &env, 123, mix_id_unbonded) + pending_events::unbond_mixnode(test.deps_mut(), &env, 123, node_id_unbonded) .unwrap(); pending_events::unbond_mixnode( test.deps_mut(), &env, 123, - mix_id_unbonded_leftover, + node_id_unbonded_leftover, ) .unwrap(); - let env = test.env(); - let sender = test.rewarding_validator(); let performance = test_helpers::performance(100.0); - try_reward_mixnode( - test.deps_mut(), - env.clone(), - sender.clone(), - mix_id_unbonded, - performance, - ) - .unwrap(); + test.assert_rewarding(node_id_unbonded, performance); assert_eq!( EpochState::Rewarding { - last_rewarded: mix_id_unbonded, - final_node_id: mix_id_never_existed, + last_rewarded: node_id_unbonded, + final_node_id: node_id_never_existed, }, interval_storage::current_epoch_status(test.deps().storage) .unwrap() .state ); - try_reward_mixnode( - test.deps_mut(), - env.clone(), - sender.clone(), - mix_id_unbonded_leftover, - performance, - ) - .unwrap(); + test.assert_rewarding(node_id_unbonded_leftover, performance); assert_eq!( EpochState::Rewarding { - last_rewarded: mix_id_unbonded_leftover, - final_node_id: mix_id_never_existed, + last_rewarded: node_id_unbonded_leftover, + final_node_id: node_id_never_existed, }, interval_storage::current_epoch_status(test.deps().storage) .unwrap() .state ); - try_reward_mixnode( - test.deps_mut(), - env, - sender, - mix_id_never_existed, - performance, - ) - .unwrap(); + test.assert_rewarding(node_id_never_existed, performance); assert_eq!( EpochState::ReconcilingEvents, interval_storage::current_epoch_status(test.deps().storage) @@ -442,16 +467,14 @@ pub mod tests { #[test] fn when_target_mixnode_has_zero_performance() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let node_id = test.add_rewarded_legacy_mixnode("mix-owner", None); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id]); + test.force_change_rewarded_set(vec![node_id]); test.start_epoch_transition(); - let zero_performance = test_helpers::performance(0.); - let env = test.env(); - let sender = test.rewarding_validator(); - try_reward_mixnode(test.deps_mut(), env, sender, mix_id, zero_performance).unwrap(); + let zero_performance = test_helpers::performance(0.); + test.assert_rewarding(node_id, zero_performance); assert_eq!( EpochState::ReconcilingEvents, interval_storage::current_epoch_status(test.deps().storage) @@ -463,16 +486,14 @@ pub mod tests { #[test] fn when_theres_only_one_node_to_reward() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let node_id = test.add_rewarded_legacy_mixnode("mix-owner", None); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id]); + test.force_change_rewarded_set(vec![node_id]); test.start_epoch_transition(); - let performance = test_helpers::performance(100.0); - let env = test.env(); - let sender = test.rewarding_validator(); - try_reward_mixnode(test.deps_mut(), env, sender, mix_id, performance).unwrap(); + let performance = test_helpers::performance(100.0); + test.assert_rewarding(node_id, performance); assert_eq!( EpochState::ReconcilingEvents, interval_storage::current_epoch_status(test.deps().storage) @@ -487,36 +508,27 @@ pub mod tests { let mut ids = Vec::new(); for i in 0..100 { - let mix_id = test.add_dummy_mixnode(&format!("mix-owner{i}"), None); - ids.push(mix_id); + let node_id = test.add_rewarded_legacy_mixnode(&format!("mix-owner{i}"), None); + ids.push(node_id); } test.skip_to_next_epoch_end(); test.force_change_rewarded_set(ids.clone()); test.start_epoch_transition(); let performance = test_helpers::performance(100.0); - let env = test.env(); - let sender = test.rewarding_validator(); - for mix_id in ids { - try_reward_mixnode( - test.deps_mut(), - env.clone(), - sender.clone(), - mix_id, - performance, - ) - .unwrap(); + for node_id in ids { + test.assert_rewarding(node_id, performance); let current_state = interval_storage::current_epoch_status(test.deps().storage) .unwrap() .state; - if mix_id == 100 { + if node_id == 100 { assert_eq!(EpochState::ReconcilingEvents, current_state) } else { assert_eq!( EpochState::Rewarding { - last_rewarded: mix_id, + last_rewarded: node_id, final_node_id: 100, }, current_state @@ -531,12 +543,13 @@ pub mod tests { let bad_states = vec![ EpochState::InProgress, EpochState::ReconcilingEvents, - EpochState::AdvancingEpoch, + EpochState::RoleAssignment { + next: Role::first(), + }, ]; for bad_state in bad_states { let mut test = TestSetup::new(); - let rewarding_validator = test.rewarding_validator(); let mut status = EpochStatus::new(test.rewarding_validator().sender); status.state = bad_state; @@ -545,15 +558,9 @@ pub mod tests { test.skip_to_current_epoch_end(); test.force_change_rewarded_set(vec![1, 2, 3]); - let env = test.env(); - let res = try_reward_mixnode( - test.deps_mut(), - env, - rewarding_validator, - 1, - test_helpers::performance(100.), - ); + let res = test.legacy_rewarding(1, test_helpers::performance(100.0)); + assert_eq!( res, Err(MixnetContractError::UnexpectedNonRewardingEpochState { @@ -566,39 +573,39 @@ pub mod tests { #[test] fn can_only_be_performed_by_specified_rewarding_validator() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let node_id = test.add_rewarded_legacy_mixnode("mix-owner", None); let some_sender = mock_info("foomper", &[]); // skip time to when the following epoch is over (since mixnodes are not eligible for rewarding // in the same epoch they're bonded and we need the rewarding epoch to be over) test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id]); + test.force_change_rewarded_set(vec![node_id]); test.start_epoch_transition(); - let performance = test_helpers::performance(100.); + let params = test.legacy_rewarding_params(node_id, 100.); let env = test.env(); - let res = try_reward_mixnode(test.deps_mut(), env, some_sender, mix_id, performance); + let res = try_reward_node(test.deps_mut(), env, some_sender, node_id, params); assert_eq!(res, Err(MixnetContractError::Unauthorized)); // good address (sanity check) let env = test.env(); let sender = test.rewarding_validator(); - let res = try_reward_mixnode(test.deps_mut(), env, sender, mix_id, performance); + let res = try_reward_node(test.deps_mut(), env, sender, node_id, params); assert!(res.is_ok()); } #[test] fn can_only_be_performed_if_node_is_fully_bonded() { let mut test = TestSetup::new(); - let mix_id_never_existed = 42; - let mix_id_unbonded = test.add_dummy_mixnode("mix-owner-unbonded", None); - let mix_id_unbonded_leftover = - test.add_dummy_mixnode("mix-owner-unbonded-leftover", None); + let node_id_never_existed = 42; + let node_id_unbonded = test.add_rewarded_legacy_mixnode("mix-owner-unbonded", None); + let node_id_unbonded_leftover = + test.add_rewarded_legacy_mixnode("mix-owner-unbonded-leftover", None); test.skip_to_next_epoch_end(); test.force_change_rewarded_set(vec![ - mix_id_unbonded, - mix_id_unbonded_leftover, - mix_id_never_existed, + node_id_unbonded, + node_id_unbonded_leftover, + node_id_never_existed, ]); test.start_epoch_transition(); @@ -608,43 +615,34 @@ pub mod tests { // since before performing the nym-api should clear out the event queue // manually adjust delegation info as to indicate the rewarding information shouldnt get removed - let mut rewarding_details = storage::MIXNODE_REWARDING - .load(test.deps().storage, mix_id_unbonded_leftover) + let mut rewarding_details = storage::NYMNODE_REWARDING + .load(test.deps().storage, node_id_unbonded_leftover) .unwrap(); rewarding_details.delegates = Decimal::raw(12345); rewarding_details.unique_delegations = 1; - storage::MIXNODE_REWARDING + storage::NYMNODE_REWARDING .save( test.deps_mut().storage, - mix_id_unbonded_leftover, + node_id_unbonded_leftover, &rewarding_details, ) .unwrap(); - pending_events::unbond_mixnode(test.deps_mut(), &env, 123, mix_id_unbonded).unwrap(); + pending_events::unbond_mixnode(test.deps_mut(), &env, 123, node_id_unbonded).unwrap(); - pending_events::unbond_mixnode(test.deps_mut(), &env, 123, mix_id_unbonded_leftover) + pending_events::unbond_mixnode(test.deps_mut(), &env, 123, node_id_unbonded_leftover) .unwrap(); - let env = test.env(); - let sender = test.rewarding_validator(); let performance = test_helpers::performance(100.0); - for &mix_id in &[ - mix_id_unbonded, - mix_id_unbonded_leftover, - mix_id_never_existed, + for &node_id in &[ + node_id_unbonded, + node_id_unbonded_leftover, + node_id_never_existed, ] { - let res = try_reward_mixnode( - test.deps_mut(), - env.clone(), - sender.clone(), - mix_id, - performance, - ) - .unwrap(); + let res = test.assert_rewarding(node_id, performance); let reason = find_attribute( - Some(MixnetEventType::MixnodeRewarding.to_string()), + Some(MixnetEventType::NodeRewarding.to_string()), NO_REWARD_REASON_KEY, &res, ); @@ -656,16 +654,14 @@ pub mod tests { fn can_only_be_performed_once_epoch_is_over() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); - let sender = test.rewarding_validator(); + let node_id = test.add_rewarded_legacy_mixnode("mix-owner", None); // node is in the active set BUT the current epoch has just begun test.skip_to_next_epoch(); - test.force_change_rewarded_set(vec![mix_id]); - let performance = test_helpers::performance(100.); + test.force_change_rewarded_set(vec![node_id]); - let env = test.env(); - let res = try_reward_mixnode(test.deps_mut(), env, sender.clone(), mix_id, performance); + let performance = test_helpers::performance(100.); + let res = test.legacy_rewarding(node_id, performance); assert!(matches!( res, Err(MixnetContractError::EpochInProgress { .. }) @@ -674,199 +670,157 @@ pub mod tests { // epoch is over (sanity check) test.skip_to_current_epoch_end(); test.start_epoch_transition(); - let env = test.env(); - let res = try_reward_mixnode(test.deps_mut(), env, sender, mix_id, performance); + let res = test.legacy_rewarding(node_id, performance); assert!(res.is_ok()); } #[test] fn can_only_be_performed_for_nodes_in_rewarded_set() { - let mut test = TestSetup::new(); - - let active_mix_id = test.add_dummy_mixnode("mix-owner-active", None); - let standby_mix_id = test.add_dummy_mixnode("mix-owner-standby", None); - let inactive_mix_id = test.add_dummy_mixnode("mix-owner-inactive", None); - let sender = test.rewarding_validator(); - - test.skip_to_next_epoch_end(); - - // manually set the rewarded set so that we'd have 1 active node, 1 standby and 1 inactive - interval_storage::REWARDED_SET - .save( - test.deps_mut().storage, - active_mix_id, - &RewardedSetNodeStatus::Active, - ) - .unwrap(); - interval_storage::REWARDED_SET - .save( - test.deps_mut().storage, - standby_mix_id, - &RewardedSetNodeStatus::Standby, - ) - .unwrap(); - - // actually add one more dummy node with high id so we wouldn't go into the next state - interval_storage::REWARDED_SET - .save( - test.deps_mut().storage, - 9001, - &RewardedSetNodeStatus::Standby, - ) - .unwrap(); - test.start_epoch_transition(); - - let performance = test_helpers::performance(100.); - let env = test.env(); - let res_active = try_reward_mixnode( - test.deps_mut(), - env.clone(), - sender.clone(), - active_mix_id, - performance, - ); - let res_standby = try_reward_mixnode( - test.deps_mut(), - env.clone(), - sender.clone(), - standby_mix_id, - performance, - ); - let res_inactive = - try_reward_mixnode(test.deps_mut(), env, sender, inactive_mix_id, performance); - - assert!(res_active.is_ok()); - assert!(res_standby.is_ok()); - assert!(matches!( - res_inactive, - Err(MixnetContractError::MixnodeNotInRewardedSet { mix_id, .. }) if mix_id == inactive_mix_id - )); + // let mut test = TestSetup::new(); + // + // let active_node_id = test.add_rewarded_legacy_mixnode("mix-owner-active", None); + // let standby_node_id = test.add_rewarded_legacy_mixnode("mix-owner-standby", None); + // let inactive_node_id = test.add_rewarded_legacy_mixnode("mix-owner-inactive", None); + // let sender = test.rewarding_validator(); + // + // test.skip_to_next_epoch_end(); + // + // // manually set the rewarded set so that we'd have 1 active node, 1 standby and 1 inactive + // interval_storage::REWARDED_SET + // .save( + // test.deps_mut().storage, + // active_node_id, + // &RewardedSetNodeStatus::Active, + // ) + // .unwrap(); + // interval_storage::REWARDED_SET + // .save( + // test.deps_mut().storage, + // standby_node_id, + // &RewardedSetNodeStatus::Standby, + // ) + // .unwrap(); + // + // // actually add one more dummy node with high id so we wouldn't go into the next state + // interval_storage::REWARDED_SET + // .save( + // test.deps_mut().storage, + // 9001, + // &RewardedSetNodeStatus::Standby, + // ) + // .unwrap(); + // test.start_epoch_transition(); + // + // let performance = test_helpers::performance(100.); + // let env = test.env(); + // let res_active = try_reward_node( + // test.deps_mut(), + // env.clone(), + // sender.clone(), + // active_node_id, + // performance, + // ); + // let res_standby = try_reward_node( + // test.deps_mut(), + // env.clone(), + // sender.clone(), + // standby_node_id, + // performance, + // ); + // let res_inactive = + // try_reward_node(test.deps_mut(), env, sender, inactive_node_id, performance); + // + // assert!(res_active.is_ok()); + // assert!(res_standby.is_ok()); + // assert!(matches!( + // res_inactive, + // Err(MixnetContractError::MixnodeNotInRewardedSet { node_id, .. }) if node_id == inactive_node_id + // )); } #[test] fn can_only_be_performed_once_per_node_per_epoch() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let node_id = test.add_rewarded_legacy_mixnode("mix-owner", None); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id, 42]); + test.force_change_rewarded_set(vec![node_id, 42]); test.start_epoch_transition(); let performance = test_helpers::performance(100.); - let env = test.env(); - let sender = test.rewarding_validator(); // first rewarding - let res = try_reward_mixnode( - test.deps_mut(), - env.clone(), - sender.clone(), - mix_id, - performance, - ); - assert!(res.is_ok()); + test.assert_rewarding(node_id, performance); // second rewarding - let res = try_reward_mixnode(test.deps_mut(), env, sender.clone(), mix_id, performance); + let res = test.legacy_rewarding(node_id, performance); assert!(matches!( res, - Err(MixnetContractError::MixnodeAlreadyRewarded { mix_id, .. }) if mix_id == mix_id + Err(MixnetContractError::NodeAlreadyRewarded { node_id, .. }) if node_id == node_id )); // in the following epoch we're good again test.skip_to_next_epoch_end(); test.start_epoch_transition(); - let env = test.env(); - let res = try_reward_mixnode(test.deps_mut(), env, sender, mix_id, performance); + let res = test.legacy_rewarding(node_id, performance); assert!(res.is_ok()); } #[test] fn requires_nonzero_performance_score() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let node_id = test.add_rewarded_legacy_mixnode("mix-owner", None); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id, 42]); + test.force_change_rewarded_set(vec![node_id, 42]); test.start_epoch_transition(); let zero_performance = test_helpers::performance(0.); let performance = test_helpers::performance(100.0); - let env = test.env(); - let sender = test.rewarding_validator(); // first rewarding - let res = try_reward_mixnode( - test.deps_mut(), - env.clone(), - sender.clone(), - mix_id, - zero_performance, - ) - .unwrap(); - let reason = find_attribute( - Some(MixnetEventType::MixnodeRewarding.to_string()), - NO_REWARD_REASON_KEY, - &res, - ); + let res = test.assert_rewarding(node_id, zero_performance); + let reason = res.attribute(MixnetEventType::NodeRewarding, NO_REWARD_REASON_KEY); assert_eq!(ZERO_PERFORMANCE_VALUE, reason); - // sanity check: it's still treated as rewarding, so you we can't reward the node again + // sanity check: it's still treated as rewarding, so we can't reward the node again // with different performance for the same epoch - let res = try_reward_mixnode( - test.deps_mut(), - env, - sender.clone(), - mix_id, - zero_performance, - ); + let res = test.legacy_rewarding(node_id, zero_performance); assert!(matches!( res, - Err(MixnetContractError::MixnodeAlreadyRewarded { mix_id, .. }) if mix_id == mix_id + Err(MixnetContractError::NodeAlreadyRewarded { node_id, .. }) if node_id == node_id )); // but in the next epoch, as always, we're good again test.skip_to_next_epoch_end(); test.start_epoch_transition(); - let env = test.env(); - let res = - try_reward_mixnode(test.deps_mut(), env, sender, mix_id, performance).unwrap(); + let res = test.assert_rewarding(node_id, performance); // rewards got distributed (in this test we don't care what they were exactly, but they must be non-zero) - let operator = find_attribute( - Some(MixnetEventType::MixnodeRewarding.to_string()), - OPERATOR_REWARD_KEY, - &res, - ); + let operator = res.attribute(MixnetEventType::NodeRewarding, OPERATOR_REWARD_KEY); assert!(!operator.is_empty()); assert_ne!("0", operator); - let delegates = find_attribute( - Some(MixnetEventType::MixnodeRewarding.to_string()), - DELEGATES_REWARD_KEY, - &res, - ); + let delegates = res.attribute(MixnetEventType::NodeRewarding, DELEGATES_REWARD_KEY); assert_eq!("0", delegates); } #[test] fn correctly_accounts_for_rewards_distributed() { let mut test = TestSetup::new(); - let mix_id1 = test.add_dummy_mixnode("mix-owner1", None); - let mix_id2 = test.add_dummy_mixnode("mix-owner2", None); - let mix_id3 = test.add_dummy_mixnode("mix-owner3", None); + let node_id1 = test.add_rewarded_legacy_mixnode("mix-owner1", None); + let node_id2 = test.add_rewarded_legacy_mixnode("mix-owner2", None); + let node_id3 = test.add_rewarded_legacy_mixnode("mix-owner3", None); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id1, mix_id2, mix_id3]); + test.force_change_rewarded_set(vec![node_id1, node_id2, node_id3]); test.start_epoch_transition(); let performance = test_helpers::performance(98.0); - let env = test.env(); - let sender = test.rewarding_validator(); - test.add_immediate_delegation("delegator1", Uint128::new(100_000_000), mix_id2); + test.add_immediate_delegation("delegator1", Uint128::new(100_000_000), node_id2); - test.add_immediate_delegation("delegator1", Uint128::new(100_000_000), mix_id3); - test.add_immediate_delegation("delegator2", Uint128::new(123_456_000), mix_id3); - test.add_immediate_delegation("delegator3", Uint128::new(9_100_000_000), mix_id3); + test.add_immediate_delegation("delegator1", Uint128::new(100_000_000), node_id3); + test.add_immediate_delegation("delegator2", Uint128::new(123_456_000), node_id3); + test.add_immediate_delegation("delegator3", Uint128::new(9_100_000_000), node_id3); let change = storage::PENDING_REWARD_POOL_CHANGE .load(test.deps().storage) @@ -877,36 +831,17 @@ pub mod tests { let mut total_operator = Decimal::zero(); let mut total_delegates = Decimal::zero(); - for &mix_id in &[mix_id1, mix_id2, mix_id3] { - let before = storage::MIXNODE_REWARDING - .load(test.deps().storage, mix_id) + for &node_id in &[node_id1, node_id2, node_id3] { + let before = storage::NYMNODE_REWARDING + .load(test.deps().storage, node_id) .unwrap(); - let res = try_reward_mixnode( - test.deps_mut(), - env.clone(), - sender.clone(), - mix_id, - performance, - ) - .unwrap(); - let operator: Decimal = find_attribute( - Some(MixnetEventType::MixnodeRewarding.to_string()), - OPERATOR_REWARD_KEY, - &res, - ) - .parse() - .unwrap(); - let delegates: Decimal = find_attribute( - Some(MixnetEventType::MixnodeRewarding.to_string()), - DELEGATES_REWARD_KEY, - &res, - ) - .parse() - .unwrap(); + let res = test.assert_rewarding(node_id, performance); + let operator = res.decimal(MixnetEventType::NodeRewarding, OPERATOR_REWARD_KEY); + let delegates = res.decimal(MixnetEventType::NodeRewarding, DELEGATES_REWARD_KEY); - let after = storage::MIXNODE_REWARDING - .load(test.deps().storage, mix_id) + let after = storage::NYMNODE_REWARDING + .load(test.deps().storage, node_id) .unwrap(); // also the values emitted via events are consistent with the actual values! @@ -936,20 +871,21 @@ pub mod tests { let operator3 = Uint128::new(12_345_000_000); let mut test = TestSetup::new(); - let mix_id1 = test.add_dummy_mixnode("mix-owner1", Some(operator1)); - let mix_id2 = test.add_dummy_mixnode("mix-owner2", Some(operator2)); - let mix_id3 = test.add_dummy_mixnode("mix-owner3", Some(operator3)); + let global_rewarding_params = test.rewarding_params(); + let node_id1 = test.add_rewarded_legacy_mixnode("mix-owner1", Some(operator1)); + let node_id2 = test.add_rewarded_legacy_mixnode("mix-owner2", Some(operator2)); + let node_id3 = test.add_rewarded_legacy_mixnode("mix-owner3", Some(operator3)); test.skip_to_next_epoch_end(); test.start_epoch_transition(); - test.force_change_rewarded_set(vec![mix_id1, mix_id2, mix_id3]); + test.force_change_rewarded_set(vec![node_id1, node_id2, node_id3]); let performance = test_helpers::performance(98.0); - test.add_immediate_delegation("delegator1", Uint128::new(100_000_000), mix_id2); + test.add_immediate_delegation("delegator1", Uint128::new(100_000_000), node_id2); - test.add_immediate_delegation("delegator1", Uint128::new(100_000_000), mix_id3); - test.add_immediate_delegation("delegator2", Uint128::new(123_456_000), mix_id3); - test.add_immediate_delegation("delegator3", Uint128::new(9_100_000_000), mix_id3); + test.add_immediate_delegation("delegator1", Uint128::new(100_000_000), node_id3); + test.add_immediate_delegation("delegator2", Uint128::new(123_456_000), node_id3); + test.add_immediate_delegation("delegator3", Uint128::new(9_100_000_000), node_id3); // bypass proper epoch progression and force change the state test.set_epoch_in_progress_state(); @@ -957,13 +893,16 @@ pub mod tests { // repeat the rewarding the same set of delegates for few epochs for _ in 0..10 { test.start_epoch_transition(); - for &mix_id in &[mix_id1, mix_id2, mix_id3] { - let mut sim = test.instantiate_simulator(mix_id); - let dist = test.reward_with_distribution(mix_id, performance); - let node_params = NodeRewardParams { + for &node_id in &[node_id1, node_id2, node_id3] { + let mut sim = test.instantiate_simulator(node_id); + let dist = test.legacy_reward_with_distribution_and_legacy_work_factor( + node_id, performance, - in_active_set: true, - }; + ); + let node_params = NodeRewardingParameters::new( + performance, + global_rewarding_params.active_node_work(), + ); let sim_res = sim.simulate_epoch_single_node(node_params).unwrap(); assert_eq!(sim_res, dist); } @@ -975,11 +914,11 @@ pub mod tests { // add few more delegations and repeat it // (note: we're not concerned about whether particular delegation owner got the correct amount, // this is checked in other unit tests) - test.add_immediate_delegation("delegator1", Uint128::new(50_000_000), mix_id1); - test.add_immediate_delegation("delegator1", Uint128::new(200_000_000), mix_id2); + test.add_immediate_delegation("delegator1", Uint128::new(50_000_000), node_id1); + test.add_immediate_delegation("delegator1", Uint128::new(200_000_000), node_id2); - test.add_immediate_delegation("delegator5", Uint128::new(123_000_000), mix_id3); - test.add_immediate_delegation("delegator6", Uint128::new(456_000_000), mix_id3); + test.add_immediate_delegation("delegator5", Uint128::new(123_000_000), node_id3); + test.add_immediate_delegation("delegator6", Uint128::new(456_000_000), node_id3); // bypass proper epoch progression and force change the state test.set_epoch_in_progress_state(); @@ -987,13 +926,16 @@ pub mod tests { let performance = test_helpers::performance(12.3); for _ in 0..10 { test.start_epoch_transition(); - for &mix_id in &[mix_id1, mix_id2, mix_id3] { - let mut sim = test.instantiate_simulator(mix_id); - let dist = test.reward_with_distribution(mix_id, performance); - let node_params = NodeRewardParams { + for &node_id in &[node_id1, node_id2, node_id3] { + let mut sim = test.instantiate_simulator(node_id); + let dist = test.legacy_reward_with_distribution_and_legacy_work_factor( + node_id, performance, - in_active_set: true, - }; + ); + let node_params = NodeRewardingParameters::new( + performance, + global_rewarding_params.active_node_work(), + ); let sim_res = sim.simulate_epoch_single_node(node_params).unwrap(); assert_eq!(sim_res, dist); } @@ -1009,78 +951,53 @@ pub mod tests { let operator2 = Uint128::new(12_345_000_000); let mut test = TestSetup::new(); - let sender = test.rewarding_validator(); + let global_rewarding_params = test.rewarding_params(); - let mix_id1 = test.add_dummy_mixnode("mix-owner1", Some(operator1)); - let mix_id2 = test.add_dummy_mixnode("mix-owner2", Some(operator2)); + let node_id1 = test.add_rewarded_legacy_mixnode("mix-owner1", Some(operator1)); + let node_id2 = test.add_rewarded_legacy_mixnode("mix-owner2", Some(operator2)); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id1, mix_id2]); + test.force_change_rewarded_set(vec![node_id1, node_id2]); let performance = test_helpers::performance(98.0); - test.add_immediate_delegation("delegator1", Uint128::new(100_000_000), mix_id1); - test.add_immediate_delegation("delegator1", Uint128::new(100_000_000), mix_id2); + test.add_immediate_delegation("delegator1", Uint128::new(100_000_000), node_id1); + test.add_immediate_delegation("delegator1", Uint128::new(100_000_000), node_id2); - test.add_immediate_delegation("delegator2", Uint128::new(123_456_000), mix_id1); + test.add_immediate_delegation("delegator2", Uint128::new(123_456_000), node_id1); - let del11 = test.delegation(mix_id1, "delegator1", &None); - let del12 = test.delegation(mix_id1, "delegator2", &None); - let del21 = test.delegation(mix_id2, "delegator1", &None); + let del11 = test.delegation(node_id1, "delegator1", &None); + let del12 = test.delegation(node_id1, "delegator2", &None); + let del21 = test.delegation(node_id2, "delegator1", &None); for _ in 0..10 { test.start_epoch_transition(); // we know from the previous tests that actual rewarding distribution matches the simulator - let mut sim1 = test.instantiate_simulator(mix_id1); - let mut sim2 = test.instantiate_simulator(mix_id2); + let mut sim1 = test.instantiate_simulator(node_id1); + let mut sim2 = test.instantiate_simulator(node_id2); - let node_params = NodeRewardParams { + let node_params = NodeRewardingParameters::new( performance, - in_active_set: true, - }; + global_rewarding_params.active_node_work(), + ); let dist1 = sim1.simulate_epoch_single_node(node_params).unwrap(); let dist2 = sim2.simulate_epoch_single_node(node_params).unwrap(); - let env = test.env(); + let actual_prior1 = test.mix_rewarding(node_id1); + let actual_prior2 = test.mix_rewarding(node_id2); - let actual_prior1 = test.mix_rewarding(mix_id1); - let actual_prior2 = test.mix_rewarding(mix_id2); + let res1 = test.assert_rewarding(node_id1, performance); - let res1 = try_reward_mixnode( - test.deps_mut(), - env.clone(), - sender.clone(), - mix_id1, - performance, - ) - .unwrap(); + let prior_delegates1 = + res1.decimal(MixnetEventType::NodeRewarding, PRIOR_DELEGATES_KEY); + let delegates_reward1 = + res1.decimal(MixnetEventType::NodeRewarding, DELEGATES_REWARD_KEY); + let prior_unit_reward = + res1.decimal(MixnetEventType::NodeRewarding, PRIOR_UNIT_REWARD_KEY); - let prior_delegates1: Decimal = find_attribute( - Some(MixnetEventType::MixnodeRewarding), - PRIOR_DELEGATES_KEY, - &res1, - ) - .parse() - .unwrap(); assert_eq!(prior_delegates1, actual_prior1.delegates); - - let delegates_reward1: Decimal = find_attribute( - Some(MixnetEventType::MixnodeRewarding), - DELEGATES_REWARD_KEY, - &res1, - ) - .parse() - .unwrap(); assert_eq!(delegates_reward1, dist1.delegates); - - let prior_unit_reward: Decimal = find_attribute( - Some(MixnetEventType::MixnodeRewarding), - PRIOR_UNIT_REWARD_KEY, - &res1, - ) - .parse() - .unwrap(); assert_eq!(actual_prior1.full_reward_ratio(), prior_unit_reward); // either use the constant for (which for now is the same for all nodes) @@ -1111,40 +1028,17 @@ pub mod tests { None, ); - let res2 = try_reward_mixnode( - test.deps_mut(), - env.clone(), - sender.clone(), - mix_id2, - performance, - ) - .unwrap(); + let res2 = test.assert_rewarding(node_id2, performance); + + let prior_delegates2 = + res2.decimal(MixnetEventType::NodeRewarding, PRIOR_DELEGATES_KEY); + let delegates_reward2 = + res2.decimal(MixnetEventType::NodeRewarding, DELEGATES_REWARD_KEY); + let prior_unit_reward = + res2.decimal(MixnetEventType::NodeRewarding, PRIOR_UNIT_REWARD_KEY); - let prior_delegates2: Decimal = find_attribute( - Some(MixnetEventType::MixnodeRewarding), - PRIOR_DELEGATES_KEY, - &res2, - ) - .parse() - .unwrap(); assert_eq!(prior_delegates2, actual_prior2.delegates); - - let delegates_reward2: Decimal = find_attribute( - Some(MixnetEventType::MixnodeRewarding), - DELEGATES_REWARD_KEY, - &res2, - ) - .parse() - .unwrap(); assert_eq!(delegates_reward2, dist2.delegates); - - let prior_unit_reward: Decimal = find_attribute( - Some(MixnetEventType::MixnodeRewarding), - PRIOR_UNIT_REWARD_KEY, - &res2, - ) - .parse() - .unwrap(); assert_eq!(actual_prior2.full_reward_ratio(), prior_unit_reward); // either use the constant for (which for now is the same for all nodes) @@ -1168,66 +1062,41 @@ pub mod tests { } // add more delegations and check few more epochs (so that the delegations would start from non-default unit delegation value) - test.add_immediate_delegation("delegator3", Uint128::new(15_850_000_000), mix_id1); - test.add_immediate_delegation("delegator3", Uint128::new(15_850_000_000), mix_id2); + test.add_immediate_delegation("delegator3", Uint128::new(15_850_000_000), node_id1); + test.add_immediate_delegation("delegator3", Uint128::new(15_850_000_000), node_id2); - let del13 = test.delegation(mix_id1, "delegator3", &None); - let del23 = test.delegation(mix_id2, "delegator3", &None); + let del13 = test.delegation(node_id1, "delegator3", &None); + let del23 = test.delegation(node_id2, "delegator3", &None); for _ in 0..10 { test.start_epoch_transition(); // we know from the previous tests that actual rewarding distribution matches the simulator - let mut sim1 = test.instantiate_simulator(mix_id1); - let mut sim2 = test.instantiate_simulator(mix_id2); + let mut sim1 = test.instantiate_simulator(node_id1); + let mut sim2 = test.instantiate_simulator(node_id2); - let node_params = NodeRewardParams { + let node_params = NodeRewardingParameters::new( performance, - in_active_set: true, - }; + global_rewarding_params.active_node_work(), + ); let dist1 = sim1.simulate_epoch_single_node(node_params).unwrap(); let dist2 = sim2.simulate_epoch_single_node(node_params).unwrap(); - let env = test.env(); + let actual_prior1 = test.mix_rewarding(node_id1); + let actual_prior2 = test.mix_rewarding(node_id2); - let actual_prior1 = test.mix_rewarding(mix_id1); - let actual_prior2 = test.mix_rewarding(mix_id2); + let res1 = test.assert_rewarding(node_id1, performance); - let res1 = try_reward_mixnode( - test.deps_mut(), - env.clone(), - sender.clone(), - mix_id1, - performance, - ) - .unwrap(); + let prior_delegates1 = + res1.decimal(MixnetEventType::NodeRewarding, PRIOR_DELEGATES_KEY); + let delegates_reward1 = + res1.decimal(MixnetEventType::NodeRewarding, DELEGATES_REWARD_KEY); + let prior_unit_reward = + res1.decimal(MixnetEventType::NodeRewarding, PRIOR_UNIT_REWARD_KEY); - let prior_delegates1: Decimal = find_attribute( - Some(MixnetEventType::MixnodeRewarding), - PRIOR_DELEGATES_KEY, - &res1, - ) - .parse() - .unwrap(); assert_eq!(prior_delegates1, actual_prior1.delegates); - - let delegates_reward1: Decimal = find_attribute( - Some(MixnetEventType::MixnodeRewarding), - DELEGATES_REWARD_KEY, - &res1, - ) - .parse() - .unwrap(); assert_eq!(delegates_reward1, dist1.delegates); - - let prior_unit_reward: Decimal = find_attribute( - Some(MixnetEventType::MixnodeRewarding), - PRIOR_UNIT_REWARD_KEY, - &res1, - ) - .parse() - .unwrap(); assert_eq!(actual_prior1.full_reward_ratio(), prior_unit_reward); // either use the constant for (which for now is the same for all nodes) @@ -1266,40 +1135,17 @@ pub mod tests { None, ); - let res2 = try_reward_mixnode( - test.deps_mut(), - env.clone(), - sender.clone(), - mix_id2, - performance, - ) - .unwrap(); + let res2 = test.assert_rewarding(node_id2, performance); + + let prior_delegates2 = + res2.decimal(MixnetEventType::NodeRewarding, PRIOR_DELEGATES_KEY); + let delegates_reward2 = + res2.decimal(MixnetEventType::NodeRewarding, DELEGATES_REWARD_KEY); + let prior_unit_reward = + res2.decimal(MixnetEventType::NodeRewarding, PRIOR_UNIT_REWARD_KEY); - let prior_delegates2: Decimal = find_attribute( - Some(MixnetEventType::MixnodeRewarding), - PRIOR_DELEGATES_KEY, - &res2, - ) - .parse() - .unwrap(); assert_eq!(prior_delegates2, actual_prior2.delegates); - - let delegates_reward2: Decimal = find_attribute( - Some(MixnetEventType::MixnodeRewarding), - DELEGATES_REWARD_KEY, - &res2, - ) - .parse() - .unwrap(); assert_eq!(delegates_reward2, dist2.delegates); - - let prior_unit_reward: Decimal = find_attribute( - Some(MixnetEventType::MixnodeRewarding), - PRIOR_UNIT_REWARD_KEY, - &res2, - ) - .parse() - .unwrap(); assert_eq!(actual_prior2.full_reward_ratio(), prior_unit_reward); // either use the constant for (which for now is the same for all nodes) @@ -1349,10 +1195,10 @@ pub mod tests { fn can_only_be_done_if_delegation_exists() { let mut test = TestSetup::new(); // add relatively huge stake so that the reward would be high enough to offset operating costs - let mix_id1 = - test.add_dummy_mixnode("mix-owner1", Some(Uint128::new(1_000_000_000_000))); - let mix_id2 = - test.add_dummy_mixnode("mix-owner2", Some(Uint128::new(1_000_000_000_000))); + let node_id1 = test + .add_rewarded_legacy_mixnode("mix-owner1", Some(Uint128::new(1_000_000_000_000))); + let node_id2 = test + .add_rewarded_legacy_mixnode("mix-owner2", Some(Uint128::new(1_000_000_000_000))); let delegator1 = "delegator1"; let delegator2 = "delegator2"; @@ -1361,36 +1207,42 @@ pub mod tests { let sender2 = mock_info(delegator2, &[]); // note that there's no delegation from delegator1 towards mix1 - test.add_immediate_delegation(delegator2, 100_000_000u128, mix_id1); + test.add_immediate_delegation(delegator2, 100_000_000u128, node_id1); - test.add_immediate_delegation(delegator1, 100_000_000u128, mix_id2); - test.add_immediate_delegation(delegator2, 100_000_000u128, mix_id2); + test.add_immediate_delegation(delegator1, 100_000_000u128, node_id2); + test.add_immediate_delegation(delegator2, 100_000_000u128, node_id2); // perform some rewarding so that we'd have non-zero rewards test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id1, mix_id2]); + test.force_change_rewarded_set(vec![node_id1, node_id2]); test.start_epoch_transition(); - test.reward_with_distribution(mix_id1, test_helpers::performance(100.0)); - test.reward_with_distribution(mix_id2, test_helpers::performance(100.0)); + test.legacy_reward_with_distribution_and_legacy_work_factor( + node_id1, + test_helpers::performance(100.0), + ); + test.legacy_reward_with_distribution_and_legacy_work_factor( + node_id2, + test_helpers::performance(100.0), + ); - let res = try_withdraw_delegator_reward(test.deps_mut(), sender1.clone(), mix_id1); + let res = try_withdraw_delegator_reward(test.deps_mut(), sender1.clone(), node_id1); assert_eq!( res, - Err(MixnetContractError::NoMixnodeDelegationFound { - mix_id: mix_id1, + Err(MixnetContractError::NodeDelegationNotFound { + node_id: node_id1, address: delegator1.to_string(), proxy: None, }) ); // sanity check for other ones - let res = try_withdraw_delegator_reward(test.deps_mut(), sender1, mix_id2); + let res = try_withdraw_delegator_reward(test.deps_mut(), sender1, node_id2); assert!(res.is_ok()); - let res = try_withdraw_delegator_reward(test.deps_mut(), sender2.clone(), mix_id1); + let res = try_withdraw_delegator_reward(test.deps_mut(), sender2.clone(), node_id1); assert!(res.is_ok()); - let res = try_withdraw_delegator_reward(test.deps_mut(), sender2, mix_id2); + let res = try_withdraw_delegator_reward(test.deps_mut(), sender2, node_id2); assert!(res.is_ok()); } @@ -1398,38 +1250,44 @@ pub mod tests { fn tokens_are_only_sent_if_reward_is_nonzero() { let mut test = TestSetup::new(); // add relatively huge stake so that the reward would be high enough to offset operating costs - let mix_id1 = - test.add_dummy_mixnode("mix-owner1", Some(Uint128::new(1_000_000_000_000))); - let mix_id2 = - test.add_dummy_mixnode("mix-owner2", Some(Uint128::new(1_000_000_000_000))); + let node_id1 = test + .add_rewarded_legacy_mixnode("mix-owner1", Some(Uint128::new(1_000_000_000_000))); + let node_id2 = test + .add_rewarded_legacy_mixnode("mix-owner2", Some(Uint128::new(1_000_000_000_000))); // very low stake so operating cost would be higher than total reward let low_stake_id = - test.add_dummy_mixnode("mix-owner3", Some(Uint128::new(100_000_000))); + test.add_rewarded_legacy_mixnode("mix-owner3", Some(Uint128::new(100_000_000))); let delegator = "delegator"; let sender = mock_info(delegator, &[]); - test.add_immediate_delegation(delegator, 100_000_000u128, mix_id1); - test.add_immediate_delegation(delegator, 100_000_000u128, mix_id2); + test.add_immediate_delegation(delegator, 100_000_000u128, node_id1); + test.add_immediate_delegation(delegator, 100_000_000u128, node_id2); test.add_immediate_delegation(delegator, 1_000u128, low_stake_id); // reward mix1, but don't reward mix2 test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id1, low_stake_id]); + test.force_change_rewarded_set(vec![node_id1, low_stake_id]); test.start_epoch_transition(); - test.reward_with_distribution(mix_id1, test_helpers::performance(100.0)); - test.reward_with_distribution(low_stake_id, test_helpers::performance(100.0)); + test.legacy_reward_with_distribution_and_legacy_work_factor( + node_id1, + test_helpers::performance(100.0), + ); + test.legacy_reward_with_distribution_and_legacy_work_factor( + low_stake_id, + test_helpers::performance(100.0), + ); let res1 = - try_withdraw_delegator_reward(test.deps_mut(), sender.clone(), mix_id1).unwrap(); + try_withdraw_delegator_reward(test.deps_mut(), sender.clone(), node_id1).unwrap(); assert!(matches!( &res1.messages[0].msg, CosmosMsg::Bank(BankMsg::Send { to_address, amount }) if to_address == delegator && !amount[0].amount.is_zero() ),); let res2 = - try_withdraw_delegator_reward(test.deps_mut(), sender.clone(), mix_id2).unwrap(); + try_withdraw_delegator_reward(test.deps_mut(), sender.clone(), node_id2).unwrap(); assert!(res2.messages.is_empty()); let res3 = @@ -1442,27 +1300,33 @@ pub mod tests { // note: if node has unbonded or is in the process of unbonding, the expected // way of getting back the rewards is to completely undelegate let mut test = TestSetup::new(); - let mix_id_unbonding = - test.add_dummy_mixnode("mix-owner1", Some(Uint128::new(1_000_000_000_000))); - let mix_id_unbonded_leftover = - test.add_dummy_mixnode("mix-owner2", Some(Uint128::new(1_000_000_000_000))); + let node_id_unbonding = test + .add_rewarded_legacy_mixnode("mix-owner1", Some(Uint128::new(1_000_000_000_000))); + let node_id_unbonded_leftover = test + .add_rewarded_legacy_mixnode("mix-owner2", Some(Uint128::new(1_000_000_000_000))); let delegator = "delegator"; let sender = mock_info(delegator, &[]); - test.add_immediate_delegation(delegator, 100_000_000u128, mix_id_unbonding); - test.add_immediate_delegation(delegator, 100_000_000u128, mix_id_unbonded_leftover); + test.add_immediate_delegation(delegator, 100_000_000u128, node_id_unbonding); + test.add_immediate_delegation(delegator, 100_000_000u128, node_id_unbonded_leftover); let performance = test_helpers::performance(100.0); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id_unbonding, mix_id_unbonded_leftover]); + test.force_change_rewarded_set(vec![node_id_unbonding, node_id_unbonded_leftover]); // go through few rewarding cycles before unbonding nodes (partially or fully) for _ in 0..10 { test.start_epoch_transition(); - test.reward_with_distribution(mix_id_unbonding, performance); - test.reward_with_distribution(mix_id_unbonded_leftover, performance); + test.legacy_reward_with_distribution_and_legacy_work_factor( + node_id_unbonding, + performance, + ); + test.legacy_reward_with_distribution_and_legacy_work_factor( + node_id_unbonded_leftover, + performance, + ); test.skip_to_next_epoch_end(); // bypass proper epoch progression and force change the state @@ -1471,32 +1335,32 @@ pub mod tests { // start unbonding the first node and fully unbond the other let mut bond = mixnodes_storage::mixnode_bonds() - .load(test.deps().storage, mix_id_unbonding) + .load(test.deps().storage, node_id_unbonding) .unwrap(); bond.is_unbonding = true; mixnodes_storage::mixnode_bonds() - .save(test.deps_mut().storage, mix_id_unbonding, &bond) + .save(test.deps_mut().storage, node_id_unbonding, &bond) .unwrap(); let env = test.env(); - pending_events::unbond_mixnode(test.deps_mut(), &env, 123, mix_id_unbonded_leftover) + pending_events::unbond_mixnode(test.deps_mut(), &env, 123, node_id_unbonded_leftover) .unwrap(); let res = - try_withdraw_delegator_reward(test.deps_mut(), sender.clone(), mix_id_unbonding); + try_withdraw_delegator_reward(test.deps_mut(), sender.clone(), node_id_unbonding); assert_eq!( res, Err(MixnetContractError::MixnodeIsUnbonding { - mix_id: mix_id_unbonding + mix_id: node_id_unbonding }) ); let res = - try_withdraw_delegator_reward(test.deps_mut(), sender, mix_id_unbonded_leftover); + try_withdraw_delegator_reward(test.deps_mut(), sender, node_id_unbonded_leftover); assert_eq!( res, - Err(MixnetContractError::MixnodeHasUnbonded { - mix_id: mix_id_unbonded_leftover + Err(MixnetContractError::NymNodeBondNotFound { + node_id: node_id_unbonded_leftover }) ); } @@ -1504,10 +1368,10 @@ pub mod tests { #[test] fn correctly_determines_earned_share_and_resets_reward_ratio() { let mut test = TestSetup::new(); - let mix_id_single = - test.add_dummy_mixnode("mix-owner1", Some(Uint128::new(1_000_000_000_000))); - let mix_id_quad = - test.add_dummy_mixnode("mix-owner2", Some(Uint128::new(1_000_000_000_000))); + let node_id_single = test + .add_rewarded_legacy_mixnode("mix-owner1", Some(Uint128::new(1_000_000_000_000))); + let node_id_quad = test + .add_rewarded_legacy_mixnode("mix-owner2", Some(Uint128::new(1_000_000_000_000))); let delegator1 = "delegator1"; let delegator2 = "delegator2"; @@ -1525,28 +1389,34 @@ pub mod tests { let amount_quad3 = 250_000_000u128; let amount_quad4 = 500_000_000u128; - test.add_immediate_delegation(delegator1, amount_single, mix_id_single); + test.add_immediate_delegation(delegator1, amount_single, node_id_single); - test.add_immediate_delegation(delegator1, amount_quad1, mix_id_quad); - test.add_immediate_delegation(delegator2, amount_quad2, mix_id_quad); - test.add_immediate_delegation(delegator3, amount_quad3, mix_id_quad); - test.add_immediate_delegation(delegator4, amount_quad4, mix_id_quad); + test.add_immediate_delegation(delegator1, amount_quad1, node_id_quad); + test.add_immediate_delegation(delegator2, amount_quad2, node_id_quad); + test.add_immediate_delegation(delegator3, amount_quad3, node_id_quad); + test.add_immediate_delegation(delegator4, amount_quad4, node_id_quad); let performance = test_helpers::performance(100.0); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id_single, mix_id_quad]); + test.force_change_rewarded_set(vec![node_id_single, node_id_quad]); // accumulate some rewards let mut accumulated_single = Decimal::zero(); let mut accumulated_quad = Decimal::zero(); for _ in 0..10 { test.start_epoch_transition(); - let dist = test.reward_with_distribution(mix_id_single, performance); + let dist = test.legacy_reward_with_distribution_and_legacy_work_factor( + node_id_single, + performance, + ); // sanity check to make sure test is actually doing what it's supposed to be doing assert!(!dist.delegates.is_zero()); accumulated_single += dist.delegates; - let dist = test.reward_with_distribution(mix_id_quad, performance); + let dist = test.legacy_reward_with_distribution_and_legacy_work_factor( + node_id_quad, + performance, + ); accumulated_quad += dist.delegates; test.skip_to_next_epoch_end(); @@ -1554,30 +1424,32 @@ pub mod tests { test.set_epoch_in_progress_state(); } - let before = test.read_delegation(mix_id_single, delegator1, None); + let before = test.read_delegation(node_id_single, delegator1, None); assert_eq!(before.cumulative_reward_ratio, Decimal::zero()); let res1 = - try_withdraw_delegator_reward(test.deps_mut(), sender1.clone(), mix_id_single) + try_withdraw_delegator_reward(test.deps_mut(), sender1.clone(), node_id_single) .unwrap(); let (_, reward) = test_helpers::get_bank_send_msg(&res1).unwrap(); assert_eq!(truncate_reward_amount(accumulated_single), reward[0].amount); - let after = test.read_delegation(mix_id_single, delegator1, None); + let after = test.read_delegation(node_id_single, delegator1, None); assert_ne!(after.cumulative_reward_ratio, Decimal::zero()); assert_eq!( after.cumulative_reward_ratio, - test.mix_rewarding(mix_id_single).total_unit_reward + test.mix_rewarding(node_id_single).total_unit_reward ); // withdraw first two rewards. note that due to scaling we expect second reward to be 4x the first one - let before1 = test.read_delegation(mix_id_quad, delegator1, None); - let before2 = test.read_delegation(mix_id_quad, delegator2, None); + let before1 = test.read_delegation(node_id_quad, delegator1, None); + let before2 = test.read_delegation(node_id_quad, delegator2, None); assert_eq!(before1.cumulative_reward_ratio, Decimal::zero()); assert_eq!(before2.cumulative_reward_ratio, Decimal::zero()); - let res1 = try_withdraw_delegator_reward(test.deps_mut(), sender1.clone(), mix_id_quad) - .unwrap(); + let res1 = + try_withdraw_delegator_reward(test.deps_mut(), sender1.clone(), node_id_quad) + .unwrap(); let (_, reward1) = test_helpers::get_bank_send_msg(&res1).unwrap(); - let res2 = try_withdraw_delegator_reward(test.deps_mut(), sender2.clone(), mix_id_quad) - .unwrap(); + let res2 = + try_withdraw_delegator_reward(test.deps_mut(), sender2.clone(), node_id_quad) + .unwrap(); let (_, reward2) = test_helpers::get_bank_send_msg(&res2).unwrap(); // the seeming "error" comes from reward truncation, // say "actual" reward1 was `100.9`, while reward2 was 4x that, i.e. `403.6 @@ -1592,34 +1464,37 @@ pub mod tests { Uint128::new(4), ); - let after1 = test.read_delegation(mix_id_quad, delegator1, None); + let after1 = test.read_delegation(node_id_quad, delegator1, None); assert_ne!(after1.cumulative_reward_ratio, Decimal::zero()); assert_eq!( after1.cumulative_reward_ratio, - test.mix_rewarding(mix_id_quad).total_unit_reward + test.mix_rewarding(node_id_quad).total_unit_reward ); - let after2 = test.read_delegation(mix_id_quad, delegator2, None); + let after2 = test.read_delegation(node_id_quad, delegator2, None); assert_ne!(after2.cumulative_reward_ratio, Decimal::zero()); assert_eq!( after2.cumulative_reward_ratio, - test.mix_rewarding(mix_id_quad).total_unit_reward + test.mix_rewarding(node_id_quad).total_unit_reward ); // accumulate some more for _ in 0..10 { test.start_epoch_transition(); - let dist = test.reward_with_distribution(mix_id_quad, performance); + let dist = test.legacy_reward_with_distribution_and_legacy_work_factor( + node_id_quad, + performance, + ); accumulated_quad += dist.delegates; test.skip_to_next_epoch_end(); // bypass proper epoch progression and force change the state test.set_epoch_in_progress_state(); } - let before1_new = test.read_delegation(mix_id_quad, delegator1, None); - let before2_new = test.read_delegation(mix_id_quad, delegator2, None); - let before3 = test.read_delegation(mix_id_quad, delegator3, None); - let before4 = test.read_delegation(mix_id_quad, delegator4, None); + let before1_new = test.read_delegation(node_id_quad, delegator1, None); + let before2_new = test.read_delegation(node_id_quad, delegator2, None); + let before3 = test.read_delegation(node_id_quad, delegator3, None); + let before4 = test.read_delegation(node_id_quad, delegator4, None); assert_eq!( before1_new.cumulative_reward_ratio, @@ -1633,10 +1508,10 @@ pub mod tests { assert_eq!(before4.cumulative_reward_ratio, Decimal::zero()); let res1 = - try_withdraw_delegator_reward(test.deps_mut(), sender1, mix_id_quad).unwrap(); + try_withdraw_delegator_reward(test.deps_mut(), sender1, node_id_quad).unwrap(); let (_, reward1_new) = test_helpers::get_bank_send_msg(&res1).unwrap(); let res2 = - try_withdraw_delegator_reward(test.deps_mut(), sender2, mix_id_quad).unwrap(); + try_withdraw_delegator_reward(test.deps_mut(), sender2, node_id_quad).unwrap(); let (_, reward2_new) = test_helpers::get_bank_send_msg(&res2).unwrap(); // the ratio between first and second delegator is still the same @@ -1647,10 +1522,10 @@ pub mod tests { ); let res3 = - try_withdraw_delegator_reward(test.deps_mut(), sender3, mix_id_quad).unwrap(); + try_withdraw_delegator_reward(test.deps_mut(), sender3, node_id_quad).unwrap(); let (_, reward3) = test_helpers::get_bank_send_msg(&res3).unwrap(); let res4 = - try_withdraw_delegator_reward(test.deps_mut(), sender4, mix_id_quad).unwrap(); + try_withdraw_delegator_reward(test.deps_mut(), sender4, node_id_quad).unwrap(); let (_, reward4) = test_helpers::get_bank_send_msg(&res4).unwrap(); // (and so is the ratio between 3rd and 4th) @@ -1677,6 +1552,7 @@ pub mod tests { #[cfg(test)] mod withdrawing_operator_reward { use super::*; + use crate::compat::transactions::try_withdraw_operator_reward; use crate::interval::pending_events; use crate::support::tests::test_helpers::TestSetup; use cosmwasm_std::{Addr, BankMsg, CosmosMsg, Uint128}; @@ -1686,18 +1562,22 @@ pub mod tests { let mut test = TestSetup::new(); let owner = "mix-owner"; - let mix_id = test.add_dummy_mixnode(owner, Some(Uint128::new(1_000_000_000_000))); + let node_id = + test.add_rewarded_legacy_mixnode(owner, Some(Uint128::new(1_000_000_000_000))); let sender = mock_info("random-guy", &[]); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id]); + test.force_change_rewarded_set(vec![node_id]); test.start_epoch_transition(); - test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + test.legacy_reward_with_distribution_and_legacy_work_factor( + node_id, + test_helpers::performance(100.0), + ); let res = try_withdraw_operator_reward(test.deps_mut(), sender.clone()); assert_eq!( res, - Err(MixnetContractError::NoAssociatedMixNodeBond { + Err(MixnetContractError::NoAssociatedNodeBond { owner: sender.sender }) ) @@ -1709,17 +1589,21 @@ pub mod tests { let owner1 = "mix-owner1"; let owner2 = "mix-owner2"; - let mix_id1 = test.add_dummy_mixnode(owner1, Some(Uint128::new(1_000_000_000_000))); - test.add_dummy_mixnode(owner2, Some(Uint128::new(1_000_000_000_000))); + let node_id1 = + test.add_rewarded_legacy_mixnode(owner1, Some(Uint128::new(1_000_000_000_000))); + test.add_rewarded_legacy_mixnode(owner2, Some(Uint128::new(1_000_000_000_000))); let sender1 = mock_info(owner1, &[]); let sender2 = mock_info(owner2, &[]); // reward mix1, but don't reward mix2 test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id1]); + test.force_change_rewarded_set(vec![node_id1]); test.start_epoch_transition(); - test.reward_with_distribution(mix_id1, test_helpers::performance(100.0)); + test.legacy_reward_with_distribution_and_legacy_work_factor( + node_id1, + test_helpers::performance(100.0), + ); let res1 = try_withdraw_operator_reward(test.deps_mut(), sender1).unwrap(); assert!(matches!( @@ -1740,23 +1624,29 @@ pub mod tests { let owner2 = "mix-owner2"; let sender1 = mock_info(owner1, &[]); let sender2 = mock_info(owner2, &[]); - let mix_id_unbonding = - test.add_dummy_mixnode(owner1, Some(Uint128::new(1_000_000_000_000))); - let mix_id_unbonded_leftover = - test.add_dummy_mixnode(owner2, Some(Uint128::new(1_000_000_000_000))); + let node_id_unbonding = + test.add_rewarded_legacy_mixnode(owner1, Some(Uint128::new(1_000_000_000_000))); + let node_id_unbonded_leftover = + test.add_rewarded_legacy_mixnode(owner2, Some(Uint128::new(1_000_000_000_000))); // add some delegation to the second node so that it wouldn't be cleared upon unbonding - test.add_immediate_delegation("delegator", 100_000_000u128, mix_id_unbonded_leftover); + test.add_immediate_delegation("delegator", 100_000_000u128, node_id_unbonded_leftover); let performance = test_helpers::performance(100.0); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id_unbonding, mix_id_unbonded_leftover]); + test.force_change_rewarded_set(vec![node_id_unbonding, node_id_unbonded_leftover]); // go through few rewarding cycles before unbonding nodes (partially or fully) for _ in 0..10 { test.start_epoch_transition(); - test.reward_with_distribution(mix_id_unbonding, performance); - test.reward_with_distribution(mix_id_unbonded_leftover, performance); + test.legacy_reward_with_distribution_and_legacy_work_factor( + node_id_unbonding, + performance, + ); + test.legacy_reward_with_distribution_and_legacy_work_factor( + node_id_unbonded_leftover, + performance, + ); test.skip_to_next_epoch_end(); // bypass proper epoch progression and force change the state @@ -1765,29 +1655,29 @@ pub mod tests { // start unbonding the first node and fully unbond the other let mut bond = mixnodes_storage::mixnode_bonds() - .load(test.deps().storage, mix_id_unbonding) + .load(test.deps().storage, node_id_unbonding) .unwrap(); bond.is_unbonding = true; mixnodes_storage::mixnode_bonds() - .save(test.deps_mut().storage, mix_id_unbonding, &bond) + .save(test.deps_mut().storage, node_id_unbonding, &bond) .unwrap(); let env = test.env(); - pending_events::unbond_mixnode(test.deps_mut(), &env, 123, mix_id_unbonded_leftover) + pending_events::unbond_mixnode(test.deps_mut(), &env, 123, node_id_unbonded_leftover) .unwrap(); let res = try_withdraw_operator_reward(test.deps_mut(), sender1); assert_eq!( res, - Err(MixnetContractError::MixnodeIsUnbonding { - mix_id: mix_id_unbonding + Err(MixnetContractError::NodeIsUnbonding { + node_id: node_id_unbonding }) ); let res = try_withdraw_operator_reward(test.deps_mut(), sender2); assert_eq!( res, - Err(MixnetContractError::NoAssociatedMixNodeBond { + Err(MixnetContractError::NoAssociatedNodeBond { owner: Addr::unchecked(owner2) }) ); @@ -1797,6 +1687,7 @@ pub mod tests { #[cfg(test)] mod updating_active_set { use cw_controllers::AdminError::NotAdmin; + use mixnet_contract_common::nym_node::Role; use mixnet_contract_common::EpochStatus; use crate::support::tests::test_helpers::TestSetup; @@ -1811,7 +1702,9 @@ pub mod tests { final_node_id: 0, }, EpochState::ReconcilingEvents, - EpochState::AdvancingEpoch, + EpochState::RoleAssignment { + next: Role::first(), + }, ]; for bad_state in bad_states { @@ -1825,11 +1718,11 @@ pub mod tests { interval_storage::save_current_epoch_status(test.deps_mut().storage, &status) .unwrap(); - let res = try_update_active_set_size( + let res = try_update_active_set_distribution( test.deps_mut(), env.clone(), owner.clone(), - 100, + active_set_update_fixture(), false, ); assert!(matches!( @@ -1837,8 +1730,13 @@ pub mod tests { Err(MixnetContractError::EpochAdvancementInProgress { .. }) )); - let res_forced = - try_update_active_set_size(test.deps_mut(), env.clone(), owner, 100, true); + let res_forced = try_update_active_set_distribution( + test.deps_mut(), + env.clone(), + owner, + active_set_update_fixture(), + true, + ); assert!(res_forced.is_ok()) } } @@ -1852,19 +1750,31 @@ pub mod tests { let random = mock_info("random-guy", &[]); let env = test.env(); - let res = try_update_active_set_size( + let res = try_update_active_set_distribution( test.deps_mut(), env.clone(), rewarding_validator, - 42, + active_set_update_fixture(), false, ); assert_eq!(res, Err(MixnetContractError::Admin(NotAdmin {}))); - let res = try_update_active_set_size(test.deps_mut(), env.clone(), random, 42, false); + let res = try_update_active_set_distribution( + test.deps_mut(), + env.clone(), + random, + active_set_update_fixture(), + false, + ); assert_eq!(res, Err(MixnetContractError::Admin(NotAdmin {}))); - let res = try_update_active_set_size(test.deps_mut(), env, owner, 42, false); + let res = try_update_active_set_distribution( + test.deps_mut(), + env, + owner, + active_set_update_fixture(), + false, + ); assert!(res.is_ok()) } @@ -1876,14 +1786,18 @@ pub mod tests { let rewarded_set_size = storage::REWARDING_PARAMS .load(test.deps().storage) .unwrap() - .rewarded_set_size; + .rewarded_set_size(); let env = test.env(); - let res = try_update_active_set_size( + let res = try_update_active_set_distribution( test.deps_mut(), env, owner.clone(), - rewarded_set_size + 1, + ActiveSetUpdate { + entry_gateways: rewarded_set_size, + exit_gateways: rewarded_set_size, + mixnodes: rewarded_set_size * 3, + }, false, ); assert_eq!(res, Err(MixnetContractError::InvalidActiveSetSize)); @@ -1892,11 +1806,15 @@ pub mod tests { // (make sure we start with the fresh state) let mut test = TestSetup::new(); let env = test.env(); - let res = try_update_active_set_size( + let res = try_update_active_set_distribution( test.deps_mut(), env, owner.clone(), - rewarded_set_size, + ActiveSetUpdate { + entry_gateways: rewarded_set_size / 3, + exit_gateways: rewarded_set_size / 3, + mixnodes: rewarded_set_size / 3, + }, false, ); assert!(res.is_ok()); @@ -1904,11 +1822,15 @@ pub mod tests { // as well as if its any value lower than that let mut test = TestSetup::new(); let env = test.env(); - let res = try_update_active_set_size( + let res = try_update_active_set_distribution( test.deps_mut(), env, owner, - rewarded_set_size - 100, + ActiveSetUpdate { + entry_gateways: 1, + exit_gateways: 1, + mixnodes: 3, + }, false, ); assert!(res.is_ok()); @@ -1922,27 +1844,37 @@ pub mod tests { let old = storage::REWARDING_PARAMS .load(test.deps().storage) .unwrap() - .active_set_size; + .active_set_size(); test.skip_to_current_interval_end(); let env = test.env(); - let res = try_update_active_set_size(test.deps_mut(), env, owner.clone(), 42, false); + + let update = active_set_update_fixture(); + let expected = update.active_set_size(); + let res = try_update_active_set_distribution( + test.deps_mut(), + env, + owner.clone(), + update, + false, + ); assert!(res.is_ok()); let new = storage::REWARDING_PARAMS .load(test.deps().storage) .unwrap() - .active_set_size; + .active_set_size(); assert_ne!(old, new); - assert_eq!(new, 42); + assert_eq!(new, expected); // sanity check for "normal" case let mut test = TestSetup::new(); let env = test.env(); - let res = try_update_active_set_size(test.deps_mut(), env, owner, 42, false); + let res = + try_update_active_set_distribution(test.deps_mut(), env, owner, update, false); assert!(res.is_ok()); let new = storage::REWARDING_PARAMS .load(test.deps().storage) .unwrap() - .active_set_size; + .active_set_size(); assert_eq!(old, new); } @@ -1954,16 +1886,19 @@ pub mod tests { let old = storage::REWARDING_PARAMS .load(test.deps().storage) .unwrap() - .active_set_size; + .active_set_size(); let env = test.env(); - let res = try_update_active_set_size(test.deps_mut(), env, owner, 42, true); + + let update = active_set_update_fixture(); + let expected = update.active_set_size(); + let res = try_update_active_set_distribution(test.deps_mut(), env, owner, update, true); assert!(res.is_ok()); let new = storage::REWARDING_PARAMS .load(test.deps().storage) .unwrap() - .active_set_size; + .active_set_size(); assert_ne!(old, new); - assert_eq!(new, 42); + assert_eq!(new, expected); } #[test] @@ -1974,29 +1909,39 @@ pub mod tests { let old = storage::REWARDING_PARAMS .load(test.deps().storage) .unwrap() - .active_set_size; + .active_set_size(); let env = test.env(); - let res = try_update_active_set_size(test.deps_mut(), env, owner, 42, false); + + let issued_update = active_set_update_fixture(); + let expected_updated = issued_update.active_set_size(); + let res = try_update_active_set_distribution( + test.deps_mut(), + env, + owner, + issued_update, + false, + ); assert!(res.is_ok()); let new = storage::REWARDING_PARAMS .load(test.deps().storage) .unwrap() - .active_set_size; + .active_set_size(); assert_eq!(old, new); // make sure it's actually saved to pending events let events = test.pending_epoch_events(); - assert!( - matches!(events[0].kind, PendingEpochEventKind::UpdateActiveSetSize { new_size } if new_size == 42) - ); + let PendingEpochEventKind::UpdateActiveSet { update } = events[0].kind else { + panic!("unexpected epoch event") + }; + assert_eq!(update, issued_update); test.execute_all_pending_events(); let new = storage::REWARDING_PARAMS .load(test.deps().storage) .unwrap() - .active_set_size; + .active_set_size(); assert_ne!(old, new); - assert_eq!(new, 42); + assert_eq!(new, expected_updated); } } @@ -2005,6 +1950,8 @@ pub mod tests { use cosmwasm_std::Decimal; use cw_controllers::AdminError::NotAdmin; + use mixnet_contract_common::nym_node::Role; + use mixnet_contract_common::reward_params::RewardedSetParams; use mixnet_contract_common::EpochStatus; use crate::support::tests::test_helpers::{assert_decimals, TestSetup}; @@ -2019,7 +1966,9 @@ pub mod tests { final_node_id: 0, }, EpochState::ReconcilingEvents, - EpochState::AdvancingEpoch, + EpochState::RoleAssignment { + next: Role::first(), + }, ]; let update = IntervalRewardingParamsUpdate { @@ -2029,7 +1978,12 @@ pub mod tests { sybil_resistance_percent: None, active_set_work_factor: None, interval_pool_emission: None, - rewarded_set_size: Some(123), + rewarded_set_params: Some(RewardedSetParams { + entry_gateways: 123, + exit_gateways: 123, + mixnodes: 300, + standby: 123, + }), }; for bad_state in bad_states { @@ -2076,7 +2030,12 @@ pub mod tests { sybil_resistance_percent: None, active_set_work_factor: None, interval_pool_emission: None, - rewarded_set_size: Some(123), + rewarded_set_params: Some(RewardedSetParams { + entry_gateways: 123, + exit_gateways: 123, + mixnodes: 300, + standby: 123, + }), }; let env = test.env(); @@ -2109,7 +2068,7 @@ pub mod tests { sybil_resistance_percent: None, active_set_work_factor: None, interval_pool_emission: None, - rewarded_set_size: None, + rewarded_set_params: None, }; let env = test.env(); @@ -2129,7 +2088,12 @@ pub mod tests { sybil_resistance_percent: None, active_set_work_factor: None, interval_pool_emission: None, - rewarded_set_size: Some(123), + rewarded_set_params: Some(RewardedSetParams { + entry_gateways: 123, + exit_gateways: 123, + mixnodes: 300, + standby: 123, + }), }; let old = storage::REWARDING_PARAMS.load(test.deps().storage).unwrap(); @@ -2141,7 +2105,7 @@ pub mod tests { assert!(res.is_ok()); let new = storage::REWARDING_PARAMS.load(test.deps().storage).unwrap(); assert_ne!(old, new); - assert_eq!(new.rewarded_set_size, 123); + assert_eq!(new.rewarded_set_size(), 669); // sanity check for "normal" case let mut test = TestSetup::new(); @@ -2164,7 +2128,12 @@ pub mod tests { sybil_resistance_percent: None, active_set_work_factor: None, interval_pool_emission: None, - rewarded_set_size: Some(123), + rewarded_set_params: Some(RewardedSetParams { + entry_gateways: 123, + exit_gateways: 123, + mixnodes: 300, + standby: 123, + }), }; let old = storage::REWARDING_PARAMS.load(test.deps().storage).unwrap(); @@ -2173,7 +2142,7 @@ pub mod tests { assert!(res.is_ok()); let new = storage::REWARDING_PARAMS.load(test.deps().storage).unwrap(); assert_ne!(old, new); - assert_eq!(new.rewarded_set_size, 123); + assert_eq!(new.rewarded_set_size(), 669); } #[test] @@ -2188,7 +2157,12 @@ pub mod tests { sybil_resistance_percent: None, active_set_work_factor: None, interval_pool_emission: None, - rewarded_set_size: Some(123), + rewarded_set_params: Some(RewardedSetParams { + entry_gateways: 123, + exit_gateways: 123, + mixnodes: 300, + standby: 123, + }), }; let old = storage::REWARDING_PARAMS.load(test.deps().storage).unwrap(); @@ -2200,14 +2174,18 @@ pub mod tests { // make sure it's actually saved to pending events let events = test.pending_interval_events(); - assert!( - matches!(events[0].kind,PendingIntervalEventKind::UpdateRewardingParams { update } if update.rewarded_set_size == Some(123)) - ); + let PendingIntervalEventKind::UpdateRewardingParams { update } = events[0].kind else { + panic!("unexpected epoch event") + }; + let Some(rewarded_set_update) = update.rewarded_set_params else { + panic!("no rewarded set updates"); + }; + assert_eq!(rewarded_set_update.rewarded_set_size(), 669); test.execute_all_pending_events(); let new = storage::REWARDING_PARAMS.load(test.deps().storage).unwrap(); assert_ne!(old, new); - assert_eq!(new.rewarded_set_size, 123); + assert_eq!(new.rewarded_set_size(), 669); } #[test] @@ -2229,7 +2207,7 @@ pub mod tests { sybil_resistance_percent: None, active_set_work_factor: None, interval_pool_emission: None, - rewarded_set_size: None, + rewarded_set_params: None, }; let env = test.env(); diff --git a/contracts/mixnet/src/support/helpers.rs b/contracts/mixnet/src/support/helpers.rs index 19ddf2bb6e..9d8e158e76 100644 --- a/contracts/mixnet/src/support/helpers.rs +++ b/contracts/mixnet/src/support/helpers.rs @@ -3,11 +3,15 @@ use crate::gateways::storage as gateways_storage; use crate::mixnet_contract_settings::storage as mixnet_params_storage; +use crate::mixnodes::helpers::must_get_mixnode_bond_by_owner; use crate::mixnodes::storage as mixnodes_storage; +use crate::nodes::helpers::must_get_node_bond_by_owner; +use crate::nodes::storage as nymnodes_storage; use cosmwasm_std::{Addr, BankMsg, Coin, CosmosMsg, Response, Storage}; use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::mixnode::PendingMixNodeChanges; -use mixnet_contract_common::{EpochState, EpochStatus, IdentityKeyRef, MixNodeBond}; +use mixnet_contract_common::{EpochState, EpochStatus, IdentityKeyRef, MixNodeBond, NodeId}; +use nym_contracts_common::IdentityKey; use nym_contracts_common::Percent; // helper trait to attach `Msg` to a response if it's provided @@ -132,40 +136,6 @@ pub(crate) fn ensure_epoch_in_progress_state( Ok(()) } -// pub(crate) fn ensure_mix_rewarding_state(storage: &dyn Storage) -> Result<(), MixnetContractError> { -// let epoch_status = crate::interval::storage::current_epoch_status(storage)?; -// if !matches!(epoch_status.state, EpochState::Rewarding { .. }) { -// return Err(MixnetContractError::EpochNotInMixRewardingState { -// current_state: epoch_status.state, -// }); -// } -// Ok(()) -// } -// -// pub(crate) fn ensure_event_reconciliation_state( -// storage: &dyn Storage, -// ) -> Result<(), MixnetContractError> { -// let epoch_status = crate::interval::storage::current_epoch_status(storage)?; -// if !matches!(epoch_status.state, EpochState::ReconcilingEvents) { -// return Err(MixnetContractError::EpochNotInEventReconciliationState { -// current_state: epoch_status.state, -// }); -// } -// Ok(()) -// } -// -// pub(crate) fn ensure_epoch_advancement_state( -// storage: &dyn Storage, -// ) -> Result<(), MixnetContractError> { -// let epoch_status = crate::interval::storage::current_epoch_status(storage)?; -// if !matches!(epoch_status.state, EpochState::AdvancingEpoch) { -// return Err(MixnetContractError::EpochNotInAdvancementState { -// current_state: epoch_status.state, -// }); -// } -// Ok(()) -// } - pub(crate) fn ensure_is_authorized( sender: &Addr, storage: &dyn Storage, @@ -212,12 +182,68 @@ pub(crate) fn ensure_no_pending_pledge_changes( Ok(()) } +pub(crate) fn ensure_no_pending_params_changes( + pending_changes: &PendingMixNodeChanges, +) -> Result<(), MixnetContractError> { + if let Some(pending_event_id) = pending_changes.cost_params_change { + return Err(MixnetContractError::PendingParamsChange { pending_event_id }); + } + Ok(()) +} + +/// get identity key of the currently bonded legacy mixnode or nym-node +#[allow(dead_code)] +pub(crate) fn get_bond_identity( + storage: &dyn Storage, + owner: &Addr, +) -> Result { + // legacy mixnode + if let Ok(bond) = must_get_mixnode_bond_by_owner(storage, owner) { + return Ok(bond.mix_node.identity_key); + } + // current nym-node + must_get_node_bond_by_owner(storage, owner).map(|b| b.node.identity_key) +} + +/// Checks whether a nym-node or a legacy mixnode with the provided id is currently bonded +pub(crate) fn ensure_any_node_bonded( + storage: &dyn Storage, + node_id: NodeId, +) -> Result<(), MixnetContractError> { + // legacy mixnode + if let Some(mixnode_bond) = mixnodes_storage::mixnode_bonds().may_load(storage, node_id)? { + return if mixnode_bond.is_unbonding { + Err(MixnetContractError::MixnodeIsUnbonding { mix_id: node_id }) + } else { + Ok(()) + }; + } + + // current nym-node + match nymnodes_storage::nym_nodes().may_load(storage, node_id)? { + None => return Err(MixnetContractError::NymNodeBondNotFound { node_id }), + Some(bond) if bond.is_unbonding => { + return Err(MixnetContractError::NodeIsUnbonding { node_id }) + } + _ => Ok(()), + } +} + // check if the target address has already bonded a mixnode or gateway, // in either case, return an appropriate error pub(crate) fn ensure_no_existing_bond( sender: &Addr, storage: &dyn Storage, ) -> Result<(), MixnetContractError> { + if nymnodes_storage::nym_nodes() + .idx + .owner + .item(storage, sender.clone())? + .is_some() + { + return Err(MixnetContractError::AlreadyOwnsNymNode); + } + if mixnodes_storage::mixnode_bonds() .idx .owner diff --git a/contracts/mixnet/src/support/legacy.rs b/contracts/mixnet/src/support/legacy.rs new file mode 100644 index 0000000000..755fb6cc8b --- /dev/null +++ b/contracts/mixnet/src/support/legacy.rs @@ -0,0 +1,2 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 diff --git a/contracts/mixnet/src/support/mod.rs b/contracts/mixnet/src/support/mod.rs index f27f7eecd0..41cb1691e3 100644 --- a/contracts/mixnet/src/support/mod.rs +++ b/contracts/mixnet/src/support/mod.rs @@ -2,4 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 pub(crate) mod helpers; +pub(crate) mod legacy; + +const foo: &str = "remove that hack"; +// #[cfg(test)] pub(crate) mod tests; diff --git a/contracts/mixnet/src/support/tests/fixtures.rs b/contracts/mixnet/src/support/tests/fixtures.rs index 5a3d5e38a6..d76b9598b8 100644 --- a/contracts/mixnet/src/support/tests/fixtures.rs +++ b/contracts/mixnet/src/support/tests/fixtures.rs @@ -1,7 +1,11 @@ -use crate::constants::{INITIAL_GATEWAY_PLEDGE_AMOUNT, INITIAL_MIXNODE_PLEDGE_AMOUNT}; +use crate::constants::INITIAL_PLEDGE_AMOUNT; use cosmwasm_std::{coin, Coin}; -use mixnet_contract_common::mixnode::MixNodeCostParams; -use mixnet_contract_common::{Gateway, MixNode, Percent}; +use mixnet_contract_common::mixnode::NodeCostParams; +use mixnet_contract_common::reward_params::ActiveSetUpdate; +use mixnet_contract_common::{ + Gateway, MixNode, Percent, DEFAULT_INTERVAL_OPERATING_COST_AMOUNT, + DEFAULT_PROFIT_MARGIN_PERCENT, +}; pub const TEST_COIN_DENOM: &str = "unym"; @@ -17,10 +21,11 @@ pub fn mix_node_fixture() -> MixNode { } } -pub fn mix_node_cost_params_fixture() -> MixNodeCostParams { - MixNodeCostParams { - profit_margin_percent: Percent::from_percentage_value(10).unwrap(), - interval_operating_cost: coin(40_000_000, TEST_COIN_DENOM), +pub fn node_cost_params_fixture() -> NodeCostParams { + NodeCostParams { + profit_margin_percent: Percent::from_percentage_value(DEFAULT_PROFIT_MARGIN_PERCENT) + .unwrap(), + interval_operating_cost: coin(DEFAULT_INTERVAL_OPERATING_COST_AMOUNT, TEST_COIN_DENOM), } } @@ -36,16 +41,25 @@ pub fn gateway_fixture() -> Gateway { } } -pub fn good_mixnode_pledge() -> Vec { +pub fn good_node_plegge() -> Vec { vec![Coin { denom: TEST_COIN_DENOM.to_string(), - amount: INITIAL_MIXNODE_PLEDGE_AMOUNT, + amount: INITIAL_PLEDGE_AMOUNT, }] } -pub fn good_gateway_pledge() -> Vec { - vec![Coin { - denom: TEST_COIN_DENOM.to_string(), - amount: INITIAL_GATEWAY_PLEDGE_AMOUNT, - }] +pub fn good_mixnode_pledge() -> Vec { + good_node_plegge() +} + +pub fn good_gateway_pledge() -> Vec { + good_node_plegge() +} + +pub fn active_set_update_fixture() -> ActiveSetUpdate { + ActiveSetUpdate { + entry_gateways: 30, + exit_gateways: 30, + mixnodes: 30, + } } diff --git a/contracts/mixnet/src/support/tests/legacy.rs b/contracts/mixnet/src/support/tests/legacy.rs new file mode 100644 index 0000000000..d74808cdf1 --- /dev/null +++ b/contracts/mixnet/src/support/tests/legacy.rs @@ -0,0 +1,65 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::gateways::storage as gateways_storage; +use crate::gateways::storage::PREASSIGNED_LEGACY_IDS; +use crate::interval::storage as interval_storage; +use crate::mixnodes::storage as mixnodes_storage; +use crate::nodes::storage::next_nymnode_id_counter; +use crate::rewards::storage as rewards_storage; +use cosmwasm_std::{Addr, Coin, Env, Storage}; +use mixnet_contract_common::error::MixnetContractError; +use mixnet_contract_common::{ + Gateway, GatewayBond, MixNode, MixNodeBond, NodeCostParams, NodeId, NodeRewarding, +}; +use nym_contracts_common::IdentityKey; + +pub(crate) fn save_new_mixnode( + storage: &mut dyn Storage, + env: Env, + mixnode: MixNode, + cost_params: NodeCostParams, + owner: Addr, + pledge: Coin, +) -> Result { + let mix_id = next_nymnode_id_counter(storage)?; + let current_epoch = interval_storage::current_interval(storage)?.current_epoch_absolute_id(); + + let mixnode_rewarding = NodeRewarding::initialise_new(cost_params, &pledge, current_epoch)?; + let mixnode_bond = MixNodeBond { + mix_id, + owner, + original_pledge: pledge, + mix_node: mixnode, + proxy: None, + bonding_height: env.block.height, + is_unbonding: false, + }; + + // save mixnode bond data + // note that this implicitly checks for uniqueness on identity key, sphinx key and owner + mixnodes_storage::mixnode_bonds().save(storage, mix_id, &mixnode_bond)?; + + // save rewarding data + rewards_storage::MIXNODE_REWARDING.save(storage, mix_id, &mixnode_rewarding)?; + + Ok(mix_id) +} + +pub(crate) fn save_new_gateway( + storage: &mut dyn Storage, + env: Env, + gateway: Gateway, + owner: Addr, + pledge: Coin, +) -> Result { + let gateway_identity = gateway.identity_key.clone(); + let bond = GatewayBond::new(pledge.clone(), owner.clone(), env.block.height, gateway); + + gateways_storage::gateways().save(storage, bond.identity(), &bond)?; + + let id = next_nymnode_id_counter(storage)?; + PREASSIGNED_LEGACY_IDS.save(storage, gateway_identity.clone(), &id)?; + + Ok(gateway_identity) +} diff --git a/contracts/mixnet/src/support/tests/messages.rs b/contracts/mixnet/src/support/tests/messages.rs deleted file mode 100644 index 61a0815d91..0000000000 --- a/contracts/mixnet/src/support/tests/messages.rs +++ /dev/null @@ -1,36 +0,0 @@ -use cosmwasm_std::{Coin, Deps}; -use mixnet_contract_common::{ExecuteMsg, Gateway, IdentityKey}; -use nym_crypto::asymmetric::identity; -use rand_chacha::rand_core::{CryptoRng, RngCore}; - -use crate::support::tests; -use crate::support::tests::test_helpers::{ed25519_sign_message, gateway_bonding_sign_payload}; - -pub(crate) fn valid_bond_gateway_msg( - mut rng: impl RngCore + CryptoRng, - deps: Deps<'_>, - stake: Vec, - sender: &str, -) -> (ExecuteMsg, IdentityKey) { - let keypair = identity::KeyPair::new(&mut rng); - let identity_key = keypair.public_key().to_base58_string(); - let legit_sphinx_keys = nym_crypto::asymmetric::encryption::KeyPair::new(&mut rng); - - let gateway = Gateway { - identity_key, - sphinx_key: legit_sphinx_keys.public_key().to_base58_string(), - ..tests::fixtures::gateway_fixture() - }; - - let msg = gateway_bonding_sign_payload(deps, sender, gateway.clone(), stake); - let owner_signature = ed25519_sign_message(msg, keypair.private_key()); - - let identity_key = keypair.public_key().to_base58_string(); - ( - ExecuteMsg::BondGateway { - gateway, - owner_signature, - }, - identity_key, - ) -} diff --git a/contracts/mixnet/src/support/tests/mod.rs b/contracts/mixnet/src/support/tests/mod.rs index dee121b2f0..de41b21d43 100644 --- a/contracts/mixnet/src/support/tests/mod.rs +++ b/contracts/mixnet/src/support/tests/mod.rs @@ -3,40 +3,46 @@ #[cfg(test)] pub mod fixtures; -#[cfg(test)] -pub mod messages; +pub(crate) mod legacy; #[cfg(test)] pub mod test_helpers { use crate::constants; - use crate::contract::instantiate; - use crate::delegations::queries::query_mixnode_delegations_paged; + use crate::contract::{execute, instantiate}; + use crate::delegations::queries::query_node_delegations_paged; use crate::delegations::storage as delegations_storage; - use crate::delegations::transactions::try_delegate_to_mixnode; - use crate::families::transactions::{try_create_family, try_join_family}; - use crate::gateways::transactions::try_add_gateway; + use crate::delegations::transactions::try_delegate_to_node; use crate::interval::transactions::{ perform_pending_epoch_actions, perform_pending_interval_actions, try_begin_epoch_transition, }; use crate::interval::{pending_events, storage as interval_storage}; - use crate::mixnet_contract_settings::storage::{self as mixnet_params_storage}; use crate::mixnet_contract_settings::storage::{ - minimum_gateway_pledge, minimum_mixnode_pledge, rewarding_denom, - rewarding_validator_address, + self as mixnet_params_storage, minimum_node_pledge, }; + use crate::mixnet_contract_settings::storage::{rewarding_denom, rewarding_validator_address}; use crate::mixnodes::storage as mixnodes_storage; use crate::mixnodes::storage::mixnode_bonds; - use crate::mixnodes::transactions::{try_add_mixnode, try_remove_mixnode}; + use crate::mixnodes::transactions::try_remove_mixnode; + use crate::nodes::helpers::{get_node_details_by_identity, must_get_node_bond_by_owner}; + use crate::nodes::storage as nymnodes_storage; + use crate::nodes::storage::helpers::RoleStorageBucket; + use crate::nodes::storage::rewarded_set::{ACTIVE_ROLES_BUCKET, ROLES, ROLES_METADATA}; + use crate::nodes::storage::{read_assigned_roles, save_assignment, swap_active_role_bucket}; + use crate::nodes::transactions::{try_add_nym_node, try_remove_nym_node}; + use crate::rewards::helpers::expensive_role_lookup; use crate::rewards::queries::{ query_pending_delegator_reward, query_pending_mixnode_operator_reward, }; use crate::rewards::storage as rewards_storage; - use crate::rewards::transactions::try_reward_mixnode; + use crate::rewards::storage::RewardingStorage; + use crate::rewards::transactions::try_reward_node; use crate::signing::storage as signing_storage; + use crate::support::helpers::ensure_no_existing_bond; use crate::support::tests; use crate::support::tests::fixtures::{ - good_gateway_pledge, good_mixnode_pledge, TEST_COIN_DENOM, + good_gateway_pledge, good_mixnode_pledge, good_node_plegge, TEST_COIN_DENOM, }; + use crate::support::tests::{legacy, test_helpers}; use cosmwasm_std::testing::mock_dependencies; use cosmwasm_std::testing::mock_env; use cosmwasm_std::testing::mock_info; @@ -48,21 +54,25 @@ pub mod test_helpers { use cosmwasm_std::{Deps, OwnedDeps}; use cosmwasm_std::{DepsMut, MessageInfo}; use cosmwasm_std::{Env, Response, Timestamp, Uint128}; + use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::events::{ may_find_attribute, MixnetEventType, DELEGATES_REWARD_KEY, OPERATOR_REWARD_KEY, }; - use mixnet_contract_common::families::FamilyHead; - use mixnet_contract_common::mixnode::{MixNodeRewarding, UnbondedMixnode}; + use mixnet_contract_common::mixnode::{NodeRewarding, UnbondedMixnode}; + use mixnet_contract_common::nym_node::{RewardedSetMetadata, Role}; use mixnet_contract_common::pending_events::{PendingEpochEventData, PendingIntervalEventData}; - use mixnet_contract_common::reward_params::{Performance, RewardingParams}; + use mixnet_contract_common::reward_params::{ + NodeRewardingParameters, Performance, RewardedSetParams, RewardingParams, WorkFactor, + }; use mixnet_contract_common::rewarding::simulator::simulated_node::SimulatedNode; use mixnet_contract_common::rewarding::simulator::Simulator; use mixnet_contract_common::rewarding::RewardDistribution; use mixnet_contract_common::{ - construct_family_join_permit, Delegation, EpochEventId, EpochState, EpochStatus, Gateway, + Delegation, EpochEventId, EpochState, EpochStatus, ExecuteMsg, Gateway, GatewayBondingPayload, IdentityKey, IdentityKeyRef, InitialRewardingParams, InstantiateMsg, - Interval, MixId, MixNode, MixNodeBond, MixnodeBondingPayload, Percent, - RewardedSetNodeStatus, SignableGatewayBondingMsg, SignableMixNodeBondingMsg, + Interval, MixNode, MixNodeBond, MixnodeBondingPayload, NodeId, NymNode, NymNodeBond, + NymNodeBondingPayload, Percent, RoleAssignment, SignableGatewayBondingMsg, + SignableMixNodeBondingMsg, SignableNymNodeBondingMsg, }; use nym_contracts_common::signing::{ ContractMessageContent, MessageSignature, SignableMessage, SigningAlgorithm, SigningPurpose, @@ -72,8 +82,34 @@ pub mod test_helpers { use rand_chacha::rand_core::{CryptoRng, RngCore, SeedableRng}; use rand_chacha::ChaCha20Rng; use serde::Serialize; + use std::collections::HashMap; + use std::fmt::Debug; + use std::str::FromStr; use std::time::Duration; + pub enum NodeQueryType { + ById(NodeId), + ByIdentity(IdentityKey), + ByOwner(Addr), + } + + impl From for NodeQueryType { + fn from(value: NodeId) -> Self { + NodeQueryType::ById(value) + } + } + + impl From for NodeQueryType { + fn from(value: IdentityKey) -> Self { + NodeQueryType::ByIdentity(value) + } + } + impl From for NodeQueryType { + fn from(value: Addr) -> Self { + NodeQueryType::ByOwner(value) + } + } + pub fn assert_eq_with_leeway(a: Uint128, b: Uint128, leeway: Uint128) { if a > b { assert!(a - b <= leeway) @@ -132,6 +168,99 @@ pub mod test_helpers { self.env.clone() } + #[allow(unused)] + pub fn execute( + &mut self, + info: MessageInfo, + msg: ExecuteMsg, + ) -> Result { + let env = self.env.clone(); + execute(self.deps_mut(), env, info, msg) + } + + #[allow(unused)] + pub fn execute_no_funds( + &mut self, + sender: impl Into, + msg: ExecuteMsg, + ) -> Result { + self.execute(self.mock_info(sender), msg) + } + + #[allow(unused)] + pub fn execute_fn( + &mut self, + exec_fn: F, + info: MessageInfo, + ) -> Result + where + F: FnOnce(DepsMut<'_>, Env, MessageInfo) -> Result, + { + let env = self.env().clone(); + exec_fn(self.deps_mut(), env, info) + } + + #[allow(unused)] + pub fn execute_fn_no_funds( + &mut self, + exec_fn: F, + sender: impl Into, + ) -> Result + where + F: FnOnce(DepsMut<'_>, Env, MessageInfo) -> Result, + { + let info = self.mock_info(sender); + self.execute_fn(exec_fn, info) + } + + #[allow(unused)] + #[track_caller] + pub fn assert_simple_execution(&mut self, exec_fn: F, info: MessageInfo) -> Response + where + F: FnOnce(DepsMut<'_>, Env, MessageInfo) -> Result, + { + let caller = std::panic::Location::caller(); + self.execute_fn(exec_fn, info) + .unwrap_or_else(|err| panic!("{caller} failed with: '{err}' ({err:?})")) + } + + #[allow(unused)] + #[track_caller] + pub fn assert_simple_execution_no_funds( + &mut self, + exec_fn: F, + sender: impl Into, + ) -> Response + where + F: FnOnce(DepsMut<'_>, Env, MessageInfo) -> Result, + { + let caller = std::panic::Location::caller(); + self.execute_fn_no_funds(exec_fn, sender) + .unwrap_or_else(|err| panic!("{caller} failed with: '{err}' ({err:?})")) + } + + pub fn get_node_id(&self, query_type: impl Into) -> NodeId { + match query_type.into() { + NodeQueryType::ById(id) => id, + NodeQueryType::ByIdentity(identity) => { + get_node_details_by_identity(&self.deps.storage, identity) + .unwrap() + .unwrap() + .node_id() + } + NodeQueryType::ByOwner(owner) => { + must_get_node_bond_by_owner(&self.deps.storage, &owner) + .unwrap() + .node_id + } + } + } + + #[allow(unused)] + pub fn mock_info(&self, sender: impl Into) -> MessageInfo { + mock_info(&sender.into(), &[]) + } + pub fn rewarding_validator(&self) -> MessageInfo { self.rewarding_validator.clone() } @@ -158,77 +287,54 @@ pub mod test_helpers { interval_storage::current_interval(self.deps().storage).unwrap() } - pub fn rewarded_set(&self) -> Vec<(MixId, RewardedSetNodeStatus)> { - interval_storage::REWARDED_SET - .range(self.deps().storage, None, None, Order::Ascending) - .map(|res| res.unwrap()) - .collect::>() - } - - pub fn generate_family_join_permit( - &mut self, - family_owner_keys: &identity::KeyPair, - member_node: IdentityKeyRef, - ) -> MessageSignature { - let identity = family_owner_keys.public_key().to_base58_string(); - - let head_mixnode = mixnodes_storage::mixnode_bonds() - .idx - .identity_key - .item(self.deps().storage, identity.clone()) - .unwrap() - .map(|record| record.1) - .unwrap(); - - let family_head = FamilyHead::new(&identity); - let owner = head_mixnode.owner; - - let nonce = signing_storage::get_signing_nonce(self.deps().storage, owner).unwrap(); - - let msg = construct_family_join_permit(nonce, family_head, member_node.to_owned()); - - let sig_bytes = family_owner_keys - .private_key() - .sign(msg.to_plaintext().unwrap()) - .to_bytes(); - MessageSignature::from(sig_bytes.as_ref()) + pub fn active_roles_bucket(&self) -> RoleStorageBucket { + ACTIVE_ROLES_BUCKET.load(self.deps().storage).unwrap() } #[allow(unused)] - pub fn join_family( + pub fn active_roles_metadata(&self) -> RewardedSetMetadata { + let bucket = self.active_roles_bucket().other(); + ROLES_METADATA + .load(self.deps().storage, bucket as u8) + .unwrap() + } + + pub fn inactive_roles_metadata(&self) -> RewardedSetMetadata { + let bucket = self.active_roles_bucket().other(); + ROLES_METADATA + .load(self.deps().storage, bucket as u8) + .unwrap() + } + + #[allow(unused)] + pub fn active_roles(&self, role: Role) -> Vec { + let bucket = self.active_roles_bucket().other(); + ROLES + .load(self.deps().storage, (bucket as u8, role)) + .unwrap() + } + + pub fn inactive_roles(&self, role: Role) -> Vec { + let bucket = self.active_roles_bucket().other(); + ROLES + .load(self.deps().storage, (bucket as u8, role)) + .unwrap() + } + + pub fn max_role_count(&self, role: Role) -> u32 { + RewardingStorage::load() + .global_rewarding_params + .load(self.deps().storage) + .unwrap() + .rewarded_set + .maximum_role_count(role) + } + + pub fn set_pending_pledge_change( &mut self, - member: &str, - member_keys: &identity::KeyPair, - head_keys: &identity::KeyPair, + mix_id: NodeId, + event_id: Option, ) { - let member_identity = member_keys.public_key().to_base58_string(); - let head_identity = head_keys.public_key().to_base58_string(); - - let join_permit = self.generate_family_join_permit(head_keys, &member_identity); - let family_head = FamilyHead::new(head_identity); - - try_join_family( - self.deps_mut(), - mock_info(member, &[]), - join_permit, - family_head, - ) - .unwrap(); - } - - #[allow(dead_code)] - pub fn create_dummy_mixnode_with_new_family( - &mut self, - head: &str, - label: &str, - ) -> (MixId, identity::KeyPair) { - let (mix_id, keys) = self.add_dummy_mixnode_with_keypair(head, None); - - try_create_family(self.deps_mut(), mock_info(head, &[]), label.to_string()).unwrap(); - (mix_id, keys) - } - - pub fn set_pending_pledge_change(&mut self, mix_id: MixId, event_id: Option) { let mut changes = mixnodes_storage::PENDING_MIXNODE_CHANGES .load(self.deps().storage, mix_id) .unwrap_or_default(); @@ -239,77 +345,206 @@ pub mod test_helpers { .unwrap(); } - pub fn add_dummy_mixnode(&mut self, owner: &str, stake: Option) -> MixId { - let stake = self.make_mix_pledge(stake); - let (mixnode, owner_signature, _) = - self.mixnode_with_signature(owner, Some(stake.clone())); + pub fn immediately_assign_lowest_mix_layer(&mut self, node_id: NodeId) -> Role { + let active_bucket = ACTIVE_ROLES_BUCKET.load(&self.deps.storage).unwrap(); + + let mut layer1 = read_assigned_roles(&self.deps.storage, Role::Layer1).unwrap(); + let mut layer2 = read_assigned_roles(&self.deps.storage, Role::Layer2).unwrap(); + let mut layer3 = read_assigned_roles(&self.deps.storage, Role::Layer3).unwrap(); + let l1 = layer1.len(); + let l2 = layer2.len(); + let l3 = layer3.len(); + + if l1 <= l2 && l1 <= l3 { + layer1.push(node_id); + ROLES + .save( + &mut self.deps.storage, + (active_bucket as u8, Role::Layer1), + &layer1, + ) + .unwrap(); + + Role::Layer1 + } else if l2 <= l3 && l2 <= l1 { + layer2.push(node_id); + ROLES + .save( + &mut self.deps.storage, + (active_bucket as u8, Role::Layer2), + &layer2, + ) + .unwrap(); + + Role::Layer2 + } else { + layer3.push(node_id); + ROLES + .save( + &mut self.deps.storage, + (active_bucket as u8, Role::Layer3), + &layer3, + ) + .unwrap(); + + Role::Layer3 + } + } + + pub fn add_rewarded_set_nymnode( + &mut self, + owner: &str, + stake: Option, + ) -> (NymNodeBond, MessageSignature, KeyPair) { + let res = self.add_nymnode(owner, stake); + let id = res.0.node_id; + self.immediately_assign_lowest_mix_layer(id); + + res + } + + pub fn add_rewarded_set_nymnode_id( + &mut self, + owner: &str, + stake: Option, + ) -> NodeId { + self.add_rewarded_set_nymnode(owner, stake).0.node_id + } + + pub fn add_nymnode( + &mut self, + owner: &str, + stake: Option, + ) -> (NymNodeBond, MessageSignature, KeyPair) { + let stake = self.make_node_pledge(stake); + let (node, owner_signature, keypair) = + self.node_with_signature(owner, Some(stake.clone())); let info = mock_info(owner, stake.as_ref()); - let current_id_counter = mixnodes_storage::MIXNODE_ID_COUNTER - .may_load(self.deps().storage) - .unwrap() - .unwrap_or_default(); - let env = self.env(); - try_add_mixnode( + try_add_nym_node( self.deps_mut(), env, - info, - mixnode, - tests::fixtures::mix_node_cost_params_fixture(), - owner_signature, + info.clone(), + node, + tests::fixtures::node_cost_params_fixture(), + owner_signature.clone(), ) .unwrap(); - // newly added mixnode gets assigned the current counter + 1 - current_id_counter + 1 + let bond = must_get_node_bond_by_owner(&self.deps.storage, &info.sender).unwrap(); + + (bond, owner_signature, keypair) } - pub fn add_dummy_gateway(&mut self, sender: &str, stake: Option) -> IdentityKey { - let stake = self.make_gateway_pledge(stake); - let (gateway, owner_signature) = - self.gateway_with_signature(sender, Some(stake.clone())); + pub fn add_dummy_nymnode(&mut self, owner: &str, stake: Option) -> NodeId { + self.add_nymnode(owner, stake).0.node_id + } - let info = mock_info(sender, &stake); - let key = gateway.identity_key.clone(); + #[allow(unused)] + pub fn add_dummy_nym_node_with_keypair( + &mut self, + owner: &str, + stake: Option, + ) -> (NodeId, identity::KeyPair) { + let (bond, _, keypair) = self.add_nymnode(owner, stake); + (bond.node_id, keypair) + } + + pub fn add_legacy_mixnode(&mut self, owner: &str, stake: Option) -> NodeId { + let stake = self.make_mix_pledge(stake); + let (mixnode, _, _) = self.mixnode_with_signature(owner, Some(stake.clone())); + + let info = mock_info(owner, stake.as_ref()); let env = self.env(); - try_add_gateway(self.deps_mut(), env, info, gateway, owner_signature).unwrap(); - key + + ensure_no_existing_bond(&info.sender, &self.deps.storage).unwrap(); + signing_storage::increment_signing_nonce(&mut self.deps.storage, info.sender.clone()) + .unwrap(); + let node_id = legacy::save_new_mixnode( + &mut self.deps.storage, + env, + mixnode, + tests::fixtures::node_cost_params_fixture(), + info.sender, + info.funds[0].clone(), + ) + .unwrap(); + + node_id + } + + pub fn add_rewarded_legacy_mixnode( + &mut self, + owner: &str, + stake: Option, + ) -> NodeId { + let node_id = self.add_legacy_mixnode(owner, stake); + self.immediately_assign_lowest_mix_layer(node_id); + + node_id + } + + pub fn add_legacy_gateway(&mut self, sender: &str, stake: Option) -> IdentityKey { + let stake = self.make_gateway_pledge(stake); + let (gateway, _) = self.gateway_with_signature(sender, Some(stake.clone())); + + let env = self.env(); + let info = mock_info(sender, &stake); + + legacy::save_new_gateway( + &mut self.deps.storage, + env, + gateway, + info.sender, + info.funds[0].clone(), + ) + .unwrap() + } + + pub fn save_legacy_gateway(&mut self, gateway: Gateway, info: &MessageInfo) { + let env = self.env(); + + legacy::save_new_gateway( + &mut self.deps.storage, + env, + gateway, + info.sender.clone(), + info.funds[0].clone(), + ) + .unwrap(); } pub fn add_dummy_mixnodes(&mut self, n: usize) { for i in 0..n { - self.add_dummy_mixnode(&format!("owner{i}"), None); + self.add_legacy_mixnode(&format!("owner{i}"), None); } } pub fn add_dummy_gateways(&mut self, n: usize) { for i in 0..n { - self.add_dummy_gateway(&format!("owner{i}"), None); + self.add_legacy_gateway(&format!("owner{i}"), None); } } - pub fn make_mix_pledge(&self, stake: Option) -> Vec { + pub fn make_node_pledge(&self, stake: Option) -> Vec { let stake = match stake { Some(amount) => { let denom = rewarding_denom(self.deps().storage).unwrap(); Coin { denom, amount } } - None => minimum_mixnode_pledge(self.deps.as_ref().storage).unwrap(), + None => minimum_node_pledge(self.deps.as_ref().storage).unwrap(), }; vec![stake] } + pub fn make_mix_pledge(&self, stake: Option) -> Vec { + self.make_node_pledge(stake) + } + pub fn make_gateway_pledge(&self, stake: Option) -> Vec { - let stake = match stake { - Some(amount) => { - let denom = rewarding_denom(self.deps().storage).unwrap(); - Coin { denom, amount } - } - None => minimum_gateway_pledge(self.deps.as_ref().storage).unwrap(), - }; - vec![stake] + self.make_node_pledge(stake) } pub fn mixnode_bonding_signature( @@ -328,42 +563,48 @@ pub mod test_helpers { &mut self, owner: &str, stake: Option, - ) -> (MixId, identity::KeyPair) { + ) -> (NodeId, identity::KeyPair) { let stake = self.make_mix_pledge(stake); + let (mixnode, _, keypair) = self.mixnode_with_signature(owner, Some(stake.clone())); - let keypair = identity::KeyPair::new(&mut self.rng); - let identity_key = keypair.public_key().to_base58_string(); - let legit_sphinx_keys = nym_crypto::asymmetric::encryption::KeyPair::new(&mut self.rng); - - let mixnode = MixNode { - identity_key, - sphinx_key: legit_sphinx_keys.public_key().to_base58_string(), - ..tests::fixtures::mix_node_fixture() - }; - - let msg = - mixnode_bonding_sign_payload(self.deps(), owner, mixnode.clone(), stake.clone()); - let owner_signature = ed25519_sign_message(msg, keypair.private_key()); - - let info = mock_info(owner, &stake); - let current_id_counter = mixnodes_storage::MIXNODE_ID_COUNTER - .may_load(self.deps().storage) - .unwrap() - .unwrap_or_default(); - + let info = mock_info(owner, stake.as_ref()); let env = self.env(); - try_add_mixnode( - self.deps_mut(), + + ensure_no_existing_bond(&info.sender, &self.deps.storage).unwrap(); + signing_storage::increment_signing_nonce(&mut self.deps.storage, info.sender.clone()) + .unwrap(); + let node_id = legacy::save_new_mixnode( + &mut self.deps.storage, env, - info, mixnode, - tests::fixtures::mix_node_cost_params_fixture(), - owner_signature, + tests::fixtures::node_cost_params_fixture(), + info.sender, + info.funds[0].clone(), ) .unwrap(); - // newly added mixnode gets assigned the current counter + 1 - (current_id_counter + 1, keypair) + (node_id, keypair) + } + + pub fn node_with_signature( + &mut self, + sender: &str, + stake: Option>, + ) -> (NymNode, MessageSignature, KeyPair) { + let stake = stake.unwrap_or(good_node_plegge()); + + let keypair = identity::KeyPair::new(&mut self.rng); + let identity_key = keypair.public_key().to_base58_string(); + + let node = NymNode { + host: "1.2.3.4".to_string(), + custom_http_port: None, + identity_key, + }; + let msg = nymnode_bonding_sign_payload(self.deps(), sender, node.clone(), stake); + let owner_signature = ed25519_sign_message(msg, keypair.private_key()); + + (node, owner_signature, keypair) } pub fn mixnode_with_signature( @@ -390,7 +631,7 @@ pub mod test_helpers { pub fn gateway_with_signature( &mut self, - sender: &str, + sender: impl Into, stake: Option>, ) -> (Gateway, MessageSignature) { let stake = stake.unwrap_or(good_gateway_pledge()); @@ -405,13 +646,18 @@ pub mod test_helpers { ..tests::fixtures::gateway_fixture() }; - let msg = gateway_bonding_sign_payload(self.deps(), sender, gateway.clone(), stake); + let msg = gateway_bonding_sign_payload( + self.deps(), + sender.into().as_str(), + gateway.clone(), + stake, + ); let owner_signature = ed25519_sign_message(msg, keypair.private_key()); (gateway, owner_signature) } - pub fn start_unbonding_mixnode(&mut self, mix_id: MixId) { + pub fn start_unbonding_mixnode(&mut self, mix_id: NodeId) { let bond_details = mixnodes_storage::mixnode_bonds() .load(self.deps().storage, mix_id) .unwrap(); @@ -425,17 +671,45 @@ pub mod test_helpers { .unwrap(); } - pub fn immediately_unbond_mixnode(&mut self, mix_id: MixId) { + pub fn start_unbonding_nymnode(&mut self, node_id: NodeId) { + let bond_details = nymnodes_storage::nym_nodes() + .load(self.deps().storage, node_id) + .unwrap(); + + let env = self.env(); + try_remove_nym_node( + self.deps_mut(), + env, + mock_info(bond_details.owner.as_str(), &[]), + ) + .unwrap(); + } + + #[track_caller] + pub fn immediately_unbond_node(&mut self, node: impl Into) { + let node_id = self.get_node_id(node); + let env = self.env(); + pending_events::unbond_nym_node(self.deps_mut(), &env, env.block.height, node_id) + .unwrap(); + } + + pub fn immediately_unbond_mixnode(&mut self, mix_id: NodeId) { let env = self.env(); pending_events::unbond_mixnode(self.deps_mut(), &env, env.block.height, mix_id) .unwrap(); } + pub fn immediately_unbond_nymnode(&mut self, node_id: NodeId) { + let env = self.env(); + pending_events::unbond_nym_node(self.deps_mut(), &env, env.block.height, node_id) + .unwrap(); + } + pub fn add_immediate_delegation( &mut self, delegator: &str, amount: impl Into, - target: MixId, + target: NodeId, ) { let denom = rewarding_denom(self.deps().storage).unwrap(); let amount = Coin { @@ -459,7 +733,7 @@ pub mod test_helpers { &mut self, delegator: &str, amount: impl Into, - target: MixId, + target: NodeId, ) { let denom = rewarding_denom(self.deps().storage).unwrap(); let amount = Coin { @@ -470,7 +744,7 @@ pub mod test_helpers { delegate(self.deps_mut(), env, delegator, vec![amount], target) } - pub fn remove_immediate_delegation(&mut self, delegator: &str, target: MixId) { + pub fn remove_immediate_delegation(&mut self, delegator: &str, target: NodeId) { let height = self.env.block.height; pending_events::undelegate(self.deps_mut(), height, Addr::unchecked(delegator), target) .unwrap(); @@ -482,6 +756,12 @@ pub mod test_helpers { try_begin_epoch_transition(self.deps_mut(), env, sender).unwrap(); } + pub fn epoch_state(&self) -> EpochState { + interval_storage::current_epoch_status(self.deps().storage) + .unwrap() + .state + } + pub fn set_epoch_in_progress_state(&mut self) { let being_advanced_by = self.rewarding_validator.sender.clone(); interval_storage::save_current_epoch_status( @@ -506,20 +786,22 @@ pub mod test_helpers { .unwrap(); } - pub fn set_epoch_advancement_state(&mut self) { + pub fn set_epoch_role_assignment_state(&mut self) { let being_advanced_by = self.rewarding_validator.sender.clone(); interval_storage::save_current_epoch_status( self.deps_mut().storage, &EpochStatus { being_advanced_by, - state: EpochState::AdvancingEpoch, + state: EpochState::RoleAssignment { + next: Role::first(), + }, }, ) .unwrap(); } #[allow(unused)] - pub fn pending_operator_reward(&mut self, mix: MixId) -> Decimal { + pub fn pending_operator_reward(&mut self, mix: NodeId) -> Decimal { query_pending_mixnode_operator_reward(self.deps(), mix) .unwrap() .amount_earned_detailed @@ -527,7 +809,7 @@ pub mod test_helpers { } #[allow(unused)] - pub fn pending_delegator_reward(&mut self, delegator: &str, target: MixId) -> Decimal { + pub fn pending_delegator_reward(&mut self, delegator: &str, target: NodeId) -> Decimal { query_pending_delegator_reward(self.deps(), delegator.into(), target, None) .unwrap() .amount_earned_detailed @@ -583,16 +865,52 @@ pub mod test_helpers { self.set_epoch_in_progress_state(); } - pub fn force_change_rewarded_set(&mut self, nodes: Vec) { - let active_set_size = rewards_storage::REWARDING_PARAMS - .load(self.deps().storage) - .unwrap() - .active_set_size; - interval_storage::update_rewarded_set(self.deps_mut().storage, active_set_size, nodes) - .unwrap(); + pub fn reset_role_assignment(&mut self) { + let active_bucket = ACTIVE_ROLES_BUCKET.load(&self.deps.storage).unwrap(); + + for role in vec![ + Role::EntryGateway, + Role::ExitGateway, + Role::Layer1, + Role::Layer2, + Role::Layer3, + Role::Standby, + ] { + ROLES + .save(&mut self.deps.storage, (active_bucket as u8, role), &vec![]) + .unwrap(); + } } - pub fn instantiate_simulator(&self, node: MixId) -> Simulator { + // note: this does NOT assign gateway role + pub fn force_change_rewarded_set(&mut self, nodes: Vec) { + // reset current assignment + self.reset_role_assignment(); + let mut roles = HashMap::new(); + for node in nodes { + let role = self.immediately_assign_lowest_mix_layer(node); + let assigned = roles.entry(role).or_insert(Vec::new()); + assigned.push(node) + } + + // we cheat a bit to write to the 'active' bucket instead + swap_active_role_bucket(self.deps_mut().storage).unwrap(); + + for (role, assignment) in roles { + save_assignment( + self.deps_mut().storage, + RoleAssignment { + role, + nodes: assignment, + }, + ) + .unwrap(); + } + + swap_active_role_bucket(self.deps_mut().storage).unwrap(); + } + + pub fn instantiate_simulator(&self, node: NodeId) -> Simulator { simulator_from_single_node_state(self.deps(), node) } @@ -615,39 +933,164 @@ pub mod test_helpers { .collect::>() } + pub fn active_node_work(&self) -> WorkFactor { + self.rewarding_params().active_node_work() + } + + pub fn standby_node_work(&self) -> WorkFactor { + self.rewarding_params().standby_node_work() + } + + pub fn active_node_params(&self, performance: f32) -> NodeRewardingParameters { + NodeRewardingParameters { + performance: test_helpers::performance(performance), + work_factor: self.active_node_work(), + } + } + + pub fn standby_node_params(&self, performance: f32) -> NodeRewardingParameters { + NodeRewardingParameters { + performance: test_helpers::performance(performance), + work_factor: self.standby_node_work(), + } + } + + pub fn reward_with_distribution_ignore_state( + &mut self, + node_id: NodeId, + params: NodeRewardingParameters, + ) -> RewardDistribution { + self.reward_with_distribution_with_state_bypass( + node_id, + params.performance, + params.work_factor, + ) + } + pub fn reward_with_distribution_with_state_bypass( &mut self, - mix_id: MixId, + node_id: NodeId, performance: Performance, + work_factor: WorkFactor, ) -> RewardDistribution { let initial_status = interval_storage::current_epoch_status(self.deps().storage).unwrap(); self.start_epoch_transition(); - let res = self.reward_with_distribution(mix_id, performance); + let res = self.reward_with_distribution( + node_id, + NodeRewardingParameters::new(performance, work_factor), + ); interval_storage::save_current_epoch_status(self.deps_mut().storage, &initial_status) .unwrap(); res } + #[deprecated] + pub fn legacy_reward_with_distribution_with_state_bypass( + &mut self, + node_id: NodeId, + performance: Performance, + ) -> RewardDistribution { + let work_factor = self.get_legacy_rewarding_node_work_factor(node_id); + self.reward_with_distribution_with_state_bypass(node_id, performance, work_factor) + } + + #[track_caller] + pub fn node_role(&self, node_id: NodeId) -> Role { + if read_assigned_roles(&self.deps.storage, Role::EntryGateway) + .unwrap() + .contains(&node_id) + { + Role::EntryGateway + } else if read_assigned_roles(&self.deps.storage, Role::ExitGateway) + .unwrap() + .contains(&node_id) + { + Role::ExitGateway + } else if read_assigned_roles(&self.deps.storage, Role::Layer1) + .unwrap() + .contains(&node_id) + { + Role::Layer1 + } else if read_assigned_roles(&self.deps.storage, Role::Layer2) + .unwrap() + .contains(&node_id) + { + Role::Layer2 + } else if read_assigned_roles(&self.deps.storage, Role::Layer3) + .unwrap() + .contains(&node_id) + { + Role::Layer3 + } else if read_assigned_roles(&self.deps.storage, Role::Standby) + .unwrap() + .contains(&node_id) + { + Role::Standby + } else { + let caller = std::panic::Location::caller(); + panic!("{caller}: no assigned roles") + } + } + + pub fn legacy_rewarding_params( + &self, + node_id: NodeId, + performance: f32, + ) -> NodeRewardingParameters { + let performance = test_helpers::performance(performance); + let work_factor = self.get_legacy_rewarding_node_work_factor(node_id); + NodeRewardingParameters { + performance, + work_factor, + } + } + + pub fn get_legacy_rewarding_node_work_factor(&self, node_id: NodeId) -> Decimal { + let global_rewarding_params = self.rewarding_params(); + let work_factor = + match expensive_role_lookup(self.deps.as_ref().storage, node_id).unwrap() { + None => Decimal::zero(), + Some(Role::Standby) => global_rewarding_params.standby_node_work(), + _ => global_rewarding_params.active_node_work(), + }; + work_factor + } + + pub fn legacy_reward_with_distribution_and_legacy_work_factor( + &mut self, + node_id: NodeId, + performance: Performance, + ) -> RewardDistribution { + let work_factor = self.get_legacy_rewarding_node_work_factor(node_id); + self.reward_with_distribution( + node_id, + NodeRewardingParameters { + performance, + work_factor, + }, + ) + } + pub fn reward_with_distribution( &mut self, - mix_id: MixId, - performance: Performance, + node_id: NodeId, + rewarding_params: NodeRewardingParameters, ) -> RewardDistribution { let env = self.env(); let sender = self.rewarding_validator(); let res = - try_reward_mixnode(self.deps_mut(), env, sender, mix_id, performance).unwrap(); + try_reward_node(self.deps_mut(), env, sender, node_id, rewarding_params).unwrap(); let operator: Decimal = find_attribute( - Some(MixnetEventType::MixnodeRewarding.to_string()), + Some(MixnetEventType::NodeRewarding.to_string()), OPERATOR_REWARD_KEY, &res, ) .parse() .unwrap(); let delegates: Decimal = find_attribute( - Some(MixnetEventType::MixnodeRewarding.to_string()), + Some(MixnetEventType::NodeRewarding.to_string()), DELEGATES_REWARD_KEY, &res, ) @@ -662,7 +1105,7 @@ pub mod test_helpers { pub fn read_delegation( &mut self, - mix: MixId, + mix: NodeId, owner: &str, proxy: Option<&str>, ) -> Delegation { @@ -675,19 +1118,25 @@ pub mod test_helpers { .unwrap() } - pub fn mix_rewarding(&self, node: MixId) -> MixNodeRewarding { + pub fn mix_rewarding(&self, node: NodeId) -> NodeRewarding { rewards_storage::MIXNODE_REWARDING .load(self.deps().storage, node) .unwrap() } #[allow(unused)] - pub fn mix_bond(&self, mix_id: MixId) -> MixNodeBond { + pub fn mix_bond(&self, mix_id: NodeId) -> MixNodeBond { mixnode_bonds().load(self.deps().storage, mix_id).unwrap() } - pub fn delegation(&self, mix: MixId, owner: &str, proxy: &Option) -> Delegation { - read_delegation(self.deps().storage, mix, &Addr::unchecked(owner), proxy).unwrap() + #[track_caller] + pub fn delegation(&self, mix: NodeId, owner: &str, proxy: &Option) -> Delegation { + let caller = std::panic::Location::caller(); + + read_delegation(self.deps().storage, mix, &Addr::unchecked(owner), proxy) + .unwrap_or_else(|| { + panic!("{caller} failed with: delegation for {mix}/{owner} doesn't exist") + }) } } @@ -707,11 +1156,11 @@ pub mod test_helpers { } } - pub fn simulator_from_single_node_state(deps: Deps<'_>, node: MixId) -> Simulator { + pub fn simulator_from_single_node_state(deps: Deps<'_>, node: NodeId) -> Simulator { let mix_rewarding = rewards_storage::MIXNODE_REWARDING .load(deps.storage, node) .unwrap(); - let delegations = query_mixnode_delegations_paged(deps, node, None, None).unwrap(); + let delegations = query_node_delegations_paged(deps, node, None, None).unwrap(); if delegations.delegations.len() as u32 == constants::DELEGATION_PAGE_DEFAULT_RETRIEVAL_LIMIT { @@ -769,6 +1218,50 @@ pub mod test_helpers { panic!("did not find the attribute") } + pub(crate) trait FindAttribute { + fn attribute(&self, event_type: E, attribute: &str) -> String + where + E: Into>, + S: Into; + + fn parsed_attribute(&self, event_type: E, attribute: &str) -> T + where + E: Into>, + S: Into, + T: FromStr, + ::Err: Debug; + + fn decimal(&self, event_type: E, attribute: &str) -> Decimal + where + E: Into>, + S: Into, + { + self.parsed_attribute(event_type, attribute) + } + } + + impl FindAttribute for Response { + fn attribute(&self, event_type: E, attribute: &str) -> String + where + E: Into>, + S: Into, + { + find_attribute(event_type.into(), attribute, self) + } + + fn parsed_attribute(&self, event_type: E, attribute: &str) -> T + where + E: Into>, + S: Into, + T: FromStr, + ::Err: Debug, + { + find_attribute(event_type.into(), attribute, self) + .parse() + .unwrap() + } + } + // using floats in tests is fine // (what it does is converting % value, like 12.34 into `Performance` (`Percent`) // which internally is represented by decimal `0.1234` @@ -842,7 +1335,7 @@ pub mod test_helpers { // (gateway, owner_signature) // } - pub fn add_dummy_delegations(mut deps: DepsMut<'_>, env: Env, mix_id: MixId, n: usize) { + pub fn add_dummy_delegations(mut deps: DepsMut<'_>, env: Env, mix_id: NodeId, n: usize) { for i in 0..n { pending_events::delegate( deps.branch(), @@ -916,7 +1409,7 @@ pub mod test_helpers { deps: DepsMut<'_>, identity_key: Option<&str>, owner: &str, - ) -> MixId { + ) -> NodeId { let id = loop { let candidate = rng.next_u32(); if !mixnodes_storage::unbonded_mixnodes().has(deps.storage, candidate) { @@ -943,13 +1436,28 @@ pub mod test_helpers { id } + pub fn nymnode_bonding_sign_payload( + deps: Deps<'_>, + owner: &str, + node: NymNode, + stake: Vec, + ) -> SignableNymNodeBondingMsg { + let cost_params = tests::fixtures::node_cost_params_fixture(); + let nonce = + signing_storage::get_signing_nonce(deps.storage, Addr::unchecked(owner)).unwrap(); + + let payload = NymNodeBondingPayload::new(node, cost_params); + let content = ContractMessageContent::new(Addr::unchecked(owner), stake, payload); + SignableNymNodeBondingMsg::new(nonce, content) + } + pub fn mixnode_bonding_sign_payload( deps: Deps<'_>, owner: &str, mixnode: MixNode, stake: Vec, ) -> SignableMixNodeBondingMsg { - let cost_params = tests::fixtures::mix_node_cost_params_fixture(); + let cost_params = tests::fixtures::node_cost_params_fixture(); let nonce = signing_storage::get_signing_nonce(deps.storage, Addr::unchecked(owner)).unwrap(); @@ -972,6 +1480,15 @@ pub mod test_helpers { SignableGatewayBondingMsg::new(nonce, content) } + fn intial_rewarded_set_params() -> RewardedSetParams { + RewardedSetParams { + entry_gateways: 50, + exit_gateways: 70, + mixnodes: 120, + standby: 50, + } + } + fn initial_rewarding_params() -> InitialRewardingParams { let reward_pool = 250_000_000_000_000u128; let staking_supply = 100_000_000_000_000u128; @@ -983,8 +1500,7 @@ pub mod test_helpers { sybil_resistance: Percent::from_percentage_value(30).unwrap(), active_set_work_factor: Decimal::from_atomics(10u32, 0).unwrap(), interval_pool_emission: Percent::from_percentage_value(2).unwrap(), - rewarded_set_size: 240, - active_set_size: 100, + rewarded_set_params: intial_rewarded_set_params(), } } @@ -1006,14 +1522,14 @@ pub mod test_helpers { deps } - pub fn delegate(deps: DepsMut<'_>, env: Env, sender: &str, stake: Vec, mix_id: MixId) { + pub fn delegate(deps: DepsMut<'_>, env: Env, sender: &str, stake: Vec, mix_id: NodeId) { let info = mock_info(sender, &stake); - try_delegate_to_mixnode(deps, env, info, mix_id).unwrap(); + try_delegate_to_node(deps, env, info, mix_id).unwrap(); } pub(crate) fn read_delegation( storage: &dyn Storage, - mix: MixId, + mix: NodeId, owner: &Addr, proxy: &Option, ) -> Option { diff --git a/contracts/mixnet/src/testing/legacy.rs b/contracts/mixnet/src/testing/legacy.rs new file mode 100644 index 0000000000..38a919fd33 --- /dev/null +++ b/contracts/mixnet/src/testing/legacy.rs @@ -0,0 +1,12 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn legacy_mixnode_bonding() { + todo!() + } +} diff --git a/contracts/mixnet/src/testing/mod.rs b/contracts/mixnet/src/testing/mod.rs index 30de4b3136..ac99770b50 100644 --- a/contracts/mixnet/src/testing/mod.rs +++ b/contracts/mixnet/src/testing/mod.rs @@ -2,3 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 pub(crate) mod transactions; + +// the purpose of that module is to keep track of tests of legacy features that will eventually be phased out +// such as standalone mixnode/gateway bonding +pub(crate) mod legacy; diff --git a/contracts/mixnet/src/vesting_migration.rs b/contracts/mixnet/src/vesting_migration.rs index 3faf826ec0..cfdc18fcf6 100644 --- a/contracts/mixnet/src/vesting_migration.rs +++ b/contracts/mixnet/src/vesting_migration.rs @@ -10,7 +10,7 @@ use crate::support::helpers::{ }; use cosmwasm_std::{wasm_execute, DepsMut, MessageInfo, Response}; use mixnet_contract_common::error::MixnetContractError; -use mixnet_contract_common::{Delegation, MixId}; +use mixnet_contract_common::{Delegation, NodeId}; use vesting_contract_common::messages::ExecuteMsg as VestingExecuteMsg; pub(crate) fn try_migrate_vested_mixnode( @@ -61,7 +61,7 @@ pub(crate) fn try_migrate_vested_mixnode( pub(crate) fn try_migrate_vested_delegation( deps: DepsMut<'_>, info: MessageInfo, - mix_id: MixId, + mix_id: NodeId, ) -> Result { ensure_epoch_in_progress_state(deps.storage)?; diff --git a/contracts/vesting/src/queries.rs b/contracts/vesting/src/queries.rs index 17330c6dbe..a744db56ad 100644 --- a/contracts/vesting/src/queries.rs +++ b/contracts/vesting/src/queries.rs @@ -8,7 +8,7 @@ use crate::vesting::StorableVestingAccountExt; use contracts_common::{get_build_information, ContractBuildInformation}; use cosmwasm_std::{Coin, Deps, Env, Order, StdResult, Timestamp, Uint128}; use cw_storage_plus::Bound; -use mixnet_contract_common::MixId; +use mixnet_contract_common::NodeId; use vesting_contract_common::{ Account, AccountVestingCoins, AccountsResponse, AllDelegationsResponse, BaseVestingAccountInfo, DelegationTimesResponse, OriginalVestingResponse, Period, PledgeData, VestingCoinsResponse, @@ -259,7 +259,7 @@ pub fn try_get_withdrawn_coins( pub fn try_get_delegation_times( deps: Deps<'_>, vesting_account_address: &str, - mix_id: MixId, + mix_id: NodeId, ) -> Result { let owner = deps.api.addr_validate(vesting_account_address)?; let account = account_from_address(vesting_account_address, deps.storage, deps.api)?; @@ -277,7 +277,7 @@ pub fn try_get_delegation_times( pub fn try_get_all_delegations( deps: Deps<'_>, - start_after: Option<(u32, MixId, BlockTimestampSecs)>, + start_after: Option<(u32, NodeId, BlockTimestampSecs)>, limit: Option, ) -> Result { let limit = limit.unwrap_or(100).min(200) as usize; @@ -314,7 +314,7 @@ pub fn try_get_all_delegations( pub fn try_get_delegation( deps: Deps<'_>, vesting_account_address: &str, - mix_id: MixId, + mix_id: NodeId, block_timestamp_secs: BlockTimestampSecs, ) -> Result { let account = account_from_address(vesting_account_address, deps.storage, deps.api)?; @@ -333,7 +333,7 @@ pub fn try_get_delegation( pub fn try_get_delegation_amount( deps: Deps<'_>, vesting_account_address: &str, - mix_id: MixId, + mix_id: NodeId, ) -> Result { let account = account_from_address(vesting_account_address, deps.storage, deps.api)?; diff --git a/contracts/vesting/src/storage.rs b/contracts/vesting/src/storage.rs index d6d363d36f..a2122de58f 100644 --- a/contracts/vesting/src/storage.rs +++ b/contracts/vesting/src/storage.rs @@ -1,7 +1,7 @@ use cosmwasm_std::{Addr, Api, Storage, Uint128}; use cosmwasm_std::{Coin, Order}; use cw_storage_plus::{Item, Map}; -use mixnet_contract_common::{IdentityKey, MixId}; +use mixnet_contract_common::{IdentityKey, NodeId}; use vesting_contract_common::account::VestingAccountStorageKey; use vesting_contract_common::{Account, PledgeData, VestingContractError}; @@ -40,7 +40,7 @@ pub const _OLD_DELEGATIONS: Map< /// Storage map containing information about tokens delegated towards particular mixnodes /// in the mixnet contract with given vesting account. -pub const DELEGATIONS: Map<'_, (VestingAccountStorageKey, MixId, BlockTimestampSecs), Uint128> = +pub const DELEGATIONS: Map<'_, (VestingAccountStorageKey, NodeId, BlockTimestampSecs), Uint128> = Map::new("dlg_v2"); /// Explicit contract admin that is allowed, among other things, to create new vesting accounts. @@ -53,7 +53,7 @@ pub const MIXNET_CONTRACT_ADDRESS: Item<'_, Addr> = Item::new("mix"); pub const MIX_DENOM: Item<'_, String> = Item::new("den"); pub fn save_delegation( - key: (VestingAccountStorageKey, MixId, BlockTimestampSecs), + key: (VestingAccountStorageKey, NodeId, BlockTimestampSecs), amount: Uint128, storage: &mut dyn Storage, ) -> Result<(), VestingContractError> { @@ -68,7 +68,7 @@ pub fn save_delegation( } pub fn remove_delegation( - key: (VestingAccountStorageKey, MixId, BlockTimestampSecs), + key: (VestingAccountStorageKey, NodeId, BlockTimestampSecs), storage: &mut dyn Storage, ) -> Result<(), VestingContractError> { DELEGATIONS.remove(storage, key); @@ -76,7 +76,7 @@ pub fn remove_delegation( } pub fn load_delegation_timestamps( - prefix: (VestingAccountStorageKey, MixId), + prefix: (VestingAccountStorageKey, NodeId), storage: &dyn Storage, ) -> Result, VestingContractError> { let block_timestamps = DELEGATIONS @@ -87,7 +87,7 @@ pub fn load_delegation_timestamps( } pub fn count_subdelegations_for_mix( - prefix: (VestingAccountStorageKey, MixId), + prefix: (VestingAccountStorageKey, NodeId), storage: &dyn Storage, ) -> u32 { DELEGATIONS diff --git a/contracts/vesting/src/traits/bonding_account.rs b/contracts/vesting/src/traits/bonding_account.rs index 8e4c7c6a62..d4fb13e330 100644 --- a/contracts/vesting/src/traits/bonding_account.rs +++ b/contracts/vesting/src/traits/bonding_account.rs @@ -2,7 +2,7 @@ use contracts_common::signing::MessageSignature; use cosmwasm_std::{Coin, Env, Response, Storage}; use mixnet_contract_common::{ gateway::GatewayConfigUpdate, - mixnode::{MixNodeConfigUpdate, MixNodeCostParams}, + mixnode::{MixNodeConfigUpdate, NodeCostParams}, Gateway, MixNode, }; use vesting_contract_common::VestingContractError; @@ -16,7 +16,7 @@ pub trait MixnodeBondingAccount { fn try_bond_mixnode( &self, mix_node: MixNode, - cost_params: MixNodeCostParams, + cost_params: NodeCostParams, owner_signature: MessageSignature, pledge: Coin, env: &Env, @@ -58,7 +58,7 @@ pub trait MixnodeBondingAccount { fn try_update_mixnode_cost_params( &self, - new_costs: MixNodeCostParams, + new_costs: NodeCostParams, storage: &mut dyn Storage, ) -> Result; fn try_track_migrated_mixnode( diff --git a/contracts/vesting/src/traits/delegating_account.rs b/contracts/vesting/src/traits/delegating_account.rs index 2e7b5a65d3..b0d2154537 100644 --- a/contracts/vesting/src/traits/delegating_account.rs +++ b/contracts/vesting/src/traits/delegating_account.rs @@ -1,17 +1,17 @@ use cosmwasm_std::{Coin, Env, Response, Storage, Uint128}; -use mixnet_contract_common::MixId; +use mixnet_contract_common::NodeId; use vesting_contract_common::VestingContractError; pub trait DelegatingAccount { fn try_claim_delegator_reward( &self, - mix_id: MixId, + mix_id: NodeId, storage: &dyn Storage, ) -> Result; fn try_delegate_to_mixnode( &self, - mix_id: MixId, + mix_id: NodeId, amount: Coin, env: &Env, storage: &mut dyn Storage, @@ -19,7 +19,7 @@ pub trait DelegatingAccount { fn try_undelegate_from_mixnode( &self, - mix_id: MixId, + mix_id: NodeId, storage: &dyn Storage, ) -> Result; @@ -30,7 +30,7 @@ pub trait DelegatingAccount { fn track_delegation( &self, block_height: u64, - mix_id: MixId, + mix_id: NodeId, // Save some gas by passing it in current_balance: Uint128, delegation: Coin, @@ -40,13 +40,13 @@ pub trait DelegatingAccount { // vesting account performs an undelegation. fn track_undelegation( &self, - mix_id: MixId, + mix_id: NodeId, amount: Coin, storage: &mut dyn Storage, ) -> Result<(), VestingContractError>; fn track_migrated_delegation( &self, - mix_id: MixId, + mix_id: NodeId, storage: &mut dyn Storage, ) -> Result<(), VestingContractError>; } diff --git a/contracts/vesting/src/traits/mod.rs b/contracts/vesting/src/traits/mod.rs index d1126bb92c..60cff028d4 100644 --- a/contracts/vesting/src/traits/mod.rs +++ b/contracts/vesting/src/traits/mod.rs @@ -1,9 +1,7 @@ pub mod bonding_account; pub mod delegating_account; -pub mod node_families; pub mod vesting_account; pub use self::bonding_account::{GatewayBondingAccount, MixnodeBondingAccount}; pub use self::delegating_account::DelegatingAccount; -pub use self::node_families::NodeFamilies; pub use self::vesting_account::VestingAccount; diff --git a/contracts/vesting/src/traits/node_families.rs b/contracts/vesting/src/traits/node_families.rs deleted file mode 100644 index f5af460d17..0000000000 --- a/contracts/vesting/src/traits/node_families.rs +++ /dev/null @@ -1,32 +0,0 @@ -use contracts_common::signing::MessageSignature; -use cosmwasm_std::{Response, Storage}; -use mixnet_contract_common::families::FamilyHead; -use mixnet_contract_common::IdentityKeyRef; -use vesting_contract_common::VestingContractError; - -pub trait NodeFamilies { - fn try_create_family( - &self, - storage: &dyn Storage, - label: String, - ) -> Result; - - fn try_join_family( - &self, - storage: &dyn Storage, - join_permit: MessageSignature, - family_head: FamilyHead, - ) -> Result; - - fn try_leave_family( - &self, - storage: &dyn Storage, - family_head: FamilyHead, - ) -> Result; - - fn try_head_kick_member( - &self, - storage: &dyn Storage, - member: IdentityKeyRef<'_>, - ) -> Result; -} diff --git a/contracts/vesting/src/transactions.rs b/contracts/vesting/src/transactions.rs index a51f084862..09e3ac9a8a 100644 --- a/contracts/vesting/src/transactions.rs +++ b/contracts/vesting/src/transactions.rs @@ -6,14 +6,13 @@ use crate::storage::{ account_from_address, save_account, ADMIN, MIXNET_CONTRACT_ADDRESS, MIX_DENOM, }; use crate::traits::{ - DelegatingAccount, GatewayBondingAccount, MixnodeBondingAccount, NodeFamilies, VestingAccount, + DelegatingAccount, GatewayBondingAccount, MixnodeBondingAccount, VestingAccount, }; use crate::vesting::{populate_vesting_periods, StorableVestingAccountExt}; use contracts_common::signing::MessageSignature; use cosmwasm_std::{coin, BankMsg, Coin, DepsMut, Env, MessageInfo, Response, Timestamp}; -use mixnet_contract_common::families::FamilyHead; use mixnet_contract_common::{ - Gateway, GatewayConfigUpdate, MixId, MixNode, MixNodeConfigUpdate, MixNodeCostParams, + Gateway, GatewayConfigUpdate, MixNode, MixNodeConfigUpdate, NodeCostParams, NodeId, }; use vesting_contract_common::events::{ new_ownership_transfer_event, new_periodic_vesting_account_event, @@ -24,40 +23,6 @@ use vesting_contract_common::events::{ }; use vesting_contract_common::{Account, PledgeCap, VestingContractError, VestingSpecification}; -pub fn try_create_family( - info: MessageInfo, - deps: DepsMut, - label: String, -) -> Result { - let account = account_from_address(info.sender.as_ref(), deps.storage, deps.api)?; - account.try_create_family(deps.storage, label) -} -pub fn try_join_family( - info: MessageInfo, - deps: DepsMut, - join_permit: MessageSignature, - family_head: FamilyHead, -) -> Result { - let account = account_from_address(info.sender.as_ref(), deps.storage, deps.api)?; - account.try_join_family(deps.storage, join_permit, family_head) -} -pub fn try_leave_family( - info: MessageInfo, - deps: DepsMut, - family_head: FamilyHead, -) -> Result { - let account = account_from_address(info.sender.as_ref(), deps.storage, deps.api)?; - account.try_leave_family(deps.storage, family_head) -} -pub fn try_kick_family_member( - info: MessageInfo, - deps: DepsMut, - member: String, -) -> Result { - let account = account_from_address(info.sender.as_ref(), deps.storage, deps.api)?; - account.try_head_kick_member(deps.storage, &member) -} - /// Update locked_pledge_cap, the hard cap for staking/bonding with unvested tokens. /// /// Callable by ADMIN only, see [instantiate]. @@ -99,7 +64,7 @@ pub fn try_update_gateway_config( } pub fn try_update_mixnode_cost_params( - new_costs: MixNodeCostParams, + new_costs: NodeCostParams, info: MessageInfo, deps: DepsMut, ) -> Result { @@ -273,7 +238,7 @@ pub fn try_track_migrate_mixnode( /// Track vesting delegation being converted into the usage of liquid tokens. invoked by the mixnet contract after successful migration. pub fn try_track_migrate_delegation( owner: &str, - mix_id: MixId, + mix_id: NodeId, info: MessageInfo, deps: DepsMut<'_>, ) -> Result { @@ -288,7 +253,7 @@ pub fn try_track_migrate_delegation( /// Bond a mixnode, sends [mixnet_contract_common::ExecuteMsg::BondMixnodeOnBehalf] to [crate::storage::MIXNET_CONTRACT_ADDRESS]. pub fn try_bond_mixnode( mix_node: MixNode, - cost_params: MixNodeCostParams, + cost_params: NodeCostParams, owner_signature: MessageSignature, amount: Coin, info: MessageInfo, @@ -392,7 +357,7 @@ pub fn try_track_reward( /// Track undelegation, invoked by the mixnet contract after sucessful undelegation, message contains coins returned with any accrued rewards. pub fn try_track_undelegation( address: &str, - mix_id: MixId, + mix_id: NodeId, amount: Coin, info: MessageInfo, deps: DepsMut<'_>, @@ -408,7 +373,7 @@ pub fn try_track_undelegation( /// Delegate to mixnode, sends [mixnet_contract_common::ExecuteMsg::DelegateToMixnodeOnBehalf] to [crate::storage::MIXNET_CONTRACT_ADDRESS].. pub fn try_delegate_to_mixnode( - mix_id: MixId, + mix_id: NodeId, amount: Coin, on_behalf_of: Option, info: MessageInfo, @@ -452,7 +417,7 @@ pub fn try_claim_operator_reward( pub fn try_claim_delegator_reward( deps: DepsMut<'_>, info: MessageInfo, - mix_id: MixId, + mix_id: NodeId, ) -> Result { let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?; @@ -461,7 +426,7 @@ pub fn try_claim_delegator_reward( /// Undelegates from a mixnode, sends [mixnet_contract_common::ExecuteMsg::UndelegateFromMixnodeOnBehalf] to [crate::storage::MIXNET_CONTRACT_ADDRESS]. pub fn try_undelegate_from_mixnode( - mix_id: MixId, + mix_id: NodeId, on_behalf_of: Option, info: MessageInfo, deps: DepsMut<'_>, diff --git a/contracts/vesting/src/vesting/account/delegating_account.rs b/contracts/vesting/src/vesting/account/delegating_account.rs index d01bea6789..167707401a 100644 --- a/contracts/vesting/src/vesting/account/delegating_account.rs +++ b/contracts/vesting/src/vesting/account/delegating_account.rs @@ -5,7 +5,7 @@ use crate::traits::DelegatingAccount; use crate::vesting::account::StorableVestingAccountExt; use cosmwasm_std::{wasm_execute, Coin, Env, Response, Storage, Uint128}; use mixnet_contract_common::ExecuteMsg as MixnetExecuteMsg; -use mixnet_contract_common::MixId; +use mixnet_contract_common::NodeId; use vesting_contract_common::events::{ new_vesting_delegation_event, new_vesting_undelegation_event, }; @@ -16,7 +16,7 @@ use super::Account; impl DelegatingAccount for Account { fn try_claim_delegator_reward( &self, - mix_id: MixId, + mix_id: NodeId, storage: &dyn Storage, ) -> Result { let msg = MixnetExecuteMsg::WithdrawDelegatorRewardOnBehalf { @@ -32,7 +32,7 @@ impl DelegatingAccount for Account { fn try_delegate_to_mixnode( &self, - mix_id: MixId, + mix_id: NodeId, coin: Coin, env: &Env, storage: &mut dyn Storage, @@ -74,7 +74,7 @@ impl DelegatingAccount for Account { fn try_undelegate_from_mixnode( &self, - mix_id: MixId, + mix_id: NodeId, storage: &dyn Storage, ) -> Result { if !self.any_delegation_for_mix(mix_id, storage) { @@ -99,7 +99,7 @@ impl DelegatingAccount for Account { fn track_delegation( &self, block_timestamp_secs: u64, - mix_id: MixId, + mix_id: NodeId, current_balance: Uint128, delegation: Coin, storage: &mut dyn Storage, @@ -116,7 +116,7 @@ impl DelegatingAccount for Account { fn track_undelegation( &self, - mix_id: MixId, + mix_id: NodeId, amount: Coin, storage: &mut dyn Storage, ) -> Result<(), VestingContractError> { @@ -128,7 +128,7 @@ impl DelegatingAccount for Account { fn track_migrated_delegation( &self, - mix_id: MixId, + mix_id: NodeId, storage: &mut dyn Storage, ) -> Result<(), VestingContractError> { let delegation = self.total_delegations_for_mix(mix_id, storage)?; diff --git a/contracts/vesting/src/vesting/account/mixnode_bonding_account.rs b/contracts/vesting/src/vesting/account/mixnode_bonding_account.rs index ee14207d3e..fb3ede091f 100644 --- a/contracts/vesting/src/vesting/account/mixnode_bonding_account.rs +++ b/contracts/vesting/src/vesting/account/mixnode_bonding_account.rs @@ -8,7 +8,7 @@ use crate::vesting::account::StorableVestingAccountExt; use contracts_common::signing::MessageSignature; use cosmwasm_std::{wasm_execute, Coin, Env, Response, Storage, Uint128}; use mixnet_contract_common::mixnode::MixNodeConfigUpdate; -use mixnet_contract_common::mixnode::MixNodeCostParams; +use mixnet_contract_common::mixnode::NodeCostParams; use mixnet_contract_common::{ExecuteMsg as MixnetExecuteMsg, MixNode}; use vesting_contract_common::events::{ new_vesting_decrease_pledge_event, new_vesting_mixnode_bonding_event, @@ -35,7 +35,7 @@ impl MixnodeBondingAccount for Account { fn try_bond_mixnode( &self, mix_node: MixNode, - cost_params: MixNodeCostParams, + cost_params: NodeCostParams, owner_signature: MessageSignature, pledge: Coin, env: &Env, @@ -206,7 +206,7 @@ impl MixnodeBondingAccount for Account { fn try_update_mixnode_cost_params( &self, - new_costs: MixNodeCostParams, + new_costs: NodeCostParams, storage: &mut dyn Storage, ) -> Result { let msg = MixnetExecuteMsg::UpdateMixnodeCostParamsOnBehalf { diff --git a/contracts/vesting/src/vesting/account/mod.rs b/contracts/vesting/src/vesting/account/mod.rs index c09b6057b5..4353b748eb 100644 --- a/contracts/vesting/src/vesting/account/mod.rs +++ b/contracts/vesting/src/vesting/account/mod.rs @@ -10,14 +10,13 @@ use crate::storage::{ }; use crate::traits::VestingAccount; use cosmwasm_std::{Addr, Coin, Order, Storage, Timestamp, Uint128}; -use mixnet_contract_common::MixId; +use mixnet_contract_common::NodeId; use vesting_contract_common::account::VestingAccountStorageKey; use vesting_contract_common::{Account, PledgeCap, PledgeData, VestingContractError}; mod delegating_account; mod gateway_bonding_account; mod mixnode_bonding_account; -mod node_families; mod vesting_account; fn generate_storage_key( @@ -152,20 +151,20 @@ pub(crate) trait StorableVestingAccountExt: VestingAccount { fn remove_gateway_pledge(&self, storage: &mut dyn Storage) -> Result<(), VestingContractError>; - fn any_delegation_for_mix(&self, mix_id: MixId, storage: &dyn Storage) -> bool; + fn any_delegation_for_mix(&self, mix_id: NodeId, storage: &dyn Storage) -> bool; - fn num_subdelegations_for_mix(&self, mix_id: MixId, storage: &dyn Storage) -> u32; + fn num_subdelegations_for_mix(&self, mix_id: NodeId, storage: &dyn Storage) -> u32; fn remove_delegations_for_mix( &self, - mix_id: MixId, + mix_id: NodeId, storage: &mut dyn Storage, ) -> Result<(), VestingContractError>; #[allow(dead_code)] fn total_delegations_for_mix( &self, - mix_id: MixId, + mix_id: NodeId, storage: &dyn Storage, ) -> Result; @@ -273,7 +272,7 @@ impl StorableVestingAccountExt for Account { remove_gateway_pledge(self.storage_key(), storage) } - fn any_delegation_for_mix(&self, mix_id: MixId, storage: &dyn Storage) -> bool { + fn any_delegation_for_mix(&self, mix_id: NodeId, storage: &dyn Storage) -> bool { DELEGATIONS .prefix((self.storage_key(), mix_id)) .range(storage, None, None, Order::Ascending) @@ -281,13 +280,13 @@ impl StorableVestingAccountExt for Account { .is_some() } - fn num_subdelegations_for_mix(&self, mix_id: MixId, storage: &dyn Storage) -> u32 { + fn num_subdelegations_for_mix(&self, mix_id: NodeId, storage: &dyn Storage) -> u32 { count_subdelegations_for_mix((self.storage_key(), mix_id), storage) } fn remove_delegations_for_mix( &self, - mix_id: MixId, + mix_id: NodeId, storage: &mut dyn Storage, ) -> Result<(), VestingContractError> { // note that the limit is implicitly set to `MAX_PER_MIX_DELEGATIONS` @@ -302,7 +301,7 @@ impl StorableVestingAccountExt for Account { fn total_delegations_for_mix( &self, - mix_id: MixId, + mix_id: NodeId, storage: &dyn Storage, ) -> Result { Ok(DELEGATIONS diff --git a/contracts/vesting/src/vesting/account/node_families.rs b/contracts/vesting/src/vesting/account/node_families.rs deleted file mode 100644 index 649531688b..0000000000 --- a/contracts/vesting/src/vesting/account/node_families.rs +++ /dev/null @@ -1,71 +0,0 @@ -use super::Account; -use crate::{storage::MIXNET_CONTRACT_ADDRESS, traits::NodeFamilies}; -use contracts_common::signing::MessageSignature; -use cosmwasm_std::{wasm_execute, Response, Storage}; -use mixnet_contract_common::families::FamilyHead; -use mixnet_contract_common::{ExecuteMsg as MixnetExecuteMsg, IdentityKeyRef}; -use vesting_contract_common::VestingContractError; - -impl NodeFamilies for Account { - fn try_create_family( - &self, - storage: &dyn Storage, - label: String, - ) -> Result { - let msg = MixnetExecuteMsg::CreateFamilyOnBehalf { - owner_address: self.owner_address().into_string(), - label, - }; - - let msg = wasm_execute(MIXNET_CONTRACT_ADDRESS.load(storage)?, &msg, vec![])?; - - Ok(Response::new().add_message(msg)) - } - - fn try_join_family( - &self, - storage: &dyn Storage, - join_permit: MessageSignature, - family_head: FamilyHead, - ) -> Result { - let msg = MixnetExecuteMsg::JoinFamilyOnBehalf { - member_address: self.owner_address().to_string(), - join_permit, - family_head, - }; - - let msg = wasm_execute(MIXNET_CONTRACT_ADDRESS.load(storage)?, &msg, vec![])?; - - Ok(Response::new().add_message(msg)) - } - - fn try_leave_family( - &self, - storage: &dyn Storage, - family_head: FamilyHead, - ) -> Result { - let msg = MixnetExecuteMsg::LeaveFamilyOnBehalf { - member_address: self.owner_address().to_string(), - family_head, - }; - - let msg = wasm_execute(MIXNET_CONTRACT_ADDRESS.load(storage)?, &msg, vec![])?; - - Ok(Response::new().add_message(msg)) - } - - fn try_head_kick_member( - &self, - storage: &dyn Storage, - member: IdentityKeyRef<'_>, - ) -> Result { - let msg = MixnetExecuteMsg::KickFamilyMemberOnBehalf { - head_address: self.owner_address().to_string(), - member: member.to_string(), - }; - - let msg = wasm_execute(MIXNET_CONTRACT_ADDRESS.load(storage)?, &msg, vec![])?; - - Ok(Response::new().add_message(msg)) - } -} diff --git a/contracts/vesting/src/vesting/mod.rs b/contracts/vesting/src/vesting/mod.rs index d8da405008..33cfc2baec 100644 --- a/contracts/vesting/src/vesting/mod.rs +++ b/contracts/vesting/src/vesting/mod.rs @@ -26,7 +26,7 @@ mod tests { use contracts_common::signing::MessageSignature; use cosmwasm_std::testing::{mock_env, mock_info}; use cosmwasm_std::{coin, coins, Addr, Coin, Timestamp, Uint128}; - use mixnet_contract_common::mixnode::MixNodeCostParams; + use mixnet_contract_common::mixnode::NodeCostParams; use mixnet_contract_common::{Gateway, MixNode, Percent}; use vesting_contract_common::messages::ExecuteMsg; use vesting_contract_common::{Account, PledgeCap, VestingSpecification}; @@ -428,7 +428,7 @@ mod tests { version: "0.10.0".to_string(), }; - let cost_params = MixNodeCostParams { + let cost_params = NodeCostParams { profit_margin_percent: Percent::from_percentage_value(10).unwrap(), interval_operating_cost: Coin { denom: "NYM".to_string(), diff --git a/explorer-api/explorer-api-requests/src/lib.rs b/explorer-api/explorer-api-requests/src/lib.rs index 2109348899..3ea6ed7200 100644 --- a/explorer-api/explorer-api-requests/src/lib.rs +++ b/explorer-api/explorer-api-requests/src/lib.rs @@ -1,6 +1,6 @@ use nym_api_requests::models::NodePerformance; use nym_contracts_common::Percent; -use nym_mixnet_contract_common::{Addr, Coin, Gateway, Layer, MixId, MixNode}; +use nym_mixnet_contract_common::{Addr, Coin, Gateway, LegacyMixLayer, MixNode, NodeId}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -14,13 +14,13 @@ pub enum MixnodeStatus { #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] pub struct PrettyDetailedMixNodeBond { - pub mix_id: MixId, + pub mix_id: NodeId, pub location: Option, pub status: MixnodeStatus, pub pledge_amount: Coin, pub total_delegation: Coin, pub owner: Addr, - pub layer: Layer, + pub layer: LegacyMixLayer, pub mix_node: MixNode, pub stake_saturation: f32, pub uncapped_saturation: f32, diff --git a/explorer-api/src/gateways/models.rs b/explorer-api/src/gateways/models.rs index a3ed406067..c3a2cd9062 100644 --- a/explorer-api/src/gateways/models.rs +++ b/explorer-api/src/gateways/models.rs @@ -3,7 +3,8 @@ use crate::{cache::Cache, location::LocationCacheItem}; use nym_explorer_api_requests::{Location, PrettyDetailedGatewayBond}; -use nym_mixnet_contract_common::{GatewayBond, IdentityKey}; +use nym_mixnet_contract_common::IdentityKey; +use nym_validator_client::legacy::LegacyGatewayBondWithId; use serde::Serialize; use std::{sync::Arc, time::SystemTime}; use tokio::sync::RwLock; @@ -11,7 +12,7 @@ use tokio::sync::RwLock; use super::location::GatewayLocationCache; pub(crate) struct GatewayCache { - pub(crate) gateways: Cache, + pub(crate) gateways: Cache, } #[derive(Clone, Debug, Serialize, JsonSchema)] @@ -37,20 +38,20 @@ impl ThreadsafeGatewayCache { fn create_detailed_gateway( &self, - bond: GatewayBond, + bond: LegacyGatewayBondWithId, location: Option<&LocationCacheItem>, ) -> PrettyDetailedGatewayBond { PrettyDetailedGatewayBond { - pledge_amount: bond.pledge_amount, - owner: bond.owner, - block_height: bond.block_height, - gateway: bond.gateway, - proxy: bond.proxy, + pledge_amount: bond.bond.pledge_amount, + owner: bond.bond.owner, + block_height: bond.bond.block_height, + gateway: bond.bond.gateway, + proxy: bond.bond.proxy, location: location.and_then(|l| l.location.clone()), } } - pub(crate) async fn get_gateways(&self) -> Vec { + pub(crate) async fn get_gateways(&self) -> Vec { self.gateways.read().await.gateways.get_all() } @@ -106,7 +107,7 @@ impl ThreadsafeGatewayCache { .insert(identy_key, LocationCacheItem::new_from_location(location)); } - pub(crate) async fn update_cache(&self, gateways: Vec) { + pub(crate) async fn update_cache(&self, gateways: Vec) { let mut guard = self.gateways.write().await; for gateway in gateways { diff --git a/explorer-api/src/mix_node/delegations.rs b/explorer-api/src/mix_node/delegations.rs index feaefcf5c0..fc9420c88c 100644 --- a/explorer-api/src/mix_node/delegations.rs +++ b/explorer-api/src/mix_node/delegations.rs @@ -4,12 +4,12 @@ use super::models::SummedDelegations; use crate::client::ThreadsafeValidatorClient; use itertools::Itertools; -use nym_mixnet_contract_common::{Delegation, MixId}; +use nym_mixnet_contract_common::{Delegation, NodeId}; use nym_validator_client::nyxd::contract_traits::PagedMixnetQueryClient; pub(crate) async fn get_single_mixnode_delegations( client: &ThreadsafeValidatorClient, - mix_id: MixId, + mix_id: NodeId, ) -> Vec { match client .0 @@ -27,7 +27,7 @@ pub(crate) async fn get_single_mixnode_delegations( pub(crate) async fn get_single_mixnode_delegations_summed( client: &ThreadsafeValidatorClient, - mix_id: MixId, + mix_id: NodeId, ) -> Vec { let delegations_by_owner = get_single_mixnode_delegations(client, mix_id) .await diff --git a/explorer-api/src/mix_node/econ_stats.rs b/explorer-api/src/mix_node/econ_stats.rs index 0b1c901a40..31d5e78c93 100644 --- a/explorer-api/src/mix_node/econ_stats.rs +++ b/explorer-api/src/mix_node/econ_stats.rs @@ -5,12 +5,12 @@ use crate::client::ThreadsafeValidatorClient; use crate::helpers::best_effort_small_dec_to_f64; use crate::mix_node::models::EconomicDynamicsStats; use nym_contracts_common::truncate_decimal; -use nym_mixnet_contract_common::MixId; +use nym_mixnet_contract_common::NodeId; use nym_validator_client::client::NymApiClientExt; pub(crate) async fn retrieve_mixnode_econ_stats( client: &ThreadsafeValidatorClient, - mix_id: MixId, + mix_id: NodeId, ) -> Option { let stake_saturation = client .0 diff --git a/explorer-api/src/mix_node/http.rs b/explorer-api/src/mix_node/http.rs index e00ed8ff82..2a44097275 100644 --- a/explorer-api/src/mix_node/http.rs +++ b/explorer-api/src/mix_node/http.rs @@ -10,7 +10,7 @@ use crate::mix_node::models::{ }; use crate::state::ExplorerApiStateContext; use nym_explorer_api_requests::PrettyDetailedMixNodeBond; -use nym_mixnet_contract_common::{Delegation, MixId}; +use nym_mixnet_contract_common::{Delegation, NodeId}; use reqwest::Error as ReqwestError; use rocket::response::status::NotFound; use rocket::serde::json::Json; @@ -47,7 +47,7 @@ async fn get_mix_node_stats(host: &str, port: u16) -> Result")] pub(crate) async fn get_by_id( - mix_id: MixId, + mix_id: NodeId, state: &State, ) -> Result, NotFound> { match state.inner.mixnodes.get_detailed_mixnode(mix_id).await { @@ -59,7 +59,7 @@ pub(crate) async fn get_by_id( #[openapi(tag = "mix_node")] #[get("//delegations")] pub(crate) async fn get_delegations( - mix_id: MixId, + mix_id: NodeId, state: &State, ) -> Json> { Json(get_single_mixnode_delegations(&state.inner.validator_client, mix_id).await) @@ -68,7 +68,7 @@ pub(crate) async fn get_delegations( #[openapi(tag = "mix_node")] #[get("//delegations/summed")] pub(crate) async fn get_delegations_summed( - mix_id: MixId, + mix_id: NodeId, state: &State, ) -> Json> { Json(get_single_mixnode_delegations_summed(&state.inner.validator_client, mix_id).await) @@ -77,7 +77,7 @@ pub(crate) async fn get_delegations_summed( #[openapi(tag = "mix_node")] #[get("//description")] pub(crate) async fn get_description( - mix_id: MixId, + mix_id: NodeId, state: &State, ) -> Option> { match state.inner.mixnode.clone().get_description(mix_id).await { @@ -125,7 +125,7 @@ pub(crate) async fn get_description( #[openapi(tag = "mix_node")] #[get("//stats")] pub(crate) async fn get_stats( - mix_id: MixId, + mix_id: NodeId, state: &State, ) -> Option> { match state.inner.mixnode.get_node_stats(mix_id).await { @@ -170,7 +170,7 @@ pub(crate) async fn get_stats( #[openapi(tag = "mix_node")] #[get("//economic-dynamics-stats")] pub(crate) async fn get_economic_dynamics_stats( - mix_id: MixId, + mix_id: NodeId, state: &State, ) -> Option> { match state.inner.mixnode.get_econ_stats(mix_id).await { diff --git a/explorer-api/src/mix_node/models.rs b/explorer-api/src/mix_node/models.rs index 4adfa6dd86..d7b79ad71a 100644 --- a/explorer-api/src/mix_node/models.rs +++ b/explorer-api/src/mix_node/models.rs @@ -3,7 +3,7 @@ use crate::cache::Cache; use nym_mixnet_contract_common::Delegation; -use nym_mixnet_contract_common::{Addr, Coin, MixId}; +use nym_mixnet_contract_common::{Addr, Coin, NodeId}; use nym_validator_client::models::SelectionChance; use serde::Deserialize; use serde::Serialize; @@ -14,7 +14,7 @@ use tokio::sync::RwLock; #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)] pub struct SummedDelegations { pub owner: Addr, - pub mix_id: MixId, + pub mix_id: NodeId, pub amount: Coin, } @@ -40,9 +40,9 @@ impl SummedDelegations { } pub(crate) struct MixNodeCache { - pub(crate) descriptions: Cache, - pub(crate) node_stats: Cache, - pub(crate) econ_stats: Cache, + pub(crate) descriptions: Cache, + pub(crate) node_stats: Cache, + pub(crate) econ_stats: Cache, } #[derive(Clone)] @@ -61,19 +61,19 @@ impl ThreadsafeMixNodeCache { } } - pub(crate) async fn get_description(&self, mix_id: MixId) -> Option { + pub(crate) async fn get_description(&self, mix_id: NodeId) -> Option { self.inner.read().await.descriptions.get(&mix_id) } - pub(crate) async fn get_node_stats(&self, mix_id: MixId) -> Option { + pub(crate) async fn get_node_stats(&self, mix_id: NodeId) -> Option { self.inner.read().await.node_stats.get(&mix_id) } - pub(crate) async fn get_econ_stats(&self, mix_id: MixId) -> Option { + pub(crate) async fn get_econ_stats(&self, mix_id: NodeId) -> Option { self.inner.read().await.econ_stats.get(&mix_id) } - pub(crate) async fn set_description(&self, mix_id: MixId, description: NodeDescription) { + pub(crate) async fn set_description(&self, mix_id: NodeId, description: NodeDescription) { self.inner .write() .await @@ -81,11 +81,11 @@ impl ThreadsafeMixNodeCache { .set(mix_id, description); } - pub(crate) async fn set_node_stats(&self, mix_id: MixId, node_stats: NodeStats) { + pub(crate) async fn set_node_stats(&self, mix_id: NodeId, node_stats: NodeStats) { self.inner.write().await.node_stats.set(mix_id, node_stats); } - pub(crate) async fn set_econ_stats(&self, mix_id: MixId, econ_stats: EconomicDynamicsStats) { + pub(crate) async fn set_econ_stats(&self, mix_id: NodeId, econ_stats: EconomicDynamicsStats) { self.inner.write().await.econ_stats.set(mix_id, econ_stats); } } @@ -147,11 +147,11 @@ fn get_common_owner(delegations: &[Delegation]) -> Option { Some(owner) } -fn get_common_mix_id(delegations: &[Delegation]) -> Option { - let mix_id = delegations.iter().next()?.mix_id; +fn get_common_mix_id(delegations: &[Delegation]) -> Option { + let mix_id = delegations.iter().next()?.node_id; if delegations .iter() - .any(|delegation| delegation.mix_id != mix_id) + .any(|delegation| delegation.node_id != mix_id) { log::warn!("Unexpected different node identities when summing delegations"); return None; diff --git a/explorer-api/src/mix_nodes/location.rs b/explorer-api/src/mix_nodes/location.rs index de3c2e2fd6..4fdd1714d4 100644 --- a/explorer-api/src/mix_nodes/location.rs +++ b/explorer-api/src/mix_nodes/location.rs @@ -1,8 +1,8 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use nym_mixnet_contract_common::MixId; +use nym_mixnet_contract_common::NodeId; use crate::location::LocationCache; -pub(crate) type MixnodeLocationCache = LocationCache; +pub(crate) type MixnodeLocationCache = LocationCache; diff --git a/explorer-api/src/mix_nodes/models.rs b/explorer-api/src/mix_nodes/models.rs index fe5ae7fde8..4ffd99304f 100644 --- a/explorer-api/src/mix_nodes/models.rs +++ b/explorer-api/src/mix_nodes/models.rs @@ -1,24 +1,20 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use super::location::MixnodeLocationCache; +use crate::helpers::best_effort_small_dec_to_f64; +use crate::location::LocationCacheItem; +use crate::mix_nodes::CACHE_ENTRY_TTL; +use nym_explorer_api_requests::{Location, MixnodeStatus, PrettyDetailedMixNodeBond}; +use nym_mixnet_contract_common::rewarding::helpers::truncate_reward; +use nym_mixnet_contract_common::NodeId; +use nym_validator_client::models::MixNodeBondAnnotated; +use serde::Serialize; use std::collections::{HashMap, HashSet}; use std::sync::Arc; use std::time::{Duration, SystemTime}; - -use nym_explorer_api_requests::{Location, MixnodeStatus, PrettyDetailedMixNodeBond}; -use nym_mixnet_contract_common::rewarding::helpers::truncate_reward; -use nym_mixnet_contract_common::MixId; -use serde::Serialize; use tokio::sync::{RwLock, RwLockReadGuard}; -use crate::helpers::best_effort_small_dec_to_f64; -use crate::location::LocationCacheItem; -use nym_validator_client::models::MixNodeBondAnnotated; - -use super::location::MixnodeLocationCache; -use super::utils::family_numerical_id; -use crate::mix_nodes::CACHE_ENTRY_TTL; - #[derive(Clone, Debug, Serialize, JsonSchema)] pub(crate) struct MixNodeActiveSetSummary { pub active: usize, @@ -35,9 +31,9 @@ pub(crate) struct MixNodeSummary { #[derive(Clone, Debug)] pub(crate) struct MixNodesResult { pub(crate) valid_until: SystemTime, - pub(crate) all_mixnodes: HashMap, - active_mixnodes: HashSet, - rewarded_mixnodes: HashSet, + pub(crate) all_mixnodes: HashMap, + active_mixnodes: HashSet, + rewarded_mixnodes: HashSet, } impl MixNodesResult { @@ -50,7 +46,7 @@ impl MixNodesResult { } } - fn determine_node_status(&self, mix_id: MixId) -> MixnodeStatus { + fn determine_node_status(&self, mix_id: NodeId) -> MixnodeStatus { if self.active_mixnodes.contains(&mix_id) { MixnodeStatus::Active } else if self.rewarded_mixnodes.contains(&mix_id) { @@ -64,7 +60,7 @@ impl MixNodesResult { self.valid_until >= SystemTime::now() } - fn get_mixnode(&self, mix_id: MixId) -> Option { + fn get_mixnode(&self, mix_id: NodeId) -> Option { if self.is_valid() { self.all_mixnodes.get(&mix_id).cloned() } else { @@ -72,7 +68,7 @@ impl MixNodesResult { } } - fn get_mixnodes(&self) -> Option> { + fn get_mixnodes(&self) -> Option> { if self.is_valid() { Some(self.all_mixnodes.clone()) } else { @@ -102,7 +98,7 @@ impl ThreadsafeMixNodesCache { } } - pub(crate) async fn is_location_valid(&self, mix_id: MixId) -> bool { + pub(crate) async fn is_location_valid(&self, mix_id: NodeId) -> bool { self.locations .read() .await @@ -116,7 +112,7 @@ impl ThreadsafeMixNodesCache { self.locations.read().await.clone() } - pub(crate) async fn set_location(&self, mix_id: MixId, location: Option) { + pub(crate) async fn set_location(&self, mix_id: NodeId, location: Option) { // cache the location for this mix node so that it can be used when the mix node list is refreshed self.locations .write() @@ -124,25 +120,23 @@ impl ThreadsafeMixNodesCache { .insert(mix_id, LocationCacheItem::new_from_location(location)); } - pub(crate) async fn get_mixnode(&self, mix_id: MixId) -> Option { + pub(crate) async fn get_mixnode(&self, mix_id: NodeId) -> Option { self.mixnodes.read().await.get_mixnode(mix_id) } - pub(crate) async fn get_mixnodes(&self) -> Option> { + pub(crate) async fn get_mixnodes(&self) -> Option> { self.mixnodes.read().await.get_mixnodes() } fn create_detailed_mixnode( &self, - mix_id: MixId, + mix_id: NodeId, mixnodes_guard: &RwLockReadGuard<'_, MixNodesResult>, location: Option<&LocationCacheItem>, node: &MixNodeBondAnnotated, ) -> PrettyDetailedMixNodeBond { - let denom = &node.mixnode_details.original_pledge().denom; let rewarding_info = &node.mixnode_details.rewarding_details; - - let family_id = node.family.as_ref().map(family_numerical_id); + let denom = &rewarding_info.cost_params.interval_operating_cost.denom; PrettyDetailedMixNodeBond { mix_id, @@ -162,14 +156,14 @@ impl ThreadsafeMixNodesCache { estimated_delegators_apy: best_effort_small_dec_to_f64(node.estimated_delegators_apy), operating_cost: rewarding_info.cost_params.interval_operating_cost.clone(), profit_margin_percent: rewarding_info.cost_params.profit_margin_percent, - family_id, + family_id: None, blacklisted: node.blacklisted, } } pub(crate) async fn get_detailed_mixnode( &self, - mix_id: MixId, + mix_id: NodeId, ) -> Option { let mixnodes_guard = self.mixnodes.read().await; let location_guard = self.locations.read().await; @@ -197,8 +191,8 @@ impl ThreadsafeMixNodesCache { pub(crate) async fn update_cache( &self, all_bonds: Vec, - rewarded_nodes: HashSet, - active_nodes: HashSet, + rewarded_nodes: HashSet, + active_nodes: HashSet, ) { let mut guard = self.mixnodes.write().await; guard.all_mixnodes = all_bonds diff --git a/explorer-api/src/mix_nodes/utils.rs b/explorer-api/src/mix_nodes/utils.rs index 5273d66f55..2742fd2415 100644 --- a/explorer-api/src/mix_nodes/utils.rs +++ b/explorer-api/src/mix_nodes/utils.rs @@ -1,13 +1,8 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use isocountry::CountryCode; -use nym_mixnet_contract_common::families::FamilyHead; -use rand::Rng; -use rand_pcg::Pcg64; -use rand_seeder::Seeder; - use crate::location::GeoLocation; +use isocountry::CountryCode; #[allow(dead_code)] pub(crate) fn map_2_letter_to_3_letter_country_code(geo: &GeoLocation) -> String { @@ -22,10 +17,3 @@ pub(crate) fn map_2_letter_to_3_letter_country_code(geo: &GeoLocation) -> String } } } - -// We don't need numerical IDs anywhere, so to avoid modifying the contract storage again and -// since this is for explorer ergonomics, it will generate a deterministic random u16 based on the family Identity. -pub(crate) fn family_numerical_id(fh: &FamilyHead) -> u16 { - let mut rng: Pcg64 = Seeder::from(fh.identity()).make_rng(); - rng.gen() -} diff --git a/explorer-api/src/ping/http.rs b/explorer-api/src/ping/http.rs index c3cd4107f1..952e681340 100644 --- a/explorer-api/src/ping/http.rs +++ b/explorer-api/src/ping/http.rs @@ -1,7 +1,7 @@ // Copyright 2021-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use nym_mixnet_contract_common::{MixId, MixNode}; +use nym_mixnet_contract_common::{NodeId, MixNode}; use rocket::serde::json::Json; use rocket::{Route, State}; use rocket_okapi::okapi::openapi3::OpenApi; @@ -23,7 +23,7 @@ pub fn ping_make_default_routes(settings: &OpenApiSettings) -> (Vec, Open #[openapi(tag = "ping")] #[get("/")] pub(crate) async fn index( - mix_id: MixId, + mix_id: NodeId, state: &State, ) -> Option> { match state.inner.ping.clone().get(mix_id).await { diff --git a/explorer-api/src/ping/models.rs b/explorer-api/src/ping/models.rs index 9aebf7d5fd..10d6d832ce 100644 --- a/explorer-api/src/ping/models.rs +++ b/explorer-api/src/ping/models.rs @@ -2,12 +2,12 @@ use std::collections::HashMap; use std::sync::Arc; use std::time::{Duration, SystemTime}; -use nym_mixnet_contract_common::MixId; +use nym_mixnet_contract_common::NodeId; use serde::Deserialize; use serde::Serialize; use tokio::sync::RwLock; -pub(crate) type PingCache = HashMap; +pub(crate) type PingCache = HashMap; const PING_TTL: Duration = Duration::from_secs(60 * 5); // 5 mins, before port check will be re-tried (only while pending) const CACHE_TTL: Duration = Duration::from_secs(60 * 60); // 1 hour, to cache result from port check @@ -24,7 +24,7 @@ impl ThreadsafePingCache { } } - pub(crate) async fn get(&self, mix_id: MixId) -> Option { + pub(crate) async fn get(&self, mix_id: NodeId) -> Option { self.inner .read() .await @@ -44,7 +44,7 @@ impl ThreadsafePingCache { }) } - pub(crate) async fn set_pending(&self, mix_id: MixId) { + pub(crate) async fn set_pending(&self, mix_id: NodeId) { self.inner.write().await.insert( mix_id, PingCacheItem { @@ -55,7 +55,7 @@ impl ThreadsafePingCache { ); } - pub(crate) async fn set(&self, mix_id: MixId, item: PingResponse) { + pub(crate) async fn set(&self, mix_id: NodeId, item: PingResponse) { self.inner.write().await.insert( mix_id, PingCacheItem { diff --git a/explorer-api/src/state.rs b/explorer-api/src/state.rs index 8ecee92ae2..b007fae76d 100644 --- a/explorer-api/src/state.rs +++ b/explorer-api/src/state.rs @@ -3,7 +3,7 @@ use std::path::Path; use chrono::{DateTime, Utc}; use log::info; -use nym_mixnet_contract_common::MixId; +use nym_mixnet_contract_common::NodeId; use serde::{Deserialize, Serialize}; use crate::client::ThreadsafeValidatorClient; @@ -39,7 +39,7 @@ pub struct ExplorerApiState { } impl ExplorerApiState { - pub(crate) async fn get_mix_node(&self, mix_id: MixId) -> Option { + pub(crate) async fn get_mix_node(&self, mix_id: NodeId) -> Option { self.mixnodes.get_mixnode(mix_id).await } } diff --git a/explorer-api/src/tasks.rs b/explorer-api/src/tasks.rs index b0b9a9701b..52c0192538 100644 --- a/explorer-api/src/tasks.rs +++ b/explorer-api/src/tasks.rs @@ -1,8 +1,8 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use nym_mixnet_contract_common::GatewayBond; use nym_task::TaskClient; +use nym_validator_client::legacy::LegacyGatewayBondWithId; use nym_validator_client::models::MixNodeBondAnnotated; use nym_validator_client::nyxd::error::NyxdError; use nym_validator_client::nyxd::{Paging, TendermintRpcClient, ValidatorResponse}; @@ -28,13 +28,12 @@ impl ExplorerApiTasks { F: FnOnce(&'a QueryHttpRpcValidatorClient) -> Fut, Fut: Future, ValidatorClientError>>, { - let bonds = match f(&self.state.inner.validator_client.0).await { - Ok(result) => result, - Err(err) => { + let bonds = f(&self.state.inner.validator_client.0) + .await + .unwrap_or_else(|err| { error!("Unable to retrieve mixnode bonds: {err}"); vec![] - } - }; + }); info!("Fetched {} mixnode bonds", bonds.len()); bonds @@ -48,7 +47,9 @@ impl ExplorerApiTasks { .await } - async fn retrieve_all_gateways(&self) -> Result, ValidatorClientError> { + async fn retrieve_all_gateways( + &self, + ) -> Result, ValidatorClientError> { info!("About to retrieve all gateways..."); self.state .inner diff --git a/nym-api/Cargo.toml b/nym-api/Cargo.toml index 194be8cc2d..48521ecf46 100644 --- a/nym-api/Cargo.toml +++ b/nym-api/Cargo.toml @@ -5,13 +5,9 @@ name = "nym-api" license = "GPL-3.0" version = "1.1.44" -authors = [ - "Dave Hrycyszyn ", - "Jędrzej Stuczyński ", - "Drazen Urch ", -] +authors.workspace = true edition = "2021" -rust-version = "1.76.0" +rust-version.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -31,16 +27,12 @@ humantime-serde = { workspace = true } k256 = { workspace = true, features = [ "ecdsa-core", ] } # needed for the Verifier trait; pull whatever version is used by other dependencies -log = { workspace = true } pin-project = { workspace = true } rand = { workspace = true } rand_chacha = { workspace = true } reqwest = { workspace = true, features = ["json"] } -rocket = { workspace = true, features = ["json"] } -rocket_cors = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } -tap = { workspace = true } thiserror = { workspace = true } time = { workspace = true, features = ["serde-human-readable", "parsing"] } tokio = { workspace = true, features = [ @@ -66,20 +58,17 @@ sqlx = { workspace = true, features = [ "time", ] } -okapi = { workspace = true, features = ["impl_json_schema"] } -rocket_okapi = { workspace = true, features = ["swagger"] } schemars = { workspace = true, features = ["preserve_order"] } zeroize = { workspace = true } # for axum server -axum = { workspace = true, features = ["tokio"], optional = true } -axum-extra = { workspace = true, features = ["typed-header"], optional = true } -tower-http = { workspace = true, features = ["cors", "trace"], optional = true } -utoipa = { workspace = true, features = ["axum_extras", "time"], optional = true } -utoipa-swagger-ui = { workspace = true, features = ["axum"], optional = true} -utoipauto = { workspace = true, optional = true } -tracing-subscriber = { workspace = true, features = ["env-filter"], optional = true } -tracing = { workspace = true, optional = true } +axum = { workspace = true, features = ["tokio"] } +axum-extra = { workspace = true, features = ["typed-header"] } +tower-http = { workspace = true, features = ["cors", "trace"] } +utoipa = { workspace = true, features = ["axum_extras", "time"] } +utoipauto = { workspace = true } +utoipa-swagger-ui = { workspace = true, features = ["axum"] } +tracing = { workspace = true } ## ephemera-specific #actix-web = "4" @@ -112,7 +101,7 @@ cw4 = { workspace = true } nym-dkg = { path = "../common/dkg", features = ["cw-types"] } nym-gateway-client = { path = "../common/client-libs/gateway-client" } nym-inclusion-probability = { path = "../common/inclusion-probability" } -nym-mixnet-contract-common = { path = "../common/cosmwasm-smart-contracts/mixnet-contract", features = ["utoipa"]} +nym-mixnet-contract-common = { path = "../common/cosmwasm-smart-contracts/mixnet-contract", features = ["utoipa"] } nym-vesting-contract-common = { path = "../common/cosmwasm-smart-contracts/vesting-contract" } nym-contracts-common = { path = "../common/cosmwasm-smart-contracts/contracts-common" } nym-multisig-contract-common = { path = "../common/cosmwasm-smart-contracts/multisig-contract" } @@ -121,29 +110,19 @@ nym-sphinx = { path = "../common/nymsphinx" } nym-pemstore = { path = "../common/pemstore" } nym-task = { path = "../common/task" } nym-topology = { path = "../common/topology" } -nym-api-requests = { path = "nym-api-requests", features = ["rocket-traits"] } +nym-api-requests = { path = "nym-api-requests" } nym-validator-client = { path = "../common/client-libs/validator-client" } -nym-bin-common = { path = "../common/bin-common", features = ["output_format", "openapi"] } +nym-bin-common = { path = "../common/bin-common", features = ["output_format", "openapi", "basic_tracing"] } nym-node-tester-utils = { path = "../common/node-tester-utils" } nym-node-requests = { path = "../nym-node/nym-node-requests" } nym-types = { path = "../common/types" } nym-http-api-common = { path = "../common/http-api-common", features = ["utoipa"] } +nym-serde-helpers = { path = "../common/serde-helpers", features = ["date"] } [features] no-reward = [] v2-performance = [] generate-ts = ["ts-rs"] -axum = ["dep:axum", - "axum-extra", - "tower-http", - "utoipa", - "utoipauto", - "tracing-subscriber", - "tracing", - "utoipa-swagger-ui", - "nym-http-api-common/utoipa", - "nym-mixnet-contract-common/utoipa" -] [build-dependencies] tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } diff --git a/nym-api/Rocket.toml b/nym-api/Rocket.toml deleted file mode 100644 index a094d2157a..0000000000 --- a/nym-api/Rocket.toml +++ /dev/null @@ -1,7 +0,0 @@ -[default] -limits = { forms = "64 kB", json = "1 MiB" } -port = 8080 -address = "127.0.0.1" - -[release] -address = "0.0.0.0" \ No newline at end of file diff --git a/nym-api/build.rs b/nym-api/build.rs index cdd97f9505..d6e0c45770 100644 --- a/nym-api/build.rs +++ b/nym-api/build.rs @@ -1,4 +1,4 @@ -use sqlx::{Connection, SqliteConnection}; +use sqlx::{Connection, FromRow, SqliteConnection}; use std::env; #[tokio::main] @@ -15,6 +15,42 @@ async fn main() { .await .expect("Failed to perform SQLx migrations"); + #[derive(FromRow)] + struct Exists { + exists: bool, + } + + // check if it was already run + let res: Exists = sqlx::query_as("SELECT EXISTS (SELECT 1 FROM v3_migration_info) AS 'exists'") + .fetch_one(&mut conn) + .await + .unwrap(); + + let already_run = res.exists; + + // execute the manual v3 migration + // it's performed on an empty storage, so we don't need to actually make any network queries + if !already_run { + sqlx::query( + r#" + CREATE TABLE gateway_details_temp + ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + node_id INTEGER NOT NULL UNIQUE, + identity VARCHAR NOT NULL UNIQUE + ); + + DROP TABLE gateway_details; + ALTER TABLE gateway_details_temp RENAME TO gateway_details; + + INSERT INTO v3_migration_info(id) VALUES (0); + "#, + ) + .execute(&mut conn) + .await + .expect("failed to update post v3 migration tables"); + } + #[cfg(target_family = "unix")] println!("cargo:rustc-env=DATABASE_URL=sqlite://{}", &database_path); diff --git a/nym-api/migrations/20240726120000_v3_changes.sql b/nym-api/migrations/20240726120000_v3_changes.sql new file mode 100644 index 0000000000..652729d215 --- /dev/null +++ b/nym-api/migrations/20240726120000_v3_changes.sql @@ -0,0 +1,24 @@ +/* + * Copyright 2024 - Nym Technologies SA + * SPDX-License-Identifier: GPL-3.0-only + */ + +-- we don't have to be keeping track of the gateway owner; it will make things easier in code +ALTER TABLE gateway_details DROP COLUMN owner; +ALTER TABLE mixnode_details DROP COLUMN owner; + +-- NOTE: this column is made `NOT NULL UNIQUE` in code during `migrate_v3_database` call! +ALTER TABLE gateway_details ADD node_id INTEGER; + +-- a hacky table-flag to indicate whether the v3 migration has been run +CREATE TABLE v3_migration_info ( + id INTEGER PRIMARY KEY CHECK (id = 0) +); + +--CREATE TABLE node_historical_performance ( +-- contract_node_id INTEGER NOT NULL, +-- date DATE NOT NULL, +-- performance FLOAT NOT NULL, +-- +-- UNIQUE(contract_node_id, date); +--) \ No newline at end of file diff --git a/nym-api/nym-api-requests/Cargo.toml b/nym-api/nym-api-requests/Cargo.toml index 70943d6fba..f2873b9173 100644 --- a/nym-api/nym-api-requests/Cargo.toml +++ b/nym-api/nym-api-requests/Cargo.toml @@ -11,9 +11,9 @@ bs58 = { workspace = true } cosmrs = { workspace = true } cosmwasm-std = { workspace = true } getset = { workspace = true } -rocket = { workspace = true, optional = true } schemars = { workspace = true, features = ["preserve_order"] } serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } sha2.workspace = true tendermint = { workspace = true } thiserror.workspace = true @@ -32,12 +32,9 @@ nym-ecash-time = { path = "../../common/ecash-time" } nym-compact-ecash = { path = "../../common/nym_offline_compact_ecash" } nym-mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract" } nym-node-requests = { path = "../../nym-node/nym-node-requests", default-features = false, features = ["openapi"] } +nym-network-defaults = { path = "../../common/network-defaults" } -[dev-dependencies] -serde_json.workspace = true - [features] default = [] -rocket-traits = ["rocket"] generate-ts = ["ts-rs", "nym-mixnet-contract-common/generate-ts"] diff --git a/nym-api/nym-api-requests/src/legacy.rs b/nym-api/nym-api-requests/src/legacy.rs new file mode 100644 index 0000000000..f7af56252c --- /dev/null +++ b/nym-api/nym-api-requests/src/legacy.rs @@ -0,0 +1,92 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use cosmwasm_std::Decimal; +use nym_mixnet_contract_common::mixnode::PendingMixNodeChanges; +use nym_mixnet_contract_common::{ + GatewayBond, LegacyMixLayer, MixNodeBond, MixNodeDetails, NodeId, NodeRewarding, +}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use std::ops::Deref; +use utoipa::ToSchema; + +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)] +pub struct LegacyGatewayBondWithId { + // we need to flatten it so that consumers of endpoints that returned `GatewayBond` wouldn't break + #[serde(flatten)] + pub bond: GatewayBond, + pub node_id: NodeId, +} + +impl Deref for LegacyGatewayBondWithId { + type Target = GatewayBond; + fn deref(&self) -> &Self::Target { + &self.bond + } +} + +impl From for GatewayBond { + fn from(value: LegacyGatewayBondWithId) -> Self { + value.bond + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)] +pub struct LegacyMixNodeBondWithLayer { + // we need to flatten it so that consumers of endpoints that returned `MixNodeBond` wouldn't break + #[serde(flatten)] + pub bond: MixNodeBond, + + pub layer: LegacyMixLayer, +} + +impl Deref for LegacyMixNodeBondWithLayer { + type Target = MixNodeBond; + fn deref(&self) -> &Self::Target { + &self.bond + } +} + +impl From for MixNodeBond { + fn from(value: LegacyMixNodeBondWithLayer) -> Self { + value.bond + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +pub struct LegacyMixNodeDetailsWithLayer { + /// Basic bond information of this mixnode, such as owner address, original pledge, etc. + pub bond_information: LegacyMixNodeBondWithLayer, + + /// Details used for computation of rewarding related data. + pub rewarding_details: NodeRewarding, + + /// Adjustments to the mixnode that are ought to happen during future epoch transitions. + #[serde(default)] + pub pending_changes: PendingMixNodeChanges, +} + +impl LegacyMixNodeDetailsWithLayer { + pub fn mix_id(&self) -> NodeId { + self.bond_information.mix_id + } + + pub fn total_stake(&self) -> Decimal { + self.rewarding_details.node_bond() + } + + pub fn is_unbonding(&self) -> bool { + self.bond_information.is_unbonding + } +} + +impl From for MixNodeDetails { + fn from(value: LegacyMixNodeDetailsWithLayer) -> Self { + MixNodeDetails { + bond_information: value.bond_information.into(), + rewarding_details: value.rewarding_details, + pending_changes: value.pending_changes, + } + } +} diff --git a/nym-api/nym-api-requests/src/lib.rs b/nym-api/nym-api-requests/src/lib.rs index 7ea5db1f6d..664eb59fff 100644 --- a/nym-api/nym-api-requests/src/lib.rs +++ b/nym-api/nym-api-requests/src/lib.rs @@ -7,6 +7,7 @@ use serde::{Deserialize, Serialize}; pub mod constants; pub mod ecash; mod helpers; +pub mod legacy; pub mod models; pub mod nym_nodes; pub mod pagination; diff --git a/nym-api/nym-api-requests/src/models.rs b/nym-api/nym-api-requests/src/models.rs index f42c8cb764..072a2aa549 100644 --- a/nym-api/nym-api-requests/src/models.rs +++ b/nym-api/nym-api-requests/src/models.rs @@ -2,17 +2,22 @@ // SPDX-License-Identifier: Apache-2.0 use crate::helpers::unix_epoch; -use crate::nym_nodes::NodeRole; +use crate::legacy::{ + LegacyGatewayBondWithId, LegacyMixNodeBondWithLayer, LegacyMixNodeDetailsWithLayer, +}; +use crate::nym_nodes::{BasicEntryInformation, NodeRole, SkimmedNode}; use crate::pagination::PaginatedResponse; use cosmwasm_std::{Addr, Coin, Decimal, Uint128}; -use nym_mixnet_contract_common::families::FamilyHead; -use nym_mixnet_contract_common::mixnode::MixNodeDetails; use nym_mixnet_contract_common::reward_params::{Performance, RewardingParams}; use nym_mixnet_contract_common::rewarding::RewardEstimate; -use nym_mixnet_contract_common::{ - GatewayBond, IdentityKey, Interval, MixId, MixNode, MixNodeBond, Percent, RewardedSetNodeStatus, +use nym_mixnet_contract_common::{IdentityKey, Interval, MixNode, NodeId, Percent}; +use nym_network_defaults::{DEFAULT_MIX_LISTENING_PORT, DEFAULT_VERLOC_LISTENING_PORT}; +use nym_node_requests::api::v1::authenticator::models::Authenticator; +use nym_node_requests::api::v1::gateway::models::Wireguard; +use nym_node_requests::api::v1::ip_packet_router::models::IpPacketRouter; +use nym_node_requests::api::v1::node::models::{ + AuxiliaryDetails, BinaryBuildInformationOwned, NodeRoles, }; -use nym_node_requests::api::v1::node::models::{AuxiliaryDetails, BinaryBuildInformationOwned}; use schemars::gen::SchemaGenerator; use schemars::schema::{InstanceType, Schema, SchemaObject}; use schemars::JsonSchema; @@ -21,7 +26,7 @@ use std::fmt::{Debug, Display, Formatter}; use std::net::IpAddr; use std::ops::{Deref, DerefMut}; use std::{fmt, time::Duration}; -use time::OffsetDateTime; +use time::{Date, OffsetDateTime}; use utoipa::{IntoParams, ToResponse, ToSchema}; #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] @@ -66,18 +71,6 @@ pub enum MixnodeStatus { Inactive, // in neither the rewarded set nor the active set, but is bonded NotFound, // doesn't even exist in the bonded set } - -impl From for Option { - fn from(status: MixnodeStatus) -> Self { - match status { - MixnodeStatus::Active => Some(RewardedSetNodeStatus::Active), - MixnodeStatus::Standby => Some(RewardedSetNodeStatus::Standby), - MixnodeStatus::Inactive => None, - MixnodeStatus::NotFound => None, - } - } -} - impl MixnodeStatus { pub fn is_active(&self) -> bool { *self == MixnodeStatus::Active @@ -91,7 +84,7 @@ impl MixnodeStatus { ts(export_to = "ts-packages/types/src/types/rust/MixnodeCoreStatusResponse.ts") )] pub struct MixnodeCoreStatusResponse { - pub mix_id: MixId, + pub mix_id: NodeId, pub count: i32, } @@ -126,23 +119,58 @@ pub struct NodePerformance { pub last_24h: Performance, } -#[derive(ToSchema)] -#[schema(title = "MixNodeDetails")] -pub struct MixNodeDetailsSchema { +// imo for now there's no point in exposing more than that, +// nym-api shouldn't be calculating apy or stake saturation for you. +// it should just return its own metrics (performance) and then you can do with it as you wish +#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)] +pub struct NodeAnnotation { + pub last_24h_performance: Performance, +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] +pub struct AnnotationResponse { + #[schema(value_type = u32)] + pub node_id: NodeId, + pub annotation: Option, +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] +pub struct NodePerformanceResponse { + #[schema(value_type = u32)] + pub node_id: NodeId, + pub performance: Option, +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] +pub struct NodeDatePerformanceResponse { + #[schema(value_type = u32)] + pub node_id: NodeId, + #[schema(value_type = String, example = "1970-01-01")] + #[schemars(with = "String")] + pub date: Date, + pub performance: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] +#[schema(title = "LegacyMixNodeDetailsWithLayer")] +pub struct LegacyMixNodeDetailsWithLayerSchema { /// Basic bond information of this mixnode, such as owner address, original pledge, etc. + #[schema(example = "unimplemented schema")] pub bond_information: String, /// Details used for computation of rewarding related data. + #[schema(example = "unimplemented schema")] pub rewarding_details: String, /// Adjustments to the mixnode that are ought to happen during future epoch transitions. + #[schema(example = "unimplemented schema")] pub pending_changes: String, } #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] pub struct MixNodeBondAnnotated { - #[schema(value_type = MixNodeDetailsSchema)] - pub mixnode_details: MixNodeDetails, + #[schema(value_type = LegacyMixNodeDetailsWithLayerSchema)] + pub mixnode_details: LegacyMixNodeDetailsWithLayer, #[schema(value_type = String)] pub stake_saturation: StakeSaturation, #[schema(value_type = String)] @@ -153,7 +181,6 @@ pub struct MixNodeBondAnnotated { pub node_performance: NodePerformance, pub estimated_operator_apy: Decimal, pub estimated_delegators_apy: Decimal, - pub family: Option, pub blacklisted: bool, // a rather temporary thing until we query self-described endpoints of mixnodes @@ -166,7 +193,7 @@ impl MixNodeBondAnnotated { &self.mixnode_details.bond_information.mix_node } - pub fn mix_id(&self) -> MixId { + pub fn mix_id(&self) -> NodeId { self.mixnode_details.mix_id() } @@ -177,11 +204,15 @@ impl MixNodeBondAnnotated { pub fn owner(&self) -> &Addr { self.mixnode_details.bond_information.owner() } + + pub fn version(&self) -> &str { + &self.mixnode_details.bond_information.mix_node.version + } } #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] pub struct GatewayBondAnnotated { - pub gateway_bond: GatewayBond, + pub gateway_bond: LegacyGatewayBondWithId, #[serde(default)] pub self_described: Option, @@ -196,12 +227,16 @@ pub struct GatewayBondAnnotated { } impl GatewayBondAnnotated { + pub fn version(&self) -> &str { + &self.gateway_bond.gateway.version + } + pub fn identity(&self) -> &String { - self.gateway_bond.identity() + self.gateway_bond.bond.identity() } pub fn owner(&self) -> &Addr { - self.gateway_bond.owner() + self.gateway_bond.bond.owner() } } @@ -240,7 +275,7 @@ pub struct RewardEstimationResponse { #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] pub struct UptimeResponse { #[schema(value_type = u32)] - pub mix_id: MixId, + pub mix_id: NodeId, // The same as node_performance.last_24h. Legacy pub avg_uptime: u8, pub node_performance: NodePerformance, @@ -349,7 +384,7 @@ pub struct AllInclusionProbabilitiesResponse { #[derive(Clone, Serialize, schemars::JsonSchema, ToSchema)] pub struct InclusionProbability { #[schema(value_type = u32)] - pub mix_id: MixId, + pub mix_id: NodeId, pub in_active: f64, pub in_reserve: f64, } @@ -358,7 +393,7 @@ type Uptime = u8; #[derive(Clone, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] pub struct MixnodeStatusReportResponse { - pub mix_id: MixId, + pub mix_id: NodeId, pub identity: IdentityKey, pub owner: String, pub most_recent: Uptime, @@ -378,8 +413,40 @@ pub struct GatewayStatusReportResponse { pub last_day: Uptime, } +#[derive(Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +pub struct PerformanceHistoryResponse { + #[schema(value_type = u32)] + pub node_id: NodeId, + pub history: PaginatedResponse, +} + +#[derive(Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +pub struct UptimeHistoryResponse { + #[schema(value_type = u32)] + pub node_id: NodeId, + pub history: PaginatedResponse, +} + #[derive(Clone, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] pub struct HistoricalUptimeResponse { + #[schema(value_type = String, example = "1970-01-01")] + #[schemars(with = "String")] + pub date: Date, + + pub uptime: Uptime, +} + +#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +pub struct HistoricalPerformanceResponse { + #[schema(value_type = String, example = "1970-01-01")] + #[schemars(with = "String")] + pub date: Date, + + pub performance: f64, +} + +#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +pub struct OldHistoricalUptimeResponse { pub date: String, #[schema(value_type = u8)] pub uptime: Uptime, @@ -387,17 +454,17 @@ pub struct HistoricalUptimeResponse { #[derive(Clone, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] pub struct MixnodeUptimeHistoryResponse { - pub mix_id: MixId, + pub mix_id: NodeId, pub identity: String, pub owner: String, - pub history: Vec, + pub history: Vec, } #[derive(Clone, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] pub struct GatewayUptimeHistoryResponse { pub identity: String, pub owner: String, - pub history: Vec, + pub history: Vec, } #[derive(ToSchema)] @@ -543,14 +610,70 @@ impl JsonSchema for OffsetDateTimeJsonSchemaWrapper { } } -// this struct is getting quite bloated... #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] pub struct NymNodeDescription { + #[schema(value_type = u32)] + pub node_id: NodeId, + pub contract_node_type: DescribedNodeType, + pub description: NymNodeData, +} + +impl NymNodeDescription { + pub fn version(&self) -> &str { + &self.description.build_information.build_version + } + + pub fn entry_information(&self) -> BasicEntryInformation { + BasicEntryInformation { + hostname: self.description.host_information.hostname.clone(), + ws_port: self.description.mixnet_websockets.ws_port, + wss_port: self.description.mixnet_websockets.wss_port, + } + } + + pub fn to_skimmed_node(&self, role: NodeRole, performance: Performance) -> SkimmedNode { + let keys = &self.description.host_information.keys; + let entry = if self.description.declared_role.can_operate_entry_gateway() { + Some(self.entry_information()) + } else { + None + }; + + SkimmedNode { + node_id: self.node_id, + ed25519_identity_pubkey: keys.ed25519.clone(), + ip_addresses: self.description.host_information.ip_address.clone(), + mix_port: self.description.mix_port(), + x25519_sphinx_pubkey: keys.x25519.clone(), + // we can't use the declared roles, we have to take whatever was provided in the contract. + // why? say this node COULD operate as an exit, but it might be the case the contract decided + // to assign it an ENTRY role only. we have to use that one instead. + role, + entry, + performance, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum DescribedNodeType { + LegacyMixnode, + LegacyGateway, + NymNode, +} + +// this struct is getting quite bloated... +#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +pub struct NymNodeData { #[serde(default)] pub last_polled: OffsetDateTimeJsonSchemaWrapper, pub host_information: HostInformation, + #[serde(default)] + pub declared_role: NodeRoles, + #[serde(default)] pub auxiliary_details: AuxiliaryDetails, @@ -571,25 +694,33 @@ pub struct NymNodeDescription { // for now we only care about their ws/wss situation, nothing more pub mixnet_websockets: WebSockets, - - #[serde(default = "default_node_role")] - pub role: NodeRole, } -// For backwards compatibility, set a slightly artificial default -fn default_node_role() -> NodeRole { - NodeRole::Inactive +impl NymNodeData { + pub fn mix_port(&self) -> u16 { + self.auxiliary_details + .announce_ports + .mix_port + .unwrap_or(DEFAULT_MIX_LISTENING_PORT) + } + + pub fn verloc_port(&self) -> u16 { + self.auxiliary_details + .announce_ports + .verloc_port + .unwrap_or(DEFAULT_VERLOC_LISTENING_PORT) + } } #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] -pub struct DescribedGateway { - pub bond: GatewayBond, - pub self_described: Option, +pub struct LegacyDescribedGateway { + pub bond: LegacyGatewayBondWithId, + pub self_described: Option, } -impl From for DescribedGateway { - fn from(bond: GatewayBond) -> Self { - DescribedGateway { +impl From for LegacyDescribedGateway { + fn from(bond: LegacyGatewayBondWithId) -> Self { + LegacyDescribedGateway { bond, self_described: None, } @@ -597,14 +728,14 @@ impl From for DescribedGateway { } #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] -pub struct DescribedMixNode { - pub bond: MixNodeBond, - pub self_described: Option, +pub struct LegacyDescribedMixNode { + pub bond: LegacyMixNodeBondWithLayer, + pub self_described: Option, } -impl From for DescribedMixNode { - fn from(bond: MixNodeBond) -> Self { - DescribedMixNode { +impl From for LegacyDescribedMixNode { + fn from(bond: LegacyMixNodeBondWithLayer) -> Self { + LegacyDescribedMixNode { bond, self_described: None, } @@ -626,18 +757,45 @@ pub struct IpPacketRouterDetails { pub address: String, } +// works for current simple case. +impl From for IpPacketRouterDetails { + fn from(value: IpPacketRouter) -> Self { + IpPacketRouterDetails { + address: value.address, + } + } +} + #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] pub struct AuthenticatorDetails { /// address of the embedded authenticator pub address: String, } +// works for current simple case. +impl From for AuthenticatorDetails { + fn from(value: Authenticator) -> Self { + AuthenticatorDetails { + address: value.address, + } + } +} #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] pub struct WireguardDetails { pub port: u16, pub public_key: String, } +// works for current simple case. +impl From for WireguardDetails { + fn from(value: Wireguard) -> Self { + WireguardDetails { + port: value.port, + public_key: value.public_key, + } + } +} + #[derive(Clone, Copy, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] pub struct ApiHealthResponse { pub status: ApiStatus, diff --git a/nym-api/nym-api-requests/src/nym_nodes.rs b/nym-api/nym-api-requests/src/nym_nodes.rs index 3a4ea40ca9..7499c3f83b 100644 --- a/nym-api/nym-api-requests/src/nym_nodes.rs +++ b/nym-api/nym-api-requests/src/nym_nodes.rs @@ -2,12 +2,13 @@ // SPDX-License-Identifier: Apache-2.0 use crate::models::{ - GatewayBondAnnotated, MixNodeBondAnnotated, NymNodeDescription, OffsetDateTimeJsonSchemaWrapper, + GatewayBondAnnotated, MixNodeBondAnnotated, NymNodeData, OffsetDateTimeJsonSchemaWrapper, }; use nym_mixnet_contract_common::reward_params::Performance; -use nym_mixnet_contract_common::MixId; +use nym_mixnet_contract_common::NodeId; use serde::{Deserialize, Serialize}; use std::net::IpAddr; +use time::OffsetDateTime; use utoipa::ToSchema; #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)] @@ -16,9 +17,23 @@ pub struct CachedNodesResponse { pub nodes: Vec, } +impl From> for CachedNodesResponse { + fn from(nodes: Vec) -> Self { + CachedNodesResponse::new(nodes) + } +} + +impl CachedNodesResponse { + pub fn new(nodes: Vec) -> Self { + CachedNodesResponse { + refreshed_at: OffsetDateTime::now_utc().into(), + nodes, + } + } +} + #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, utoipa::ToSchema)] #[serde(rename_all = "kebab-case")] -#[cfg_attr(feature = "rocket-traits", derive(rocket::form::FromFormField))] pub enum NodeRoleQueryParam { ActiveMixnode, @@ -56,8 +71,6 @@ pub struct BasicEntryInformation { pub wss_port: Option, } -type NodeId = MixId; - // the bare minimum information needed to construct sphinx packets #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] pub struct SkimmedNode { @@ -82,9 +95,16 @@ pub struct SkimmedNode { } impl SkimmedNode { + pub fn get_mix_layer(&self) -> Option { + match self.role { + NodeRole::Mixnode { layer } => Some(layer), + _ => None, + } + } + pub fn from_described_gateway( annotated: &GatewayBondAnnotated, - description: Option<&NymNodeDescription>, + description: Option<&NymNodeData>, ) -> Self { let mut base: SkimmedNode = annotated.into(); let Some(description) = description else { @@ -129,15 +149,15 @@ impl<'a> From<&'a MixNodeBondAnnotated> for SkimmedNode { impl<'a> From<&'a GatewayBondAnnotated> for SkimmedNode { fn from(value: &'a GatewayBondAnnotated) -> Self { SkimmedNode { - node_id: MixId::MAX, + node_id: value.gateway_bond.node_id, ip_addresses: value.ip_addresses.clone(), - ed25519_identity_pubkey: value.gateway_bond.identity().clone(), - mix_port: value.gateway_bond.gateway.mix_port, - x25519_sphinx_pubkey: value.gateway_bond.gateway.sphinx_key.clone(), + ed25519_identity_pubkey: value.gateway_bond.bond.identity().clone(), + mix_port: value.gateway_bond.bond.gateway.mix_port, + x25519_sphinx_pubkey: value.gateway_bond.bond.gateway.sphinx_key.clone(), role: NodeRole::EntryGateway, entry: Some(BasicEntryInformation { hostname: None, - ws_port: value.gateway_bond.gateway.clients_port, + ws_port: value.gateway_bond.bond.gateway.clients_port, wss_port: None, }), performance: value.node_performance.last_24h, @@ -159,5 +179,5 @@ pub struct FullFatNode { pub expanded: SemiSkimmedNode, // kinda temporary for now to make as few changes as possible for now - pub self_described: Option, + pub self_described: Option, } diff --git a/nym-api/src/circulating_supply_api/cache/mod.rs b/nym-api/src/circulating_supply_api/cache/mod.rs index 7a2537ebee..87e401da9d 100644 --- a/nym-api/src/circulating_supply_api/cache/mod.rs +++ b/nym-api/src/circulating_supply_api/cache/mod.rs @@ -6,7 +6,6 @@ use cosmwasm_std::Addr; use nym_api_requests::models::CirculatingSupplyResponse; use nym_validator_client::nyxd::error::NyxdError; use nym_validator_client::nyxd::Coin; -use rocket::fairing::AdHoc; use std::ops::Deref; use std::{ sync::{atomic::AtomicBool, Arc}, @@ -15,6 +14,7 @@ use std::{ use thiserror::Error; use tokio::sync::RwLock; use tokio::time; +use tracing::{error, info}; mod data; pub(crate) mod refresher; @@ -67,13 +67,6 @@ impl CirculatingSupplyCache { } } - #[deprecated(note = "TODO rocket: obsolete because it's used for Rocket")] - pub(crate) fn stage(mix_denom: String) -> AdHoc { - AdHoc::on_ignite("Circulating Supply Cache Stage", |rocket| async { - rocket.manage(Self::new(mix_denom)) - }) - } - pub(crate) async fn update(&self, mixmining_reserve: Coin, vesting_tokens: Coin) { let mut cache = self.data.write().await; @@ -81,10 +74,10 @@ impl CirculatingSupplyCache { circulating_supply.amount -= mixmining_reserve.amount; circulating_supply.amount -= vesting_tokens.amount; - log::info!("Updating circulating supply cache"); - log::info!("the mixmining reserve is now {mixmining_reserve}"); - log::info!("the number of tokens still vesting is now {vesting_tokens}"); - log::info!("the circulating supply is now {circulating_supply}"); + info!("Updating circulating supply cache"); + info!("the mixmining reserve is now {mixmining_reserve}"); + info!("the number of tokens still vesting is now {vesting_tokens}"); + info!("the circulating supply is now {circulating_supply}"); cache.mixmining_reserve.unchecked_update(mixmining_reserve); cache.vesting_tokens.unchecked_update(vesting_tokens); diff --git a/nym-api/src/circulating_supply_api/cache/refresher.rs b/nym-api/src/circulating_supply_api/cache/refresher.rs index f983390356..60d7c2bafc 100644 --- a/nym-api/src/circulating_supply_api/cache/refresher.rs +++ b/nym-api/src/circulating_supply_api/cache/refresher.rs @@ -11,6 +11,7 @@ use std::collections::HashSet; use std::sync::atomic::Ordering; use std::time::Duration; use tokio::time; +use tracing::{error, trace}; pub(crate) struct CirculatingSupplyCacheRefresher { nyxd_client: Client, diff --git a/nym-api/src/circulating_supply_api/handlers.rs b/nym-api/src/circulating_supply_api/handlers.rs index 973d1fc64e..3e49659b55 100644 --- a/nym-api/src/circulating_supply_api/handlers.rs +++ b/nym-api/src/circulating_supply_api/handlers.rs @@ -1,15 +1,13 @@ // Copyright 2022-2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::{ - node_status_api::models::{AxumErrorResponse, AxumResult}, - v2::AxumAppState, -}; +use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; +use crate::support::http::state::AppState; use axum::{extract, Router}; use nym_api_requests::models::CirculatingSupplyResponse; use nym_validator_client::nyxd::Coin; -pub(crate) fn circulating_supply_routes() -> Router { +pub(crate) fn circulating_supply_routes() -> Router { Router::new() .route("/", axum::routing::get(get_full_circulating_supply)) .route( @@ -31,7 +29,7 @@ pub(crate) fn circulating_supply_routes() -> Router { ) )] async fn get_full_circulating_supply( - extract::State(state): extract::State, + extract::State(state): extract::State, ) -> AxumResult> { match state .circulating_supply_cache() @@ -52,7 +50,7 @@ async fn get_full_circulating_supply( ) )] async fn get_total_supply( - extract::State(state): extract::State, + extract::State(state): extract::State, ) -> AxumResult> { let full_circulating_supply = match state .circulating_supply_cache() @@ -75,7 +73,7 @@ async fn get_total_supply( ) )] async fn get_circulating_supply( - extract::State(state): extract::State, + extract::State(state): extract::State, ) -> AxumResult> { let full_circulating_supply = match state .circulating_supply_cache() diff --git a/nym-api/src/circulating_supply_api/mod.rs b/nym-api/src/circulating_supply_api/mod.rs index 2bfefcc35f..9c125815cb 100644 --- a/nym-api/src/circulating_supply_api/mod.rs +++ b/nym-api/src/circulating_supply_api/mod.rs @@ -1,28 +1,22 @@ // Copyright 2022-2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use nym_task::TaskManager; -use okapi::openapi3::OpenApi; -use rocket::Route; -use rocket_okapi::{openapi_get_routes_spec, settings::OpenApiSettings}; - -use crate::support::{config, nyxd}; - use self::cache::refresher::CirculatingSupplyCacheRefresher; +use crate::support::{config, nyxd}; +use nym_task::TaskManager; pub(crate) mod cache; -#[cfg(feature = "axum")] pub(crate) mod handlers; -pub(crate) mod routes; +// pub(crate) mod routes; -/// Merges the routes with http information and returns it to Rocket for serving -pub(crate) fn circulating_supply_routes(settings: &OpenApiSettings) -> (Vec, OpenApi) { - openapi_get_routes_spec![ - settings: routes::get_full_circulating_supply, - routes::get_total_supply, - routes::get_circulating_supply - ] -} +// /// Merges the routes with http information and returns it to Rocket for serving +// pub(crate) fn circulating_supply_routes(settings: &OpenApiSettings) -> (Vec, OpenApi) { +// openapi_get_routes_spec![ +// settings: routes::get_full_circulating_supply, +// routes::get_total_supply, +// routes::get_circulating_supply +// ] +// } /// Spawn the circulating supply cache refresher. pub(crate) fn start_cache_refresh( diff --git a/nym-api/src/ecash/api_routes/aggregation.rs b/nym-api/src/ecash/api_routes/aggregation.rs index f552698d86..9e6018550c 100644 --- a/nym-api/src/ecash/api_routes/aggregation.rs +++ b/nym-api/src/ecash/api_routes/aggregation.rs @@ -3,7 +3,6 @@ use crate::ecash::error::{EcashError, Result}; use crate::ecash::state::EcashState; -use log::trace; use nym_api_requests::ecash::models::{ AggregatedCoinIndicesSignatureResponse, AggregatedExpirationDateSignatureResponse, }; @@ -14,6 +13,7 @@ use rocket::serde::json::Json; use rocket::State as RocketState; use rocket_okapi::openapi; use time::Date; +use tracing::trace; // routes with globally aggregated keys, signatures, etc. diff --git a/nym-api/src/ecash/api_routes/aggregation_axum.rs b/nym-api/src/ecash/api_routes/aggregation_axum.rs index 4a03313580..d956f2897d 100644 --- a/nym-api/src/ecash/api_routes/aggregation_axum.rs +++ b/nym-api/src/ecash/api_routes/aggregation_axum.rs @@ -5,10 +5,9 @@ use crate::ecash::api_routes::helpers::EpochIdParam; use crate::ecash::error::EcashError; use crate::ecash::state::EcashState; use crate::node_status_api::models::AxumResult; -use crate::v2::AxumAppState; +use crate::support::http::state::AppState; use axum::extract::Path; use axum::{Json, Router}; -use log::trace; use nym_api_requests::ecash::models::{ AggregatedCoinIndicesSignatureResponse, AggregatedExpirationDateSignatureResponse, }; @@ -18,10 +17,11 @@ use nym_validator_client::nym_api::rfc_3339_date; use serde::Deserialize; use std::sync::Arc; use time::Date; +use tracing::trace; use utoipa::IntoParams; /// routes with globally aggregated keys, signatures, etc. -pub(crate) fn aggregation_routes(ecash_state: Arc) -> Router { +pub(crate) fn aggregation_routes(ecash_state: Arc) -> Router { Router::new() .route( "/master-verification-key:epoch_id", diff --git a/nym-api/src/ecash/api_routes/handlers.rs b/nym-api/src/ecash/api_routes/handlers.rs index b40ccf9773..5b9d6cee46 100644 --- a/nym-api/src/ecash/api_routes/handlers.rs +++ b/nym-api/src/ecash/api_routes/handlers.rs @@ -6,11 +6,11 @@ use crate::ecash::api_routes::issued_axum::issued_routes; use crate::ecash::api_routes::partial_signing_axum::partial_signing_routes; use crate::ecash::api_routes::spending_axum::spending_routes; use crate::ecash::state::EcashState; -use crate::v2::AxumAppState; +use crate::support::http::state::AppState; use axum::Router; use std::sync::Arc; -pub(crate) fn ecash_routes(ecash_state: Arc) -> Router { +pub(crate) fn ecash_routes(ecash_state: Arc) -> Router { Router::new() .merge(aggregation_routes(Arc::clone(&ecash_state))) .merge(issued_routes(Arc::clone(&ecash_state))) diff --git a/nym-api/src/ecash/api_routes/helpers.rs b/nym-api/src/ecash/api_routes/helpers.rs index 4469bf11d0..6fba7d43dd 100644 --- a/nym-api/src/ecash/api_routes/helpers.rs +++ b/nym-api/src/ecash/api_routes/helpers.rs @@ -27,7 +27,6 @@ pub(crate) fn build_credentials_response( Ok(IssuedCredentialsResponse { credentials }) } -#[cfg(feature = "axum")] #[derive(serde::Deserialize, utoipa::IntoParams)] #[into_params(parameter_in = Path)] pub(super) struct EpochIdParam { diff --git a/nym-api/src/ecash/api_routes/issued_axum.rs b/nym-api/src/ecash/api_routes/issued_axum.rs index 72fa55e39f..95c1439600 100644 --- a/nym-api/src/ecash/api_routes/issued_axum.rs +++ b/nym-api/src/ecash/api_routes/issued_axum.rs @@ -6,7 +6,7 @@ use crate::ecash::error::EcashError; use crate::ecash::state::EcashState; use crate::ecash::storage::EcashStorageExt; use crate::node_status_api::models::AxumResult; -use crate::v2::AxumAppState; +use crate::support::http::state::AppState; use axum::extract::Path; use axum::{Json, Router}; use nym_api_requests::ecash::models::{ @@ -17,7 +17,7 @@ use serde::Deserialize; use std::sync::Arc; use utoipa::IntoParams; -pub(crate) fn issued_routes(ecash_state: Arc) -> Router { +pub(crate) fn issued_routes(ecash_state: Arc) -> Router { Router::new() .route( "/epoch-credentials/:epoch", diff --git a/nym-api/src/ecash/api_routes/mod.rs b/nym-api/src/ecash/api_routes/mod.rs index 910e253288..9c0082748c 100644 --- a/nym-api/src/ecash/api_routes/mod.rs +++ b/nym-api/src/ecash/api_routes/mod.rs @@ -1,18 +1,14 @@ // Copyright 2023-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -pub(crate) mod aggregation; +// pub(crate) mod aggregation; mod helpers; -pub(crate) mod issued; -pub(crate) mod partial_signing; -pub(crate) mod spending; +// pub(crate) mod issued; +// pub(crate) mod partial_signing; +// pub(crate) mod spending; -cfg_if::cfg_if! { - if #[cfg(feature = "axum")] { - pub(crate) mod aggregation_axum; - pub(crate) mod handlers; - pub(crate) mod issued_axum; - pub(crate) mod partial_signing_axum; - pub(crate) mod spending_axum; - } -} +pub(crate) mod aggregation_axum; +pub(crate) mod handlers; +pub(crate) mod issued_axum; +pub(crate) mod partial_signing_axum; +pub(crate) mod spending_axum; diff --git a/nym-api/src/ecash/api_routes/partial_signing_axum.rs b/nym-api/src/ecash/api_routes/partial_signing_axum.rs index fe93ddc921..e42ffcf6e0 100644 --- a/nym-api/src/ecash/api_routes/partial_signing_axum.rs +++ b/nym-api/src/ecash/api_routes/partial_signing_axum.rs @@ -6,7 +6,7 @@ use crate::ecash::error::EcashError; use crate::ecash::helpers::blind_sign; use crate::ecash::state::EcashState; use crate::node_status_api::models::AxumResult; -use crate::v2::AxumAppState; +use crate::support::http::state::AppState; use axum::extract::Path; use axum::{Json, Router}; use nym_api_requests::ecash::{ @@ -19,9 +19,10 @@ use serde::Deserialize; use std::ops::Deref; use std::sync::Arc; use time::Date; +use tracing::{debug, trace}; use utoipa::IntoParams; -pub(crate) fn partial_signing_routes(ecash_state: Arc) -> Router { +pub(crate) fn partial_signing_routes(ecash_state: Arc) -> Router { Router::new() .route( "/blind-sign", diff --git a/nym-api/src/ecash/api_routes/spending.rs b/nym-api/src/ecash/api_routes/spending.rs index b5eedbcdbe..1ea24efde6 100644 --- a/nym-api/src/ecash/api_routes/spending.rs +++ b/nym-api/src/ecash/api_routes/spending.rs @@ -78,16 +78,16 @@ pub async fn verify_ticket( ) { IdentifyResult::NotADuplicatePayment => {} //SW NOTE This should never happen, quick message? IdentifyResult::DuplicatePayInfo(_) => { - log::warn!("Identical payInfo"); + warn!("Identical payInfo"); return reject_ticket(EcashTicketVerificationRejection::ReplayedTicket); } IdentifyResult::DoubleSpendingPublicKeys(pub_key) => { //Actual double spending - log::warn!( + warn!( "Double spending attempt for key {}", pub_key.to_base58_string() ); - log::error!("UNIMPLEMENTED: blacklisting the double spend key"); + error!("UNIMPLEMENTED: blacklisting the double spend key"); return reject_ticket(EcashTicketVerificationRejection::DoubleSpend); } } diff --git a/nym-api/src/ecash/api_routes/spending_axum.rs b/nym-api/src/ecash/api_routes/spending_axum.rs index aca64b7f0c..a5110a52fc 100644 --- a/nym-api/src/ecash/api_routes/spending_axum.rs +++ b/nym-api/src/ecash/api_routes/spending_axum.rs @@ -4,7 +4,7 @@ use crate::ecash::error::EcashError; use crate::ecash::state::EcashState; use crate::node_status_api::models::AxumResult; -use crate::v2::AxumAppState; +use crate::support::http::state::AppState; use axum::{Json, Router}; use nym_api_requests::constants::MIN_BATCH_REDEMPTION_DELAY; use nym_api_requests::ecash::models::{ @@ -18,8 +18,9 @@ use std::ops::Deref; use std::sync::Arc; use time::macros::time; use time::{OffsetDateTime, Time}; +use tracing::{error, warn}; -pub(crate) fn spending_routes(ecash_state: Arc) -> Router { +pub(crate) fn spending_routes(ecash_state: Arc) -> Router { Router::new() .route( "/verify-ecash-ticket", @@ -111,16 +112,16 @@ async fn verify_ticket( ) { IdentifyResult::NotADuplicatePayment => {} //SW NOTE This should never happen, quick message? IdentifyResult::DuplicatePayInfo(_) => { - log::warn!("Identical payInfo"); + warn!("Identical payInfo"); return reject_ticket(EcashTicketVerificationRejection::ReplayedTicket); } IdentifyResult::DoubleSpendingPublicKeys(pub_key) => { //Actual double spending - log::warn!( + warn!( "Double spending attempt for key {}", pub_key.to_base58_string() ); - log::error!("UNIMPLEMENTED: blacklisting the double spend key"); + error!("UNIMPLEMENTED: blacklisting the double spend key"); return reject_ticket(EcashTicketVerificationRejection::DoubleSpend); } } diff --git a/nym-api/src/ecash/client.rs b/nym-api/src/ecash/client.rs index e0970f2155..805252808a 100644 --- a/nym-api/src/ecash/client.rs +++ b/nym-api/src/ecash/client.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::ecash::error::Result; +use async_trait::async_trait; use cw3::{ProposalResponse, VoteResponse}; use cw4::MemberResponse; use nym_coconut_dkg_common::dealer::{ diff --git a/nym-api/src/ecash/comm.rs b/nym-api/src/ecash/comm.rs index a539cb12c2..fd70c592eb 100644 --- a/nym-api/src/ecash/comm.rs +++ b/nym-api/src/ecash/comm.rs @@ -5,6 +5,7 @@ use crate::ecash::client::Client; use crate::ecash::error::{EcashError, Result}; use crate::ecash::helpers::CachedImmutableEpochItem; use crate::{ecash, nyxd}; +use async_trait::async_trait; use nym_coconut_dkg_common::types::{Epoch, EpochId}; use nym_dkg::Threshold; use nym_validator_client::EcashApiClient; diff --git a/nym-api/src/ecash/dkg/controller/keys.rs b/nym-api/src/ecash/dkg/controller/keys.rs index 6b806bf792..474d6ab0b1 100644 --- a/nym-api/src/ecash/dkg/controller/keys.rs +++ b/nym-api/src/ecash/dkg/controller/keys.rs @@ -10,10 +10,11 @@ use nym_dkg::bte::keys::KeyPair as DkgKeyPair; use rand::{CryptoRng, RngCore}; use std::path::Path; use thiserror::__private::AsDisplay; +use tracing::warn; pub(crate) fn init_bte_keypair( rng: &mut R, - config: &config::CoconutSigner, + config: &config::EcashSigner, ) -> anyhow::Result<()> { let dkg_params = nym_dkg::bte::setup(); let kp = DkgKeyPair::new(&dkg_params, rng); @@ -27,7 +28,7 @@ pub(crate) fn init_bte_keypair( .context("DKG BTE keypair store failure") } -pub(crate) fn load_bte_keypair(config: &config::CoconutSigner) -> anyhow::Result { +pub(crate) fn load_bte_keypair(config: &config::EcashSigner) -> anyhow::Result { nym_pemstore::load_keypair(&nym_pemstore::KeyPairPath::new( &config.storage_paths.decryption_key_path, &config.storage_paths.public_key_with_proof_path, @@ -36,25 +37,25 @@ pub(crate) fn load_bte_keypair(config: &config::CoconutSigner) -> anyhow::Result } pub(crate) fn load_ecash_keypair_if_exists( - config: &config::CoconutSigner, + config: &config::EcashSigner, ) -> anyhow::Result> { - if !config.storage_paths.coconut_key_path.exists() { + if !config.storage_paths.ecash_key_path.exists() { return Ok(None); } // first attempt to load ecash keys directly, // if that fails fallback to coconut keys and perform migration if let Ok(ecash_key) = - nym_pemstore::load_key::(&config.storage_paths.coconut_key_path) + nym_pemstore::load_key::(&config.storage_paths.ecash_key_path) { return Ok(Some(ecash_key)); } - if let Ok(legacy_coconut_key) = nym_pemstore::load_key::( - &config.storage_paths.coconut_key_path, - ) { + if let Ok(legacy_coconut_key) = + nym_pemstore::load_key::(&config.storage_paths.ecash_key_path) + { let migrated_key: KeyPairWithEpoch = legacy_coconut_key.into(); - nym_pemstore::store_key(&migrated_key, &config.storage_paths.coconut_key_path) + nym_pemstore::store_key(&migrated_key, &config.storage_paths.ecash_key_path) .context("migrated key storage failure")?; return Ok(Some(migrated_key)); diff --git a/nym-api/src/ecash/dkg/controller/mod.rs b/nym-api/src/ecash/dkg/controller/mod.rs index 6dd454f10d..25cba6b48d 100644 --- a/nym-api/src/ecash/dkg/controller/mod.rs +++ b/nym-api/src/ecash/dkg/controller/mod.rs @@ -18,6 +18,7 @@ use std::path::PathBuf; use std::time::Duration; use time::OffsetDateTime; use tokio::time::{interval, MissedTickBehavior}; +use tracing::{debug, error, info, trace, warn}; mod error; pub(crate) mod keys; @@ -32,7 +33,7 @@ pub(crate) struct DkgController { impl DkgController { pub(crate) fn new( - config: &config::CoconutSigner, + config: &config::EcashSigner, nyxd_client: nyxd::Client, coconut_keypair: CoconutKeyPair, dkg_keypair: DkgKeyPair, @@ -52,7 +53,7 @@ impl DkgController { Ok(DkgController { dkg_client: DkgClient::new(nyxd_client), - coconut_key_path: config.storage_paths.coconut_key_path.clone(), + coconut_key_path: config.storage_paths.ecash_key_path.clone(), state: State::new( config.storage_paths.dkg_persistent_state_path.clone(), persistent_state, @@ -304,7 +305,7 @@ impl DkgController { } pub(crate) fn start( - config: &config::CoconutSigner, + config: &config::EcashSigner, nyxd_client: nyxd::Client, coconut_keypair: CoconutKeyPair, dkg_bte_keypair: DkgKeyPair, diff --git a/nym-api/src/ecash/dkg/dealing.rs b/nym-api/src/ecash/dkg/dealing.rs index bb8c17d92b..a0ae4ac840 100644 --- a/nym-api/src/ecash/dkg/dealing.rs +++ b/nym-api/src/ecash/dkg/dealing.rs @@ -6,7 +6,6 @@ use crate::ecash::dkg::controller::keys::archive_coconut_keypair; use crate::ecash::dkg::controller::DkgController; use crate::ecash::error::EcashError; use crate::ecash::keys::KeyPairWithEpoch; -use log::debug; use nym_coconut_dkg_common::dealing::{chunk_dealing, DealingChunkInfo, MAX_DEALING_CHUNK_SIZE}; use nym_coconut_dkg_common::types::{DealingIndex, EpochId}; use nym_dkg::{Dealing, Scalar}; @@ -15,6 +14,7 @@ use std::collections::{HashMap, HashSet}; use std::fmt::{Debug, Formatter}; use std::path::PathBuf; use thiserror::Error; +use tracing::{debug, error, info, warn}; enum DealingGeneration { Fresh { number: u32 }, diff --git a/nym-api/src/ecash/dkg/key_derivation.rs b/nym-api/src/ecash/dkg/key_derivation.rs index 23291806b8..f3e69e1fe6 100644 --- a/nym-api/src/ecash/dkg/key_derivation.rs +++ b/nym-api/src/ecash/dkg/key_derivation.rs @@ -8,7 +8,6 @@ use crate::ecash::dkg::state::key_derivation::{DealerRejectionReason, Derivation use crate::ecash::error::EcashError; use crate::ecash::keys::KeyPairWithEpoch; use cosmwasm_std::Addr; -use log::debug; use nym_coconut_dkg_common::event_attributes::DKG_PROPOSAL_ID; use nym_coconut_dkg_common::types::{DealingIndex, EpochId, NodeIndex}; use nym_compact_ecash::scheme::keygen::SecretKeyAuth; @@ -25,6 +24,8 @@ use rand::{CryptoRng, RngCore}; use std::collections::{BTreeMap, HashMap}; use std::ops::Deref; use thiserror::Error; +use tracing::debug; +use tracing::{error, info, warn}; #[derive(Debug, Error)] pub enum KeyDerivationError { diff --git a/nym-api/src/ecash/dkg/key_finalization.rs b/nym-api/src/ecash/dkg/key_finalization.rs index 9ba5ff958a..b23e2312ba 100644 --- a/nym-api/src/ecash/dkg/key_finalization.rs +++ b/nym-api/src/ecash/dkg/key_finalization.rs @@ -7,6 +7,7 @@ use cw3::Status; use nym_coconut_dkg_common::types::EpochId; use rand::{CryptoRng, RngCore}; use thiserror::Error; +use tracing::{debug, error, info, warn}; #[derive(Debug, Error)] pub enum KeyFinalizationError { diff --git a/nym-api/src/ecash/dkg/key_validation.rs b/nym-api/src/ecash/dkg/key_validation.rs index b99bacd962..aff0ea90ac 100644 --- a/nym-api/src/ecash/dkg/key_validation.rs +++ b/nym-api/src/ecash/dkg/key_validation.rs @@ -13,6 +13,7 @@ use nym_compact_ecash::{ use rand::{CryptoRng, RngCore}; use std::collections::HashMap; use thiserror::Error; +use tracing::{debug, error, info, warn}; fn vote_matches(voted_yes: bool, chain_vote: Vote) -> bool { if voted_yes && chain_vote == Vote::Yes { diff --git a/nym-api/src/ecash/dkg/public_key.rs b/nym-api/src/ecash/dkg/public_key.rs index b7f7d3e6ac..afbdf145dc 100644 --- a/nym-api/src/ecash/dkg/public_key.rs +++ b/nym-api/src/ecash/dkg/public_key.rs @@ -3,10 +3,11 @@ use crate::ecash::dkg::controller::DkgController; use crate::ecash::error::EcashError; -use log::debug; use nym_coconut_dkg_common::types::EpochId; use rand::{CryptoRng, RngCore}; use thiserror::Error; +use tracing::debug; +use tracing::info; #[derive(Debug, Error)] pub enum PublicKeySubmissionError { diff --git a/nym-api/src/ecash/dkg/state/mod.rs b/nym-api/src/ecash/dkg/state/mod.rs index eb1cca637e..065cdb3f24 100644 --- a/nym-api/src/ecash/dkg/state/mod.rs +++ b/nym-api/src/ecash/dkg/state/mod.rs @@ -10,7 +10,6 @@ use crate::ecash::dkg::state::registration::{DkgParticipant, ParticipantState, R use crate::ecash::error::EcashError; use crate::ecash::keys::{KeyPair as CoconutKeyPair, KeyPairWithEpoch}; use cosmwasm_std::Addr; -use log::debug; use nym_coconut_dkg_common::dealer::DealerDetails; use nym_coconut_dkg_common::types::EpochId; use nym_crypto::asymmetric::identity; @@ -20,6 +19,7 @@ use serde::{Deserialize, Serialize}; use std::collections::{BTreeMap, HashMap}; use std::path::{Path, PathBuf}; use time::OffsetDateTime; +use tracing::{debug, warn}; use url::Url; pub(crate) mod dealing_exchange; diff --git a/nym-api/src/ecash/error.rs b/nym-api/src/ecash/error.rs index 94201847dc..e5c7a9fd89 100644 --- a/nym-api/src/ecash/error.rs +++ b/nym-api/src/ecash/error.rs @@ -15,14 +15,6 @@ use nym_ecash_contract_common::redeem_credential::BATCH_REDEMPTION_PROPOSAL_TITL use nym_validator_client::coconut::EcashApiError; use nym_validator_client::nyxd::error::NyxdError; use nym_validator_client::nyxd::AccountId; -use okapi::openapi3::Responses; -use rocket::http::{ContentType, Status}; -use rocket::response::Responder; -use rocket::{response, Request, Response}; -use rocket_okapi::gen::OpenApiGenerator; -use rocket_okapi::response::OpenApiResponderInner; -use rocket_okapi::util::ensure_status_code_exists; -use std::io::Cursor; use std::num::ParseIntError; use thiserror::Error; use time::error::ComponentRange; @@ -219,24 +211,24 @@ pub enum EcashError { UnknownTicketBookType(#[from] UnknownTicketType), } -impl<'r, 'o: 'r> Responder<'r, 'o> for EcashError { - fn respond_to(self, _: &'r Request<'_>) -> response::Result<'o> { - let err_msg = self.to_string(); - Response::build() - .header(ContentType::Plain) - .sized_body(err_msg.len(), Cursor::new(err_msg)) - .status(Status::BadRequest) - .ok() - } -} - -impl OpenApiResponderInner for EcashError { - fn responses(_gen: &mut OpenApiGenerator) -> rocket_okapi::Result { - let mut responses = Responses::default(); - ensure_status_code_exists(&mut responses, 400); - Ok(responses) - } -} +// impl<'r, 'o: 'r> Responder<'r, 'o> for EcashError { +// fn respond_to(self, _: &'r Request<'_>) -> response::Result<'o> { +// let err_msg = self.to_string(); +// Response::build() +// .header(ContentType::Plain) +// .sized_body(err_msg.len(), Cursor::new(err_msg)) +// .status(Status::BadRequest) +// .ok() +// } +// } +// +// impl OpenApiResponderInner for EcashError { +// fn responses(_gen: &mut OpenApiGenerator) -> rocket_okapi::Result { +// let mut responses = Responses::default(); +// ensure_status_code_exists(&mut responses, 400); +// Ok(responses) +// } +// } #[derive(Debug, Error)] pub enum RedemptionError { diff --git a/nym-api/src/ecash/mod.rs b/nym-api/src/ecash/mod.rs index 662822114f..2855f8dd7a 100644 --- a/nym-api/src/ecash/mod.rs +++ b/nym-api/src/ecash/mod.rs @@ -1,11 +1,6 @@ // Copyright 2021-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use okapi::openapi3::OpenApi; -use rocket::Route; -use rocket_okapi::openapi_get_routes_spec; -use rocket_okapi::settings::OpenApiSettings; - pub(crate) mod api_routes; pub(crate) mod client; pub(crate) mod comm; @@ -22,26 +17,26 @@ pub(crate) mod tests; // equivalent of 100nym pub(crate) const MINIMUM_BALANCE: u128 = 100_000000; -pub(crate) fn routes_open_api(settings: &OpenApiSettings, enabled: bool) -> (Vec, OpenApi) { - if enabled { - openapi_get_routes_spec![ - settings: - api_routes::partial_signing::post_blind_sign, - api_routes::partial_signing::partial_expiration_date_signatures, - api_routes::partial_signing::partial_coin_indices_signatures, - api_routes::spending::verify_ticket, - api_routes::spending::batch_redeem_tickets, - api_routes::spending::double_spending_filter_v1, - api_routes::issued::epoch_credentials, - api_routes::issued::issued_credential, - api_routes::issued::issued_credentials, - api_routes::aggregation::master_verification_key, - api_routes::aggregation::coin_indices_signatures, - api_routes::aggregation::expiration_date_signatures - ] - } else { - openapi_get_routes_spec![ - settings: - ] - } -} +// pub(crate) fn routes_open_api(settings: &OpenApiSettings, enabled: bool) -> (Vec, OpenApi) { +// if enabled { +// openapi_get_routes_spec![ +// settings: +// api_routes::partial_signing::post_blind_sign, +// api_routes::partial_signing::partial_expiration_date_signatures, +// api_routes::partial_signing::partial_coin_indices_signatures, +// api_routes::spending::verify_ticket, +// api_routes::spending::batch_redeem_tickets, +// api_routes::spending::double_spending_filter_v1, +// api_routes::issued::epoch_credentials, +// api_routes::issued::issued_credential, +// api_routes::issued::issued_credentials, +// api_routes::aggregation::master_verification_key, +// api_routes::aggregation::coin_indices_signatures, +// api_routes::aggregation::expiration_date_signatures +// ] +// } else { +// openapi_get_routes_spec![ +// settings: +// ] +// } +// } diff --git a/nym-api/src/ecash/state/helpers.rs b/nym-api/src/ecash/state/helpers.rs index 2fcfd18a31..b96ab53099 100644 --- a/nym-api/src/ecash/state/helpers.rs +++ b/nym-api/src/ecash/state/helpers.rs @@ -16,6 +16,7 @@ use std::future::Future; use time::ext::NumericalDuration; use time::Date; use tokio::sync::Mutex; +use tracing::{debug, error, info, warn}; // attempt to completely rebuild the bloomfilter data for given day async fn try_rebuild_today_bloomfilter( @@ -23,10 +24,10 @@ async fn try_rebuild_today_bloomfilter( params: BloomfilterParameters, storage: &NymApiStorage, ) -> Result { - log::info!("rebuilding bloomfilter for {today}"); + info!("rebuilding bloomfilter for {today}"); let tickets = storage.get_all_spent_tickets_on(today).await?; - log::debug!( + debug!( "there are {} tickets to insert into the filter", tickets.len() ); @@ -45,7 +46,7 @@ pub(crate) async fn prepare_partial_bloomfilter_builder( start: Date, days: i64, ) -> Result { - log::info!( + info!( "attempting to rebuild partial bloomfilter starting at {start} which includes {days} days" ); @@ -56,11 +57,11 @@ pub(crate) async fn prepare_partial_bloomfilter_builder( .try_load_partial_bloomfilter_bitmap(date, params_id) .await? else { - log::warn!("missing double spending bloomfilter bitmap for {date} (if this API hasn't been running for at least {days} day(s) since 'ecash'-based zk-nyms were introduced this is expected)"); + warn!("missing double spending bloomfilter bitmap for {date} (if this API hasn't been running for at least {days} day(s) since 'ecash'-based zk-nyms were introduced this is expected)"); continue; }; if !filter_builder.add_bytes(&bitmap) { - log::error!( + error!( "failed to add bitmap from {date} to the global bloomfilter. it may be malformed!" ); } @@ -71,16 +72,16 @@ pub(crate) async fn prepare_partial_bloomfilter_builder( pub(super) async fn try_rebuild_bloomfilter( storage: &NymApiStorage, ) -> Result { - log::info!("attempting to rebuild the double spending bloomfilter..."); + info!("attempting to rebuild the double spending bloomfilter..."); let today = ecash_today().date(); let (params_id, params) = storage.get_double_spending_filter_params().await?; - log::info!("will use the following parameters: {params:?}"); + info!("will use the following parameters: {params:?}"); // we're never going to have persisted data for 'today'. we need to rebuild it from scratch let today_filter = try_rebuild_today_bloomfilter(today, params, storage).await?; - log::info!("attempting to rebuild the global filter"); + info!("attempting to rebuild the global filter"); let mut global_filter = prepare_partial_bloomfilter_builder( storage, params, @@ -91,9 +92,7 @@ pub(super) async fn try_rebuild_bloomfilter( .await?; if !global_filter.add_bytes(&today_filter.dump_bitmap()) { - log::error!( - "failed to add bitmap from {today} to the global bloomfilter. it may be malformed!" - ); + error!("failed to add bitmap from {today} to the global bloomfilter. it may be malformed!"); } Ok(TicketDoubleSpendingFilter::new( @@ -141,7 +140,7 @@ where match f(api).await { Ok(partial_share) => shares.lock().await.push(partial_share), Err(err) => { - log::warn!("failed to obtain partial threshold data from API: {disp}: {err}") + warn!("failed to obtain partial threshold data from API: {disp}: {err}") } } }) diff --git a/nym-api/src/ecash/state/local.rs b/nym-api/src/ecash/state/local.rs index fd3b5080b1..52faecbf3f 100644 --- a/nym-api/src/ecash/state/local.rs +++ b/nym-api/src/ecash/state/local.rs @@ -13,6 +13,7 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use time::{Date, OffsetDateTime}; use tokio::sync::RwLock; +use tracing::debug; pub(crate) struct TicketDoubleSpendingFilter { built_on: Date, @@ -144,7 +145,7 @@ impl LocalEcashState { if should_export { tokio::spawn(async move { - log::debug!("exporting bloomfilter bitmap"); + debug!("exporting bloomfilter bitmap"); let new = filter.read().await.export_global_bitmap(); let mut exported_guard = exported.data.write().await; exported_guard.last_exported_at = OffsetDateTime::now_utc(); diff --git a/nym-api/src/ecash/state/mod.rs b/nym-api/src/ecash/state/mod.rs index b55117ea1e..27a878f061 100644 --- a/nym-api/src/ecash/state/mod.rs +++ b/nym-api/src/ecash/state/mod.rs @@ -47,6 +47,7 @@ use nym_validator_client::EcashApiClient; use time::ext::NumericalDuration; use time::{Date, Duration, OffsetDateTime}; use tokio::sync::RwLockReadGuard; +use tracing::{debug, error, info, warn}; pub(crate) mod auxiliary; pub(crate) mod bloom; @@ -169,7 +170,7 @@ impl EcashState { }); } - log::info!( + info!( "attempting to establish master coin index signatures for epoch {epoch_id}..." ); @@ -263,7 +264,7 @@ impl EcashState { // because if it was a past epoch we **do** have those keys. // they're just archived - log::error!("received partial coin index signature request for an invalid epoch ({epoch_id}). our key was derived for epoch {}", signing_keys.issued_for_epoch); + error!("received partial coin index signature request for an invalid epoch ({epoch_id}). our key was derived for epoch {}", signing_keys.issued_for_epoch); return Err(EcashError::InvalidSigningKeyEpoch { requested: epoch_id, available: signing_keys.issued_for_epoch, @@ -550,7 +551,7 @@ impl EcashState { pub(crate) async fn accept_proposal(&self, proposal_id: u64) -> Result<()> { //SW NOTE: What to do if this fails if let Err(err) = self.aux.client.vote_proposal(proposal_id, true, None).await { - log::debug!("failed to vote on proposal {proposal_id}: {err}"); + debug!("failed to vote on proposal {proposal_id}: {err}"); } Ok(()) @@ -755,7 +756,7 @@ impl EcashState { // sanity check because this should NEVER happen, // but when it inevitably does, we don't want to crash if spending_date != yesterday { - log::error!("attempted to insert a ticket with spending date of {spending_date} while it's {today} today!!"); + error!("attempted to insert a ticket with spending date of {spending_date} while it's {today} today!!"); } // this shouldn't be happening too often, so it's fine to interact with the storage @@ -771,7 +772,7 @@ impl EcashState { return Ok(guard.insert_global_only(serial_number)); } - log::info!("we need to advance our bloomfilter"); + info!("we need to advance our bloomfilter"); let previous_bitmap = guard.export_today_bitmap(); // archive the BF for today's date diff --git a/nym-api/src/ecash/storage/manager.rs b/nym-api/src/ecash/storage/manager.rs index a77b4dbb20..0e5169fe5c 100644 --- a/nym-api/src/ecash/storage/manager.rs +++ b/nym-api/src/ecash/storage/manager.rs @@ -6,6 +6,7 @@ use crate::ecash::storage::models::{ StoredBloomfilterParams, TicketProvider, VerifiedTicket, }; use crate::support::storage::manager::StorageManager; +use async_trait::async_trait; use nym_coconut_dkg_common::types::EpochId; use nym_ecash_contract_common::deposit::DepositId; use time::{Date, OffsetDateTime}; diff --git a/nym-api/src/ecash/storage/mod.rs b/nym-api/src/ecash/storage/mod.rs index cd7fc26f40..5c15f2b2f6 100644 --- a/nym-api/src/ecash/storage/mod.rs +++ b/nym-api/src/ecash/storage/mod.rs @@ -12,6 +12,7 @@ use crate::ecash::storage::models::{ }; use crate::node_status_api::models::NymApiStorageError; use crate::support::storage::NymApiStorage; +use async_trait::async_trait; use nym_api_requests::ecash::models::Pagination; use nym_coconut_dkg_common::types::EpochId; use nym_compact_ecash::scheme::coin_indices_signatures::AnnotatedCoinIndexSignature; @@ -24,6 +25,7 @@ use nym_crypto::asymmetric::identity; use nym_ecash_contract_common::deposit::DepositId; use nym_validator_client::nyxd::AccountId; use time::{Date, OffsetDateTime}; +use tracing::info; mod helpers; pub(crate) mod manager; diff --git a/nym-api/src/epoch_operations/error.rs b/nym-api/src/epoch_operations/error.rs index a9f0c8ad99..65fc822dff 100644 --- a/nym-api/src/epoch_operations/error.rs +++ b/nym-api/src/epoch_operations/error.rs @@ -2,7 +2,8 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::node_status_api::models::NymApiStorageError; -use nym_mixnet_contract_common::{EpochState, MixId}; +use nym_mixnet_contract_common::nym_node::Role; +use nym_mixnet_contract_common::{EpochState, NodeId}; use nym_validator_client::nyxd::error::NyxdError; use nym_validator_client::nyxd::AccountId; use nym_validator_client::ValidatorClientError; @@ -23,7 +24,10 @@ pub enum RewardingError { }, #[error("it seems the current epoch is in mid-rewarding state (last rewarded is {last_rewarded}). With our current nym-api this shouldn't have been possible. Manual intervention is required.")] - MidMixRewarding { last_rewarded: MixId }, + MidNodeRewarding { last_rewarded: NodeId }, + + #[error("it seems the current epoch is in mid-role assignment state (next role to assign is {next}). With our current nym-api this shouldn't have been possible. Manual intervention is required.")] + MidRoleAssignment { next: Role }, // #[error("There were no mixnodes to reward (network is dead)")] // NoMixnodesToReward, @@ -42,12 +46,16 @@ pub enum RewardingError { #[from] source: std::num::TryFromIntError, }, + #[error("{source}")] WeightedError { #[from] source: rand::distributions::WeightedError, }, + #[error("could not obtain the current interval rewarding parameters")] + RewardingParamsRetrievalFailure, + #[error("{0}")] GenericError(#[from] anyhow::Error), } diff --git a/nym-api/src/epoch_operations/event_reconciliation.rs b/nym-api/src/epoch_operations/event_reconciliation.rs index 47230463c9..302763e68b 100644 --- a/nym-api/src/epoch_operations/event_reconciliation.rs +++ b/nym-api/src/epoch_operations/event_reconciliation.rs @@ -2,11 +2,12 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::epoch_operations::error::RewardingError; -use crate::RewardedSetUpdater; +use crate::EpochAdvancer; use nym_mixnet_contract_common::EpochState; use std::cmp::max; +use tracing::{error, info, warn}; -impl RewardedSetUpdater { +impl EpochAdvancer { pub(super) async fn reconcile_epoch_events(&self) -> Result<(), RewardingError> { let epoch_status = self.nyxd_client.get_current_epoch_status().await?; match epoch_status.state { @@ -18,17 +19,17 @@ impl RewardedSetUpdater { operation: "reconciling epoch events".to_string(), }) } - EpochState::AdvancingEpoch => { + EpochState::RoleAssignment { .. } => { warn!("we seem to have crashed mid epoch operations... no need to reconcile events as we've already done that!"); Ok(()) } EpochState::ReconcilingEvents => { - log::info!("Reconciling all pending epoch events..."); + info!("Reconciling all pending epoch events..."); if let Err(err) = self._reconcile_epoch_events().await { - log::error!("FAILED to reconcile epoch events... - {err}"); + error!("FAILED to reconcile epoch events... - {err}"); Err(err) } else { - log::info!("Reconciled all pending epoch events... SUCCESS"); + info!("Reconciled all pending epoch events... SUCCESS"); Ok(()) } } diff --git a/nym-api/src/epoch_operations/helpers.rs b/nym-api/src/epoch_operations/helpers.rs index 61ea0defeb..a4f5966615 100644 --- a/nym-api/src/epoch_operations/helpers.rs +++ b/nym-api/src/epoch_operations/helpers.rs @@ -1,24 +1,41 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::epoch_operations::RewardedSetUpdater; +use crate::epoch_operations::EpochAdvancer; use cosmwasm_std::{Decimal, Fraction}; -use nym_mixnet_contract_common::reward_params::Performance; -use nym_mixnet_contract_common::{ExecuteMsg, Interval, MixId}; +use nym_mixnet_contract_common::reward_params::{NodeRewardingParameters, Performance, WorkFactor}; +use nym_mixnet_contract_common::{ExecuteMsg, Interval, NodeId, RewardedSet, RewardingParams}; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Copy, Serialize, Deserialize)] -pub(crate) struct MixnodeWithPerformance { - pub(crate) mix_id: MixId, - +pub(crate) struct NodeWithPerformance { + pub(crate) node_id: NodeId, pub(crate) performance: Performance, } -impl From for ExecuteMsg { - fn from(mix_reward: MixnodeWithPerformance) -> Self { - ExecuteMsg::RewardMixnode { - mix_id: mix_reward.mix_id, - performance: mix_reward.performance, +impl NodeWithPerformance { + pub fn with_work(self, work_factor: WorkFactor) -> RewardedNodeWithParams { + RewardedNodeWithParams { + node_id: self.node_id, + params: NodeRewardingParameters { + performance: self.performance, + work_factor, + }, + } + } +} + +#[derive(Debug, Clone, Copy)] +pub struct RewardedNodeWithParams { + pub(crate) node_id: NodeId, + pub(crate) params: NodeRewardingParameters, +} + +impl From for ExecuteMsg { + fn from(node_reward: RewardedNodeWithParams) -> Self { + ExecuteMsg::RewardNode { + node_id: node_reward.node_id, + params: node_reward.params, } } } @@ -37,36 +54,122 @@ pub(super) fn stake_to_f64(stake: Decimal) -> f64 { } } -impl RewardedSetUpdater { - pub(crate) async fn load_performance( +impl EpochAdvancer { + pub(crate) async fn load_mixnode_performance( &self, interval: &Interval, - mix_id: MixId, - ) -> MixnodeWithPerformance { + node_id: NodeId, + ) -> NodeWithPerformance { let uptime = self .storage .get_average_mixnode_uptime_in_the_last_24hrs( - mix_id, + node_id, interval.current_epoch_end_unix_timestamp(), ) .await .unwrap_or_default(); - MixnodeWithPerformance { - mix_id, + NodeWithPerformance { + node_id, performance: uptime.into(), } } - pub(crate) async fn load_nodes_performance( + pub(crate) async fn load_gateway_performance( &self, interval: &Interval, - nodes: &[MixId], - ) -> Vec { - let mut with_performance = Vec::with_capacity(nodes.len()); - for mix_id in nodes { - with_performance.push(self.load_performance(interval, *mix_id).await) + node_id: NodeId, + ) -> NodeWithPerformance { + let uptime = self + .storage + .get_average_gateway_uptime_in_the_last_24hrs( + node_id, + interval.current_epoch_end_unix_timestamp(), + ) + .await + .unwrap_or_default(); + + NodeWithPerformance { + node_id, + performance: uptime.into(), } + } + + pub(crate) async fn load_any_performance( + &self, + interval: &Interval, + node_id: NodeId, + ) -> NodeWithPerformance { + // currently we can't do much better without new network monitor + let mix_performance = self.load_mixnode_performance(interval, node_id).await; + if !mix_performance.performance.is_zero() { + return mix_performance; + } + + self.load_gateway_performance(interval, node_id).await + } + + pub(crate) async fn load_nodes_for_rewarding( + &self, + interval: &Interval, + nodes: &RewardedSet, + // we only need reward parameters for active set work factor and rewarded/active set sizes; + // we do not need exact values of reward pool, staking supply, etc., so it's fine if it's slightly out of sync + global_rewarding_params: RewardingParams, + ) -> Vec { + // currently we are using constant omega for nodes, but that will change with tickets + // or different reward split between entry, exit, etc. at that point this will have to be calculated elsewhere + let active_node_work_factor = global_rewarding_params.active_node_work(); + let standby_node_work_factor = global_rewarding_params.standby_node_work(); + + // SANITY CHECK: + let standby_share = Decimal::from_atomics(nodes.standby.len() as u128, 0).unwrap() + * standby_node_work_factor; + let active_share = Decimal::from_atomics(nodes.active_set_size() as u128, 0).unwrap() + * active_node_work_factor; + let total_work = standby_share + active_share; + + // this HAS TO blow up. there's no recovery + assert!(total_work <= Decimal::one(), "work calculation logic is flawed! somehow the total work in the system is greater than 1!"); + + let mut with_performance = Vec::with_capacity(nodes.rewarded_set_size()); + + // all the active set mixnodes + for node_id in nodes + .layer1 + .iter() + .chain(nodes.layer2.iter()) + .chain(nodes.layer3.iter()) + { + with_performance.push( + self.load_mixnode_performance(interval, *node_id) + .await + .with_work(active_node_work_factor), + ) + } + + // all the active set gateways + for node_id in nodes + .entry_gateways + .iter() + .chain(nodes.exit_gateways.iter()) + { + with_performance.push( + self.load_gateway_performance(interval, *node_id) + .await + .with_work(active_node_work_factor), + ) + } + + // all the standby nodes + for node_id in &nodes.standby { + with_performance.push( + self.load_any_performance(interval, *node_id) + .await + .with_work(standby_node_work_factor), + ) + } + with_performance } } diff --git a/nym-api/src/epoch_operations/mod.rs b/nym-api/src/epoch_operations/mod.rs index 952bb5ca1f..e7ccdf7240 100644 --- a/nym-api/src/epoch_operations/mod.rs +++ b/nym-api/src/epoch_operations/mod.rs @@ -1,7 +1,7 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -// there is couple of reasons for putting this in a separate module: +// there is a couple of reasons for putting this in a separate module: // 1. I didn't feel it fit well in nym contract "cache". It seems like purpose of cache is to just keep updating local data // rather than attempting to change global view (i.e. the active set) // @@ -12,17 +12,20 @@ // 3. Eventually this whole procedure is going to get expanded to allow for distribution of rewarded set generation // and hence this might be a good place for it. +use crate::node_describe_cache::DescribedNodes; use crate::node_status_api::ONE_DAY; use crate::nym_contract_cache::cache::NymContractCache; +use crate::support::caching::cache::SharedCache; use crate::support::nyxd::Client; use crate::support::storage::NymApiStorage; use error::RewardingError; -pub(crate) use helpers::MixnodeWithPerformance; +pub(crate) use helpers::RewardedNodeWithParams; use nym_mixnet_contract_common::{CurrentIntervalResponse, Interval}; use nym_task::{TaskClient, TaskManager}; use std::collections::HashSet; use std::time::Duration; use tokio::time::sleep; +use tracing::{error, info, trace, warn}; pub(crate) mod error; mod event_reconciliation; @@ -31,13 +34,16 @@ mod rewarded_set_assignment; mod rewarding; mod transition_beginning; -pub struct RewardedSetUpdater { +// naming things is difficult, ok? +// this is struct responsible for advancing an epoch +pub struct EpochAdvancer { nyxd_client: Client, nym_contract_cache: NymContractCache, + described_cache: SharedCache, storage: NymApiStorage, } -impl RewardedSetUpdater { +impl EpochAdvancer { pub(crate) async fn current_interval_details( &self, ) -> Result { @@ -47,71 +53,77 @@ impl RewardedSetUpdater { pub(crate) fn new( nyxd_client: Client, nym_contract_cache: NymContractCache, + described_cache: SharedCache, storage: NymApiStorage, ) -> Self { - RewardedSetUpdater { + EpochAdvancer { nyxd_client, nym_contract_cache, + described_cache, storage, } } #[allow(clippy::doc_lazy_continuation)] // This is where the epoch gets advanced, and all epoch related transactions originate - /// Upon each epoch having finished the following actions are executed by this nym-api: - /// 1. it computes the rewards for each node using the ephemera channel for the epoch that - /// ended - /// 2. it queries the mixnet contract to check the current `EpochState` in order to figure out whether - /// a different nym-api has already started epoch transition (not yet applicable) - /// 3. it sends a `BeginEpochTransition` message to the mixnet contract causing the following to happen: - /// - if successful, the address of this validator is going to be saved as being responsible for progressing this epoch. - /// What it means in practice is that once we have multiple instances of nym-api running, - /// only this one will try to perform the rest of the actions. It will also allow it to - /// more easily recover in case of crashes. - /// - the `EpochState` changes to `Rewarding`, meaning the nym-api will now be allowed to send - /// `RewardMixnode` transactions. However, it's not going to be able anything else like `ReconcileEpochEvents` - /// until that is done. - /// - ability to send transactions (by other users) that get resolved once given epoch/interval rolls over, - /// such as `BondMixnode` or `DelegateToMixnode` will temporarily be frozen until the entire procedure is finished. - /// 4. it obtains the current rewarded set and for each node in there (**SORTED BY MIX_ID!!**), - /// it sends (in a single batch) `RewardMixnode` message with the measured performance. - /// Once the final message gets executed, the mixnet contract automatically transitions - /// the state to `ReconcilingEvents`. - /// 5. it obtains the number of pending epoch and interval events and repeatedly sends - /// `ReconcileEpochEvents` transaction until all of them are resolved. - /// At this point the mixnet contract automatically transitions the state to `AdvancingEpoch`. - /// 6. it obtains the list of all nodes on the network and pseudorandomly (but weighted by total stake) - /// determines the new rewarded set. It then assigns layers to the provided nodes taking - /// family information into consideration. Finally it sends `AdvanceCurrentEpoch` message - /// containing the set and layer information thus rolling over the epoch and changing the state - /// to `InProgress`. - /// 7. it purges old (older than 48h) measurement data - /// 8. the whole process repeats once the new epoch finishes + // TODO: make sure this is still up to date + // /// Upon each epoch having finished the following actions are executed by this nym-api: + // /// 1. it computes the rewards for each node using the ephemera channel for the epoch that + // /// ended + // /// 2. it queries the mixnet contract to check the current `EpochState` in order to figure out whether + // /// a different nym-api has already started epoch transition (not yet applicable) + // /// 3. it sends a `BeginEpochTransition` message to the mixnet contract causing the following to happen: + // /// - if successful, the address of this validator is going to be saved as being responsible for progressing this epoch. + // /// What it means in practice is that once we have multiple instances of nym-api running, + // /// only this one will try to perform the rest of the actions. It will also allow it to + // /// more easily recover in case of crashes. + // /// - the `EpochState` changes to `Rewarding`, meaning the nym-api will now be allowed to send + // /// `RewardNode` transactions. However, it's not going to be able anything else like `ReconcileEpochEvents` + // /// until that is done. + // /// - ability to send transactions (by other users) that get resolved once given epoch/interval rolls over, + // /// such as `BondMixnode` or `DelegateToMixnode` will temporarily be frozen until the entire procedure is finished. + // /// 4. it obtains the current rewarded set and for each node in there (**SORTED BY NODE_ID!!**), + // /// it sends (in a single batch) `RewardMixnode` message with the measured performance. + // /// Once the final message gets executed, the mixnet contract automatically transitions + // /// the state to `ReconcilingEvents`. + // /// 5. it obtains the number of pending epoch and interval events and repeatedly sends + // /// `ReconcileEpochEvents` transaction until all of them are resolved. + // /// At this point the mixnet contract automatically transitions the state to `AdvancingEpoch`. + // /// 6. it obtains the list of all nodes on the network and pseudorandomly (but weighted by total stake) + // /// determines the new rewarded set. It then assigns roles to the provided nodes taking + // /// family information into consideration. Finally, it sends `AssignRole` message + // /// containing the role assignment information thus (after each role has been assigned) + // /// rolling over the epoch and changing the state to `InProgress`. + // /// 7. it purges old (older than 48h) measurement data + // /// 8. the whole process repeats once the new epoch finishes async fn perform_epoch_operations(&mut self, interval: Interval) -> Result<(), RewardingError> { - let mut rewards = self.nodes_to_reward(interval).await; - rewards.sort_by_key(|a| a.mix_id); + let mut rewards = self.nodes_to_reward(interval).await?; + rewards.sort_by_key(|a| a.node_id); - log::info!("The current epoch has finished."); - log::info!( + info!("The current epoch has finished."); + info!( "Interval id: {}, epoch id: {} (absolute epoch id: {})", interval.current_interval_id(), interval.current_epoch_id(), interval.current_epoch_absolute_id() ); - log::info!( + info!( "The current epoch has lasted from {} until {}", interval.current_epoch_start(), interval.current_epoch_end() ); - log::info!("Performing all epoch operations..."); + info!("Performing all epoch operations..."); let epoch_end = interval.current_epoch_end(); - let all_mixnodes = self.nym_contract_cache.mixnodes_filtered().await; - if all_mixnodes.is_empty() { - // that's a bit weird, but - log::warn!("there don't seem to be any mixnodes on the network!") + let legacy_mixnodes = self.nym_contract_cache.legacy_mixnodes_filtered().await; + let legacy_gateways = self.nym_contract_cache.legacy_gateways_filtered().await; + let nym_nodes = self.nym_contract_cache.nym_nodes_filtered().await; + + if legacy_mixnodes.is_empty() && legacy_gateways.is_empty() && nym_nodes.is_empty() { + // that's a bit weird, but ok + warn!("there don't seem to be any nodes on the network!") } let epoch_status = self.nyxd_client.get_current_epoch_status().await?; @@ -133,23 +145,33 @@ impl RewardedSetUpdater { } // Reward all the nodes in the still current, soon to be previous rewarded set - log::info!("Rewarding the current rewarded set..."); - self.reward_current_rewarded_set(&rewards, interval).await?; + info!("Rewarding the current rewarded set..."); + self.reward_current_rewarded_set(rewards, interval).await?; // note: those operations don't really have to be atomic, so it's fine to send them // as separate transactions self.reconcile_epoch_events().await?; - self.update_rewarded_set_and_advance_epoch(interval, &all_mixnodes) - .await?; + self.update_rewarded_set_and_advance_epoch( + interval, + &legacy_mixnodes, + &legacy_gateways, + &nym_nodes, + ) + .await?; - log::info!("Purging old node statuses from the storage..."); + info!("Purging old node statuses from the storage..."); let cutoff = (epoch_end - 2 * ONE_DAY).unix_timestamp(); self.storage.purge_old_statuses(cutoff).await?; Ok(()) } - async fn update_blacklist(&mut self, interval: &Interval) -> Result<(), RewardingError> { + // this purposely does not deal with nym-nodes as they don't have a concept of a blacklist. + // instead clients are meant to be filtering out them themselves based on the provided scores. + async fn update_legacy_node_blacklist( + &mut self, + interval: &Interval, + ) -> Result<(), RewardingError> { info!("Updating blacklists"); let mut mix_blacklist_add = HashSet::new(); @@ -183,15 +205,16 @@ impl RewardedSetUpdater { for gateway in gateways { if gateway.value() <= 50.0 { - gate_blacklist_add.insert(gateway.identity().to_string()); + gate_blacklist_add.insert(gateway.node_id()); } else { - gate_blacklist_remove.insert(gateway.identity().to_string()); + gate_blacklist_remove.insert(gateway.node_id()); } } self.nym_contract_cache .update_gateways_blacklist(gate_blacklist_add, gate_blacklist_remove) .await; + Ok(()) } @@ -219,7 +242,7 @@ impl RewardedSetUpdater { return Some(current_interval.interval); } else { let time_left = current_interval.time_until_current_epoch_end(); - log::info!( + info!( "Waiting for epoch change, it should take approximately {}s", time_left.as_secs() ); @@ -244,15 +267,19 @@ impl RewardedSetUpdater { } pub(crate) async fn run(&mut self, mut shutdown: TaskClient) -> Result<(), RewardingError> { + info!("waiting for initial contract cache values before we can start rewarding"); self.nym_contract_cache.wait_for_initial_values().await; + info!("waiting for initial self-described cache values before we can start rewarding"); + self.described_cache.naive_wait_for_initial_values().await; + while !shutdown.is_shutdown() { let interval_details = match self.wait_until_epoch_end(&mut shutdown).await { // received a shutdown None => return Ok(()), Some(interval) => interval, }; - if let Err(err) = self.update_blacklist(&interval_details).await { + if let Err(err) = self.update_legacy_node_blacklist(&interval_details).await { error!("failed to update the node blacklist - {err}"); continue; } @@ -268,11 +295,16 @@ impl RewardedSetUpdater { pub(crate) fn start( nyxd_client: Client, nym_contract_cache: &NymContractCache, - storage: NymApiStorage, + described_cache: SharedCache, + storage: &NymApiStorage, shutdown: &TaskManager, ) { - let mut rewarded_set_updater = - RewardedSetUpdater::new(nyxd_client, nym_contract_cache.to_owned(), storage); + let mut rewarded_set_updater = EpochAdvancer::new( + nyxd_client, + nym_contract_cache.to_owned(), + described_cache, + storage.to_owned(), + ); let shutdown_listener = shutdown.subscribe(); tokio::spawn(async move { rewarded_set_updater.run(shutdown_listener).await }); } diff --git a/nym-api/src/epoch_operations/rewarded_set_assignment.rs b/nym-api/src/epoch_operations/rewarded_set_assignment.rs index 08a794768c..de5e3b0f92 100644 --- a/nym-api/src/epoch_operations/rewarded_set_assignment.rs +++ b/nym-api/src/epoch_operations/rewarded_set_assignment.rs @@ -3,26 +3,38 @@ use crate::epoch_operations::error::RewardingError; use crate::epoch_operations::helpers::stake_to_f64; -use crate::RewardedSetUpdater; +use crate::EpochAdvancer; use cosmwasm_std::Decimal; -use nym_mixnet_contract_common::families::FamilyHead; -use nym_mixnet_contract_common::reward_params::Performance; -use nym_mixnet_contract_common::{ - EpochState, IdentityKey, Interval, Layer, LayerAssignment, MixId, MixNodeDetails, -}; +use nym_api_requests::legacy::{LegacyGatewayBondWithId, LegacyMixNodeDetailsWithLayer}; +use nym_mixnet_contract_common::helpers::IntoBaseDecimal; +use nym_mixnet_contract_common::reward_params::{Performance, RewardedSetParams}; +use nym_mixnet_contract_common::{EpochState, Interval, NodeId, NymNodeDetails, RewardedSet}; use rand::prelude::SliceRandom; use rand::rngs::OsRng; -use std::collections::HashMap; +use std::collections::HashSet; +use tracing::{debug, error, info, warn}; + +#[derive(Debug, Clone, PartialEq)] +enum AvailableRole { + // legacy mixnodes + nym-nodes in mixing mode + Mix, + + // legacy gateways + nym-nodes in entry or exit mode + EntryGateway, + + // nym-nodes in exit mode + ExitGateway, +} #[derive(Debug, Clone)] -struct MixnodeWithStakeAndPerformance { - mix_id: MixId, - identity: IdentityKey, +struct NodeWithStakeAndPerformance { + node_id: NodeId, + available_roles: Vec, total_stake: Decimal, performance: Performance, } -impl MixnodeWithStakeAndPerformance { +impl NodeWithStakeAndPerformance { fn to_selection_weight(&self) -> f64 { let scaled_performance = match self.performance.checked_pow(20) { Ok(perf) => perf, @@ -35,150 +47,281 @@ impl MixnodeWithStakeAndPerformance { let scaled_stake = self.total_stake * scaled_performance; stake_to_f64(scaled_stake) } -} -impl RewardedSetUpdater { - // Needs to run for active and reserve sets separatley, as it does not preserve order - async fn determine_layers( - &self, - set: &[MixnodeWithStakeAndPerformance], - ) -> Result, RewardingError> { - let mut assignments = Vec::with_capacity(set.len()); - let target_layer_count = set.len() / 3; - - let mix_to_family = self.nym_contract_cache.mix_to_family().await.to_vec(); - - let mix_to_family = mix_to_family - .into_iter() - .collect::>(); - - let mut regular_nodes = Vec::with_capacity(set.len()); - - let mut families = HashMap::new(); - - for node in set.iter() { - if let Some(fh) = mix_to_family.get(&node.identity) { - let family: &mut Vec = families.entry(fh.identity()).or_default(); - family.push(node.mix_id) - } else { - regular_nodes.push(node.mix_id) - } - } - - let mut layers = HashMap::new(); - layers.insert(Layer::One, Vec::with_capacity(target_layer_count)); - layers.insert(Layer::Two, Vec::with_capacity(target_layer_count)); - layers.insert(Layer::Three, Vec::with_capacity(target_layer_count)); - - // Assign all members of a family to same layer - for (_head, members) in families.iter_mut() { - let smallest_layer = layers - .iter() - .min_by_key(|(_layer, members)| members.len()) - .map(|(layer, _members)| *layer) - .unwrap_or(Layer::One); - - let entry = layers.entry(smallest_layer).or_default(); - if entry.len() + members.len() <= target_layer_count { - entry.extend_from_slice(members) - } - } - - // Assign nodes with no families into layers - for mix_id in regular_nodes.drain(..) { - let smallest_layer = layers - .iter() - .min_by_key(|(_layer, members)| members.len()) - .map(|(layer, _members)| *layer) - .unwrap_or(Layer::One); - - let entry = layers.entry(smallest_layer).or_default(); - if entry.len() < target_layer_count { - entry.push(mix_id) - } - } - - for (layer, members) in layers { - let layer_assignments = members - .into_iter() - .map(|mix_id| LayerAssignment::new(mix_id, layer)); - assignments.extend(layer_assignments); - } - Ok(assignments) + fn can_operate_mixnode(&self) -> bool { + self.available_roles.contains(&AvailableRole::Mix) } + fn can_operate_entry_gateway(&self) -> bool { + self.available_roles.contains(&AvailableRole::EntryGateway) + } + + fn can_operate_exit_gateway(&self) -> bool { + self.available_roles.contains(&AvailableRole::ExitGateway) + } +} + +impl EpochAdvancer { fn determine_rewarded_set( &self, - mixnodes: Vec, - nodes_to_select: u32, - ) -> Result, RewardingError> { - if mixnodes.is_empty() { - return Ok(Vec::new()); + nodes: Vec, + spec: RewardedSetParams, + ) -> Result { + if nodes.is_empty() { + warn!("there are no nodes for assignment!"); + return Ok(RewardedSet::default()); } let mut rng = OsRng; - // generate list of mixnodes and their relatively weight (by total stake) - let choices = mixnodes + // generate list of nodes and their relatively weight (by total stake scaled by performance) + let all_choices = nodes .into_iter() - .map(|mix| { - let weight = mix.to_selection_weight(); - (mix, weight) + .map(|node| { + let weight = node.to_selection_weight(); + (node, weight) }) .collect::>(); - // the unwrap here is fine as an error can only be thrown under one of the following conditions: - // - our mixnode list is empty - we have already checked for that - // - we have invalid weights, i.e. less than zero or NaNs - it shouldn't happen in our case as we safely cast down from u128 - // - all weights are zero - it's impossible in our case as the list of nodes is not empty and weight is proportional to stake. You must have non-zero stake in order to bond - // - we have more than u32::MAX values (which is incredibly unrealistic to have 4B mixnodes bonded... literally every other person on the planet would need one) - Ok(choices - .choose_multiple_weighted(&mut rng, nodes_to_select as usize, |item| item.1)? - .map(|(mix, _weight)| mix.clone()) - .collect()) + // 1. determine entry gateways + let entry_eligible = all_choices + .iter() + .filter(|node| node.0.can_operate_entry_gateway()) + .collect::>(); + let entry_gateways = entry_eligible + .choose_multiple_weighted(&mut rng, spec.entry_gateways as usize, |item| item.1)? + .map(|node| node.0.node_id) + .collect::>(); + + // 2. determine exit gateways + let exit_eligible = all_choices + .iter() + .filter(|node| { + node.0.can_operate_exit_gateway() && !entry_gateways.contains(&node.0.node_id) + }) + .collect::>(); + let exit_gateways = exit_eligible + .choose_multiple_weighted(&mut rng, spec.exit_gateways as usize, |item| item.1)? + .map(|node| node.0.node_id) + .collect::>(); + + // 3. determine mixnodes + let mix_eligible = all_choices + .iter() + .filter(|node| { + node.0.can_operate_mixnode() + && !exit_gateways.contains(&node.0.node_id) + && !entry_gateways.contains(&node.0.node_id) + }) + .collect::>(); + let mixnodes = mix_eligible + .choose_multiple_weighted(&mut rng, spec.mixnodes as usize, |item| item.1)? + .map(|node| node.0.node_id) + .collect::>(); + + // 4. determine standby + let standby_eligible = all_choices + .iter() + .filter(|node| { + exit_gateways.contains(&node.0.node_id) + && !entry_gateways.contains(&node.0.node_id) + && !mixnodes.contains(&node.0.node_id) + }) + .collect::>(); + let standby = standby_eligible + .choose_multiple_weighted(&mut rng, spec.standby as usize, |item| item.1)? + .map(|node| node.0.node_id) + .collect::>(); + + // 5. split mixnodes into the layers: just shuffle the selected nodes and select every 3rd into each layer + let mut mixnodes_vec = mixnodes.into_iter().collect::>(); + mixnodes_vec.shuffle(&mut rng); + + let mut layer1 = Vec::new(); + let mut layer2 = Vec::new(); + let mut layer3 = Vec::new(); + + for (i, mix) in mixnodes_vec.iter().enumerate() { + match i % 3 { + 0 => layer1.push(*mix), + 1 => layer2.push(*mix), + 2 => layer3.push(*mix), + n => panic!("we have broken maths! somehow {i} % 3 == {n}!"), + } + } + + if entry_gateways.len() != spec.entry_gateways as usize { + warn!( + "we didn't manage to select {} entry gateways. we only got {}", + spec.entry_gateways, + entry_gateways.len() + ) + } + + if exit_gateways.len() != spec.exit_gateways as usize { + warn!( + "we didn't manage to select {} exit gateways. we only got {}", + spec.exit_gateways, + exit_gateways.len() + ) + } + + if mixnodes_vec.len() != spec.mixnodes as usize { + warn!( + "we didn't manage to select {} mixnodes. we only got {}", + spec.mixnodes, + mixnodes_vec.len() + ) + } + + if standby.len() != spec.standby as usize { + warn!( + "we didn't manage to select {} standby nodes. we only got {}", + spec.standby, + standby.len() + ) + } + + Ok(RewardedSet { + entry_gateways: entry_gateways.into_iter().collect(), + exit_gateways: exit_gateways.into_iter().collect(), + layer1, + layer2, + layer3, + standby, + }) } async fn attach_performance( &self, interval: Interval, - mixnodes: &[MixNodeDetails], - ) -> Vec { - let mut with_performance = Vec::with_capacity(mixnodes.len()); - for mix in mixnodes { - with_performance.push(MixnodeWithStakeAndPerformance { - mix_id: mix.mix_id(), - identity: mix.bond_information.identity().to_owned(), - total_stake: mix.total_stake(), - performance: self - .load_performance(&interval, mix.mix_id()) - .await - .performance, + legacy_mixnodes: &[LegacyMixNodeDetailsWithLayer], + legacy_gateways: &[LegacyGatewayBondWithId], + nym_nodes: &[NymNodeDetails], + ) -> Vec { + let mut with_performance = + Vec::with_capacity(legacy_mixnodes.len() + legacy_gateways.len() + nym_nodes.len()); + + for mix in legacy_mixnodes { + let total_stake = mix.total_stake(); + let performance = self + .load_mixnode_performance(&interval, mix.mix_id()) + .await + .performance; + debug!( + "legacy mixnode {}: stake: {total_stake}, performance: {performance}", + mix.mix_id() + ); + + with_performance.push(NodeWithStakeAndPerformance { + node_id: mix.mix_id(), + available_roles: vec![AvailableRole::Mix], + total_stake, + performance, }) } + for gateway in legacy_gateways { + let total_stake = gateway + .bond + .pledge_amount + .amount + .into_base_decimal() + .unwrap_or_default(); + let performance = self + .load_gateway_performance(&interval, gateway.node_id) + .await + .performance; + debug!( + "legacy gateway {}: stake: {total_stake}, performance: {performance}", + gateway.node_id + ); + + with_performance.push(NodeWithStakeAndPerformance { + node_id: gateway.node_id, + available_roles: vec![AvailableRole::EntryGateway], + total_stake, + performance, + }) + } + + // SAFETY: the cache MUST HAVE been initialised before now + let described_cache = self.described_cache.get().await.unwrap(); + + for nym_node in nym_nodes { + let node_id = nym_node.node_id(); + let total_stake = nym_node.total_stake(); + let performance = self + .load_any_performance(&interval, nym_node.node_id()) + .await + .performance; + debug!("nym-node {node_id}: stake: {total_stake}, performance: {performance}",); + + let Some(self_described) = described_cache.get_description(&node_id) else { + warn!("we don't have self described information for nym-node {node_id}. it is not going to get considered for rewarded set selection."); + continue; + }; + + let mut available_roles = Vec::new(); + if self_described.declared_role.can_operate_mixnode() { + available_roles.push(AvailableRole::Mix) + } + if self_described.declared_role.can_operate_entry_gateway() { + available_roles.push(AvailableRole::EntryGateway) + } + if self_described.declared_role.can_operate_exit_gateway() { + available_roles.push(AvailableRole::ExitGateway) + } + + if available_roles.is_empty() { + warn!("nym-node {node_id} can't operate under any mode!"); + continue; + } + + with_performance.push(NodeWithStakeAndPerformance { + node_id: nym_node.node_id(), + available_roles, + total_stake, + performance, + }) + } + with_performance } pub(super) async fn update_rewarded_set_and_advance_epoch( &self, current_interval: Interval, - all_mixnodes: &[MixNodeDetails], + legacy_mixnodes: &[LegacyMixNodeDetailsWithLayer], + legacy_gateways: &[LegacyGatewayBondWithId], + nym_nodes: &[NymNodeDetails], ) -> Result<(), RewardingError> { let epoch_status = self.nyxd_client.get_current_epoch_status().await?; match epoch_status.state { - EpochState::AdvancingEpoch => { - log::info!("Advancing epoch and updating the rewarded set..."); + EpochState::RoleAssignment { next } => { + // with how the nym-api is currently coded, this should never happen as we're always + // assigning roles to ALL nodes at once, but who knows what we might decide to do in the future... + if !next.is_first() { + return Err(RewardingError::MidRoleAssignment { next }); + } + + info!("attempting to assign the rewarded set for the upcoming epoch..."); let nodes_with_performance = self - .attach_performance(current_interval, all_mixnodes) + .attach_performance( + current_interval, + legacy_mixnodes, + legacy_gateways, + nym_nodes, + ) .await; if let Err(err) = self ._update_rewarded_set_and_advance_epoch(nodes_with_performance) .await { - log::error!("FAILED to advance the current epoch... - {err}"); + error!("FAILED to assign the rewarded set... - {err}"); Err(err) } else { - log::info!("Advanced the epoch and updated the rewarded set... SUCCESS"); + info!("Advanced the epoch and updated the rewarded set... SUCCESS"); Ok(()) } } @@ -195,54 +338,21 @@ impl RewardedSetUpdater { async fn _update_rewarded_set_and_advance_epoch( &self, - all_mixnodes: Vec, + all_nodes: Vec, ) -> Result<(), RewardingError> { // we grab rewarding parameters here as they might have gotten updated when performing epoch actions let rewarding_parameters = self.nyxd_client.get_current_rewarding_parameters().await?; - debug!("Rewarding paremeters: {:?}", rewarding_parameters); + debug!("Rewarding parameters: {rewarding_parameters:?}"); let new_rewarded_set = - self.determine_rewarded_set(all_mixnodes, rewarding_parameters.rewarded_set_size)?; + self.determine_rewarded_set(all_nodes, rewarding_parameters.rewarded_set)?; debug!("New rewarded set: {:?}", new_rewarded_set); - let empty = vec![]; - - let (active_set, reserve_set) = if new_rewarded_set.len() - <= rewarding_parameters.active_set_size as usize - { - warn!("Active set size ({}) is greater then rewarded set len ({}), there will be no reserve set", rewarding_parameters.active_set_size, new_rewarded_set.len()); - (new_rewarded_set.as_slice(), empty.as_slice()) - } else { - new_rewarded_set.split_at(rewarding_parameters.active_set_size as usize) - }; - - let mut active_set_layer_assignments = self.determine_layers(active_set).await?; - debug!( - "Active set layer assignments: {:?}", - active_set_layer_assignments - ); - let reserve_set_layer_assignments = self.determine_layers(reserve_set).await?; - debug!( - "Reserve set layer assignments: {:?}", - reserve_set_layer_assignments - ); - - active_set_layer_assignments.extend(reserve_set_layer_assignments); - - debug!( - "Rewarded set layer assignments: {:?}", - active_set_layer_assignments - ); - self.nyxd_client - .advance_current_epoch( - active_set_layer_assignments, - rewarding_parameters.active_set_size, - ) + .send_role_assignment_messages(new_rewarded_set) .await?; - Ok(()) } } diff --git a/nym-api/src/epoch_operations/rewarding.rs b/nym-api/src/epoch_operations/rewarding.rs index 2f8ae6160b..624ec520b4 100644 --- a/nym-api/src/epoch_operations/rewarding.rs +++ b/nym-api/src/epoch_operations/rewarding.rs @@ -2,14 +2,15 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::epoch_operations::error::RewardingError; -use crate::epoch_operations::helpers::MixnodeWithPerformance; -use crate::RewardedSetUpdater; -use nym_mixnet_contract_common::{EpochState, Interval, MixId}; +use crate::epoch_operations::helpers::RewardedNodeWithParams; +use crate::EpochAdvancer; +use nym_mixnet_contract_common::{EpochState, Interval}; +use tracing::{error, info, warn}; -impl RewardedSetUpdater { +impl EpochAdvancer { pub(super) async fn reward_current_rewarded_set( &self, - to_reward: &[MixnodeWithPerformance], + to_reward: Vec, current_interval: Interval, ) -> Result<(), RewardingError> { let epoch_status = self.nyxd_client.get_current_epoch_status().await?; @@ -22,27 +23,27 @@ impl RewardedSetUpdater { operation: "mix rewarding".to_string(), }) } - EpochState::ReconcilingEvents | EpochState::AdvancingEpoch => { - warn!("we seem to have crashed mid epoch operations... no need to reward mixnodes as we've already done that! (or this could be a false positive if there were no nodes to reward - to fix this warning later)"); + EpochState::ReconcilingEvents | EpochState::RoleAssignment { .. } => { + warn!("we seem to have crashed mid epoch operations... no need to reward nodes as we've already done that! (or this could be a false positive if there were no nodes to reward - to fix this warning later)"); Ok(()) } EpochState::Rewarding { last_rewarded, .. } => { - log::info!("Rewarding the current rewarded set..."); + info!("Rewarding the current rewarded set..."); // with how the nym-api is currently coded, this should never happen as we're always - // rewarding ALL mixnodes at once, but who knows what we might decide to do in the future... + // rewarding ALL nodes at once, but who knows what we might decide to do in the future... if last_rewarded != 0 { - return Err(RewardingError::MidMixRewarding { last_rewarded }); + return Err(RewardingError::MidNodeRewarding { last_rewarded }); } if let Err(err) = self ._reward_current_rewarded_set(to_reward, current_interval) .await { - log::error!("FAILED to reward rewarded set - {err}"); + error!("FAILED to reward rewarded set: {err}"); Err(err) } else { - log::info!("Rewarded current rewarded set... SUCCESS"); + info!("Rewarded current rewarded set... SUCCESS"); Ok(()) } } @@ -51,41 +52,55 @@ impl RewardedSetUpdater { async fn _reward_current_rewarded_set( &self, - to_reward: &[MixnodeWithPerformance], + to_reward: Vec, current_interval: Interval, ) -> Result<(), RewardingError> { if to_reward.is_empty() { error!("There are no nodes to reward in this epoch - we shouldn't have been in the 'Rewarding' state!"); - } else if let Err(err) = self.nyxd_client.send_rewarding_messages(to_reward).await { + } else if let Err(err) = self.nyxd_client.send_rewarding_messages(&to_reward).await { error!( - "failed to perform mixnode rewarding for epoch {}! Error encountered: {err}", + "failed to perform node rewarding for epoch {}! Error encountered: {err}", current_interval.current_epoch_absolute_id(), ); return Err(err.into()); } - log::info!("rewarded {} mixnodes...", to_reward.len()); + info!("rewarded {} nodes...", to_reward.len()); Ok(()) } - pub(crate) async fn nodes_to_reward(&self, interval: Interval) -> Vec { - // try to get current up to date view of the network bypassing the cache + pub(crate) async fn nodes_to_reward( + &self, + interval: Interval, + ) -> Result, RewardingError> { + // try to get current up-to-date view of the network bypassing the cache // in case the epochs were significantly shortened for the purposes of testing - let rewarded_set: Vec = match self.nyxd_client.get_rewarded_set_mixnodes().await { - Ok(nodes) => nodes.into_iter().map(|(id, _)| id).collect::>(), + let rewarded_set = match self.nyxd_client.get_rewarded_set_nodes().await { + Ok(rewarded_set) => rewarded_set, Err(err) => { - warn!("failed to obtain the current rewarded set - {err}. falling back to the cached version"); + warn!("failed to obtain the current rewarded set: {err}. falling back to the cached version"); self.nym_contract_cache - .rewarded_set() + .rewarded_set_owned() .await .into_inner() - .into_iter() - .map(|node| node.mix_id()) - .collect::>() } }; - self.load_nodes_performance(&interval, &rewarded_set).await + // we only need reward parameters for active set work factor and rewarded/active set sizes; + // we do not need exact values of reward pool, staking supply, etc., so it's fine if it's slightly out of sync + let Some(reward_params) = self + .nym_contract_cache + .interval_reward_params() + .await + .into_inner() + else { + error!("failed to obtain the current interval rewarding parameters. can't determine rewards without them"); + return Err(RewardingError::RewardingParamsRetrievalFailure); + }; + + Ok(self + .load_nodes_for_rewarding(&interval, &rewarded_set, reward_params) + .await) } } diff --git a/nym-api/src/epoch_operations/transition_beginning.rs b/nym-api/src/epoch_operations/transition_beginning.rs index f6e8f87b6a..b57779dfca 100644 --- a/nym-api/src/epoch_operations/transition_beginning.rs +++ b/nym-api/src/epoch_operations/transition_beginning.rs @@ -2,9 +2,10 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::epoch_operations::error::RewardingError; -use crate::epoch_operations::RewardedSetUpdater; +use crate::epoch_operations::EpochAdvancer; +use tracing::{error, info}; -impl RewardedSetUpdater { +impl EpochAdvancer { // returns boolean indicating whether we should bother continuing pub(super) async fn begin_epoch_transition(&self) -> Result { info!("starting the epoch transition..."); @@ -13,14 +14,14 @@ impl RewardedSetUpdater { // wasn't faster than us let epoch_status = self.nyxd_client.get_current_epoch_status().await?; if !epoch_status.is_in_progress() { - log::error!("FAILED to begin epoch progression: {err}"); + error!("FAILED to begin epoch progression: {err}"); Err(err) } else { error!("another nym-api ({}) is already advancing the epoch... but we shouldn't have other nym-apis yet!", epoch_status.being_advanced_by); Ok(false) } } else { - log::info!("Begun epoch transition... SUCCESS"); + info!("Begun epoch transition... SUCCESS"); Ok(true) } } diff --git a/nym-api/src/main.rs b/nym-api/src/main.rs index 3121ce18f2..8534879327 100644 --- a/nym-api/src/main.rs +++ b/nym-api/src/main.rs @@ -1,36 +1,16 @@ // Copyright 2020-2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -// TODO rocket remove -#![allow(deprecated)] - -#[macro_use] -extern crate rocket; - -use crate::ecash::dkg::controller::keys::{ - can_validate_coconut_keys, load_bte_keypair, load_ecash_keypair_if_exists, -}; -use crate::epoch_operations::RewardedSetUpdater; -use crate::network::models::NetworkDetails; -use crate::node_describe_cache::DescribedNodes; -use crate::node_status_api::uptime_updater::HistoricalUptimeUpdater; -use crate::support::caching::cache::SharedCache; +use crate::epoch_operations::EpochAdvancer; use crate::support::cli; -use crate::support::config::Config; use crate::support::storage; -use crate::support::storage::NymApiStorage; use ::nym_config::defaults::setup_env; -use circulating_supply_api::cache::CirculatingSupplyCache; use clap::Parser; -use ecash::dkg::controller::DkgController; use node_status_api::NodeStatusCache; -use nym_bin_common::logging::setup_logging; -use nym_config::defaults::NymNetworkDetails; +use nym_bin_common::logging::setup_tracing_logger; use nym_contract_cache::cache::NymContractCache; -use nym_sphinx::receiver::SphinxMessageReceiver; -use nym_task::TaskManager; -use rand::rngs::OsRng; -use support::{http, nyxd}; +use support::nyxd; +use tracing::{info, trace}; mod circulating_supply_api; mod ecash; @@ -43,14 +23,7 @@ pub(crate) mod nym_contract_cache; pub(crate) mod nym_nodes; mod status; pub(crate) mod support; - -#[cfg(feature = "axum")] -mod v2; - -struct ShutdownHandles { - task_manager_handle: TaskManager, - rocket_handle: rocket::Shutdown, -} +mod v3_migration; // TODO rocket: remove all such Todos once rocket is phased out completely #[tokio::main] @@ -60,143 +33,13 @@ async fn main() -> Result<(), anyhow::Error> { console_subscriber::init(); }} - setup_logging(); - // TODO rocket: replace with tracing logger once rocket is eliminated from code + setup_tracing_logger(); info!("Starting nym api..."); let args = cli::Cli::parse(); - trace!("{:#?}", args); + trace!("args: {:#?}", args); setup_env(args.config_env_file.as_ref()); args.execute().await } - -async fn start_nym_api_tasks(config: Config) -> anyhow::Result { - let nyxd_client = nyxd::Client::new(&config); - let connected_nyxd = config.get_nyxd_url(); - let nym_network_details = NymNetworkDetails::new_from_env(); - let network_details = NetworkDetails::new(connected_nyxd.to_string(), nym_network_details); - - let coconut_keypair_wrapper = ecash::keys::KeyPair::new(); - - // if the keypair doesnt exist (because say this API is running in the caching mode), nothing will happen - if let Some(loaded_keys) = load_ecash_keypair_if_exists(&config.coconut_signer)? { - let issued_for = loaded_keys.issued_for_epoch; - coconut_keypair_wrapper.set(loaded_keys).await; - - if can_validate_coconut_keys(&nyxd_client, issued_for).await? { - coconut_keypair_wrapper.validate() - } - } - - let identity_keypair = config.base.storage_paths.load_identity()?; - let identity_public_key = *identity_keypair.public_key(); - - // let's build our rocket! - let rocket = http::setup_rocket( - &config, - network_details, - nyxd_client.clone(), - identity_keypair, - coconut_keypair_wrapper.clone(), - ) - .await?; - - // setup shutdowns - let shutdown = TaskManager::new(10); - - // Rocket handles shutdown on its own, but its shutdown handling should be incorporated - // with that of the rest of the tasks. Currently its runtime is forcefully terminated once - // nym-api exits. - let rocket_shutdown_handle = rocket.shutdown(); - - // get references to the managed state - let nym_contract_cache_state = rocket.state::().unwrap(); - let node_status_cache_state = rocket.state::().unwrap(); - let circulating_supply_cache_state = rocket.state::().unwrap(); - let storage = if let Some(storage) = rocket.state::() { - storage.to_owned() - } else { - storage::NymApiStorage::init(&config.node_status_api.storage_paths.database_path).await? - }; - let described_nodes_state = rocket.state::>().unwrap(); - - // start note describe cache refresher - // we should be doing the below, but can't due to our current startup structure - // let refresher = node_describe_cache::new_refresher(&config.topology_cacher); - // let cache = refresher.get_shared_cache(); - node_describe_cache::new_refresher_with_initial_value( - &config.topology_cacher, - nym_contract_cache_state.clone(), - described_nodes_state.to_owned(), - ) - .named("node-self-described-data-refresher") - .start(shutdown.subscribe_named("node-self-described-data-refresher")); - - // start all the caches first - let nym_contract_cache_listener = nym_contract_cache::start_refresher( - &config.node_status_api, - nym_contract_cache_state, - nyxd_client.clone(), - &shutdown, - ); - - node_status_api::start_cache_refresh( - &config.node_status_api, - nym_contract_cache_state, - node_status_cache_state, - storage.to_owned(), - nym_contract_cache_listener, - &shutdown, - ); - circulating_supply_api::start_cache_refresh( - &config.circulating_supply_cacher, - nyxd_client.clone(), - circulating_supply_cache_state, - &shutdown, - ); - - // start dkg task - if config.coconut_signer.enabled { - let dkg_bte_keypair = load_bte_keypair(&config.coconut_signer)?; - - DkgController::start( - &config.coconut_signer, - nyxd_client.clone(), - coconut_keypair_wrapper, - dkg_bte_keypair, - identity_public_key, - OsRng, - &shutdown, - )?; - } - - // and then only start the uptime updater (and the monitor itself, duh) - // if the monitoring if it's enabled - if config.network_monitor.enabled { - network_monitor::start::( - &config.network_monitor, - nym_contract_cache_state, - &storage, - nyxd_client.clone(), - &shutdown, - ) - .await; - - HistoricalUptimeUpdater::start(storage.to_owned(), &shutdown); - - // start 'rewarding' if its enabled - if config.rewarding.enabled { - epoch_operations::ensure_rewarding_permission(&nyxd_client).await?; - RewardedSetUpdater::start(nyxd_client, nym_contract_cache_state, storage, &shutdown); - } - } - // Launch the rocket, serve http endpoints and finish the startup - tokio::spawn(rocket.launch()); - - Ok(ShutdownHandles { - task_manager_handle: shutdown, - rocket_handle: rocket_shutdown_handle, - }) -} diff --git a/nym-api/src/network/handlers.rs b/nym-api/src/network/handlers.rs index 0cb27ca92d..7bb77bc3fc 100644 --- a/nym-api/src/network/handlers.rs +++ b/nym-api/src/network/handlers.rs @@ -2,13 +2,13 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::network::models::{ContractInformation, NetworkDetails}; -use crate::v2::AxumAppState; +use crate::support::http::state::AppState; use axum::{extract, Router}; use nym_contracts_common::ContractBuildInformation; use std::collections::HashMap; use utoipa::ToSchema; -pub(crate) fn nym_network_routes() -> Router { +pub(crate) fn nym_network_routes() -> Router { Router::new() .route("/details", axum::routing::get(network_details)) .route("/nym-contracts", axum::routing::get(nym_contracts)) @@ -27,7 +27,7 @@ pub(crate) fn nym_network_routes() -> Router { ) )] async fn network_details( - extract::State(state): extract::State, + extract::State(state): extract::State, ) -> axum::Json { state.network_details().to_owned().into() } @@ -56,7 +56,7 @@ pub(crate) struct ContractVersionSchemaResponse { ) )] async fn nym_contracts( - extract::State(state): extract::State, + extract::State(state): extract::State, ) -> axum::Json>> { let info = state.nym_contract_cache().contract_details().await; info.iter() @@ -82,7 +82,7 @@ async fn nym_contracts( ) )] async fn nym_contracts_detailed( - extract::State(state): extract::State, + extract::State(state): extract::State, ) -> axum::Json>> { let info = state.nym_contract_cache().contract_details().await; info.iter() diff --git a/nym-api/src/network/mod.rs b/nym-api/src/network/mod.rs index 1f91dea3fd..8595827dd0 100644 --- a/nym-api/src/network/mod.rs +++ b/nym-api/src/network/mod.rs @@ -1,20 +1,14 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use okapi::openapi3::OpenApi; -use rocket::Route; -use rocket_okapi::openapi_get_routes_spec; -use rocket_okapi::settings::OpenApiSettings; - -#[cfg(feature = "axum")] pub(crate) mod handlers; pub(crate) mod models; -mod routes; - -pub(crate) fn network_routes(settings: &OpenApiSettings) -> (Vec, OpenApi) { - openapi_get_routes_spec![ - settings: routes::network_details, - routes::nym_contracts, - routes::nym_contracts_detailed - ] -} +// mod routes; +// +// pub(crate) fn network_routes(settings: &OpenApiSettings) -> (Vec, OpenApi) { +// openapi_get_routes_spec![ +// settings: routes::network_details, +// routes::nym_contracts, +// routes::nym_contracts_detailed +// ] +// } diff --git a/nym-api/src/network/models.rs b/nym-api/src/network/models.rs index 7cdaa1d052..66d0324c26 100644 --- a/nym-api/src/network/models.rs +++ b/nym-api/src/network/models.rs @@ -5,8 +5,7 @@ use nym_config::defaults::NymNetworkDetails; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -#[derive(Clone, Serialize, Deserialize, JsonSchema)] -#[cfg_attr(feature = "axum", derive(utoipa::ToSchema))] +#[derive(Clone, Serialize, Deserialize, JsonSchema, utoipa::ToSchema)] pub struct NetworkDetails { pub(crate) connected_nyxd: String, pub(crate) network: NymNetworkDetails, @@ -21,8 +20,7 @@ impl NetworkDetails { } } -#[cfg_attr(feature = "axum", derive(utoipa::ToSchema))] -#[derive(Serialize, Deserialize, Clone, JsonSchema)] +#[derive(Clone, Serialize, Deserialize, JsonSchema, utoipa::ToSchema)] #[serde(rename_all = "snake_case")] pub struct ContractInformation { pub(crate) address: Option, diff --git a/nym-api/src/network_monitor/mod.rs b/nym-api/src/network_monitor/mod.rs index cc308a627f..699498b287 100644 --- a/nym-api/src/network_monitor/mod.rs +++ b/nym-api/src/network_monitor/mod.rs @@ -11,8 +11,10 @@ use crate::network_monitor::monitor::receiver::{ use crate::network_monitor::monitor::sender::PacketSender; use crate::network_monitor::monitor::summary_producer::SummaryProducer; use crate::network_monitor::monitor::Monitor; +use crate::node_describe_cache::DescribedNodes; use crate::nym_contract_cache::cache::NymContractCache; use crate::storage::NymApiStorage; +use crate::support::caching::cache::SharedCache; use crate::support::{config, nyxd}; use futures::channel::mpsc; use nym_bandwidth_controller::BandwidthController; @@ -23,6 +25,7 @@ use nym_sphinx::params::PacketType; use nym_sphinx::receiver::MessageReceiver; use nym_task::TaskManager; use std::sync::Arc; +use tracing::info; pub(crate) mod gateways_reader; pub(crate) mod monitor; @@ -33,7 +36,8 @@ pub(crate) const ROUTE_TESTING_TEST_NONCE: u64 = 0; pub(crate) fn setup<'a>( config: &'a config::NetworkMonitor, - nym_contract_cache_state: &NymContractCache, + nym_contract_cache: &NymContractCache, + described_cache: SharedCache, storage: &NymApiStorage, nyxd_client: nyxd::Client, ) -> NetworkMonitorBuilder<'a> { @@ -41,7 +45,8 @@ pub(crate) fn setup<'a>( config, nyxd_client, storage.to_owned(), - nym_contract_cache_state.to_owned(), + nym_contract_cache.clone(), + described_cache, ) } @@ -49,7 +54,8 @@ pub(crate) struct NetworkMonitorBuilder<'a> { config: &'a config::NetworkMonitor, nyxd_client: nyxd::Client, node_status_storage: NymApiStorage, - validator_cache: NymContractCache, + contract_cache: NymContractCache, + described_cache: SharedCache, } impl<'a> NetworkMonitorBuilder<'a> { @@ -57,13 +63,15 @@ impl<'a> NetworkMonitorBuilder<'a> { config: &'a config::NetworkMonitor, nyxd_client: nyxd::Client, node_status_storage: NymApiStorage, - validator_cache: NymContractCache, + contract_cache: NymContractCache, + described_cache: SharedCache, ) -> Self { NetworkMonitorBuilder { config, nyxd_client, node_status_storage, - validator_cache, + contract_cache, + described_cache, } } @@ -84,7 +92,8 @@ impl<'a> NetworkMonitorBuilder<'a> { mpsc::unbounded(); let packet_preparer = new_packet_preparer( - self.validator_cache, + self.contract_cache, + self.described_cache, self.config.debug.per_node_test_packets, Arc::clone(&ack_key), *identity_keypair.public_key(), @@ -158,14 +167,16 @@ impl NetworkMonitorRunnables { } fn new_packet_preparer( - validator_cache: NymContractCache, + contract_cache: NymContractCache, + described_cache: SharedCache, per_node_test_packets: usize, ack_key: Arc, self_public_identity: identity::PublicKey, self_public_encryption: encryption::PublicKey, ) -> PacketPreparer { PacketPreparer::new( - validator_cache, + contract_cache, + described_cache, per_node_test_packets, ack_key, self_public_identity, @@ -218,12 +229,19 @@ fn new_packet_receiver( // TODO: 2) how do we make it non-async as other 'start' methods? pub(crate) async fn start( config: &config::NetworkMonitor, - nym_contract_cache_state: &NymContractCache, + nym_contract_cache: &NymContractCache, + described_cache: SharedCache, storage: &NymApiStorage, nyxd_client: nyxd::Client, shutdown: &TaskManager, ) { - let monitor_builder = setup(config, nym_contract_cache_state, storage, nyxd_client); + let monitor_builder = setup( + config, + nym_contract_cache, + described_cache, + storage, + nyxd_client, + ); info!("Starting network monitor..."); let runnables: NetworkMonitorRunnables = monitor_builder.build().await; runnables.spawn_tasks(shutdown); diff --git a/nym-api/src/network_monitor/monitor/gateways_pinger.rs b/nym-api/src/network_monitor/monitor/gateways_pinger.rs index a114088024..ed09c2d090 100644 --- a/nym-api/src/network_monitor/monitor/gateways_pinger.rs +++ b/nym-api/src/network_monitor/monitor/gateways_pinger.rs @@ -3,12 +3,12 @@ use crate::network_monitor::monitor::gateway_clients_cache::ActiveGatewayClients; use crate::network_monitor::monitor::receiver::{GatewayClientUpdate, GatewayClientUpdateSender}; -use log::{debug, info, trace, warn}; use nym_crypto::asymmetric::identity; use nym_crypto::asymmetric::identity::PUBLIC_KEY_LENGTH; use nym_task::TaskClient; use std::time::Duration; use tokio::time::{sleep, Instant}; +use tracing::{debug, info, trace, warn}; // TODO: should it perhaps be moved to config along other timeout values? const PING_TIMEOUT: Duration = Duration::from_secs(3); diff --git a/nym-api/src/network_monitor/monitor/mod.rs b/nym-api/src/network_monitor/monitor/mod.rs index e4c35d8f4f..1fdb0ad601 100644 --- a/nym-api/src/network_monitor/monitor/mod.rs +++ b/nym-api/src/network_monitor/monitor/mod.rs @@ -9,13 +9,14 @@ use crate::network_monitor::test_packet::NodeTestMessage; use crate::network_monitor::test_route::TestRoute; use crate::storage::NymApiStorage; use crate::support::config; -use log::{debug, error, info}; +use nym_mixnet_contract_common::NodeId; use nym_sphinx::params::PacketType; use nym_sphinx::receiver::MessageReceiver; use nym_task::TaskClient; use std::collections::{HashMap, HashSet}; use std::process; use tokio::time::{sleep, Duration, Instant}; +use tracing::{debug, error, info, trace}; pub(crate) mod gateway_clients_cache; pub(crate) mod gateways_pinger; @@ -169,11 +170,11 @@ impl Monitor { .collect() } - fn blacklist_route_nodes(&self, route: &TestRoute, blacklist: &mut HashSet) { + fn blacklist_route_nodes(&self, route: &TestRoute, blacklist: &mut HashSet) { for mix in route.topology().mixes_as_vec() { - blacklist.insert(mix.identity_key.to_base58_string()); + blacklist.insert(mix.mix_id); } - blacklist.insert(route.gateway_identity().to_base58_string()); + blacklist.insert(route.gateway().node_id); } async fn prepare_test_routes(&mut self) -> Option> { diff --git a/nym-api/src/network_monitor/monitor/preparer.rs b/nym-api/src/network_monitor/monitor/preparer.rs index 73a11b4d60..0f139b05d0 100644 --- a/nym-api/src/network_monitor/monitor/preparer.rs +++ b/nym-api/src/network_monitor/monitor/preparer.rs @@ -3,23 +3,29 @@ use crate::network_monitor::monitor::sender::GatewayPackets; use crate::network_monitor::test_route::TestRoute; +use crate::node_describe_cache::{DescribedNodes, NodeDescriptionTopologyExt}; use crate::nym_contract_cache::cache::NymContractCache; -use log::info; +use crate::support::caching::cache::SharedCache; +use nym_api_requests::legacy::{LegacyGatewayBondWithId, LegacyMixNodeBondWithLayer}; +use nym_api_requests::models::NymNodeDescription; use nym_crypto::asymmetric::{encryption, identity}; -use nym_mixnet_contract_common::{GatewayBond, Layer, MixNodeBond}; +use nym_mixnet_contract_common::{LegacyMixLayer, NodeId, RewardedSet}; use nym_node_tester_utils::node::TestableNode; use nym_node_tester_utils::NodeTester; use nym_sphinx::acknowledgements::AckKey; use nym_sphinx::addressing::clients::Recipient; use nym_sphinx::forwarding::packet::MixPacket; use nym_sphinx::params::{PacketSize, PacketType}; +use nym_topology::gateway::GatewayConversionError; +use nym_topology::mix::MixnodeConversionError; use nym_topology::{gateway, mix}; -use rand::{rngs::ThreadRng, seq::SliceRandom, thread_rng, Rng}; +use rand::prelude::SliceRandom; +use rand::{rngs::ThreadRng, thread_rng, Rng}; use std::collections::{HashMap, HashSet}; - use std::fmt::{self, Display, Formatter}; use std::sync::Arc; use std::time::Duration; +use tracing::{error, info, trace}; const DEFAULT_AVERAGE_PACKET_DELAY: Duration = Duration::from_millis(200); const DEFAULT_AVERAGE_ACK_DELAY: Duration = Duration::from_millis(200); @@ -69,7 +75,8 @@ pub(crate) struct PreparedPackets { #[derive(Clone)] pub(crate) struct PacketPreparer { - validator_cache: NymContractCache, + contract_cache: NymContractCache, + described_cache: SharedCache, /// Number of test packets sent to each node per_node_test_packets: usize, @@ -85,14 +92,16 @@ pub(crate) struct PacketPreparer { impl PacketPreparer { pub(crate) fn new( - validator_cache: NymContractCache, + contract_cache: NymContractCache, + described_cache: SharedCache, per_node_test_packets: usize, ack_key: Arc, self_public_identity: identity::PublicKey, self_public_encryption: encryption::PublicKey, ) -> Self { PacketPreparer { - validator_cache, + contract_cache, + described_cache, per_node_test_packets, ack_key, self_public_identity, @@ -138,15 +147,16 @@ impl PacketPreparer { } pub(crate) async fn wait_for_validator_cache_initial_values(&self, minimum_full_routes: usize) { - // wait for the cache to get initialised - self.validator_cache.wait_for_initial_values().await; + // wait for the caches to get initialised + self.contract_cache.wait_for_initial_values().await; + self.described_cache.naive_wait_for_initial_values().await; // now wait for at least `minimum_full_routes` mixnodes per layer and `minimum_full_routes` gateway to be online info!("Waiting for minimal topology to be online"); let initialisation_backoff = Duration::from_secs(30); loop { - let gateways = self.validator_cache.gateways_all().await; - let mixnodes = self.validator_cache.mixnodes_all_basic().await; + let gateways = self.contract_cache.legacy_gateways_all().await; + let mixnodes = self.contract_cache.legacy_mixnodes_all_basic().await; if gateways.len() < minimum_full_routes { self.topology_wait_backoff(initialisation_backoff).await; @@ -159,9 +169,9 @@ impl PacketPreparer { for mix in mixnodes { match mix.layer { - Layer::One => layer1_count += 1, - Layer::Two => layer2_count += 1, - Layer::Three => layer3_count += 1, + LegacyMixLayer::One => layer1_count += 1, + LegacyMixLayer::Two => layer2_count += 1, + LegacyMixLayer::Three => layer3_count += 1, } } @@ -176,64 +186,184 @@ impl PacketPreparer { } } - async fn all_mixnodes_and_gateways(&self) -> (Vec, Vec) { + async fn all_legacy_mixnodes_and_gateways( + &self, + ) -> ( + Vec, + Vec, + ) { info!("Obtaining network topology..."); - let mixnodes = self.validator_cache.mixnodes_all_basic().await; - let gateways = self.validator_cache.gateways_all().await; + let mixnodes = self.contract_cache.legacy_mixnodes_all_basic().await; + let gateways = self.contract_cache.legacy_gateways_all().await; (mixnodes, gateways) } - async fn filtered_mixnodes_and_gateways(&self) -> (Vec, Vec) { + async fn filtered_legacy_mixnodes_and_gateways( + &self, + ) -> ( + Vec, + Vec, + ) { info!("Obtaining network topology..."); - let mixnodes = self.validator_cache.mixnodes_filtered_basic().await; - let gateways = self.validator_cache.gateways_filtered().await; + let mixnodes = self.contract_cache.legacy_mixnodes_filtered_basic().await; + let gateways = self.contract_cache.legacy_gateways_filtered().await; (mixnodes, gateways) } - pub(crate) fn try_parse_mix_bond(&self, mix: &MixNodeBond) -> Result { - let identity = mix.mix_node.identity_key.clone(); - mix.try_into().map_err(|_| identity) + pub(crate) fn try_parse_mix_bond( + &self, + bond: &LegacyMixNodeBondWithLayer, + ) -> Result { + fn parse_bond( + bond: &LegacyMixNodeBondWithLayer, + ) -> Result { + let host = mix::LegacyNode::parse_host(&bond.mix_node.host)?; + + // try to completely resolve the host in the mix situation to avoid doing it every + // single time we want to construct a path + let mix_host = mix::LegacyNode::extract_mix_host(&host, bond.mix_node.mix_port)?; + + Ok(mix::LegacyNode { + mix_id: bond.mix_id, + host, + mix_host, + identity_key: identity::PublicKey::from_base58_string(&bond.mix_node.identity_key)?, + sphinx_key: encryption::PublicKey::from_base58_string(&bond.mix_node.sphinx_key)?, + layer: bond.layer, + version: bond.mix_node.version.as_str().into(), + }) + } + + let identity = bond.mix_node.identity_key.clone(); + parse_bond(bond).map_err(|_| identity) } pub(crate) fn try_parse_gateway_bond( &self, - gateway: &GatewayBond, - ) -> Result { + gateway: &LegacyGatewayBondWithId, + ) -> Result { + fn parse_bond( + bond: &LegacyGatewayBondWithId, + ) -> Result { + let host = gateway::LegacyNode::parse_host(&bond.gateway.host)?; + + // try to completely resolve the host in the mix situation to avoid doing it every + // single time we want to construct a path + let mix_host = gateway::LegacyNode::extract_mix_host(&host, bond.gateway.mix_port)?; + + Ok(gateway::LegacyNode { + node_id: bond.node_id, + host, + mix_host, + clients_ws_port: bond.gateway.clients_port, + clients_wss_port: None, + identity_key: identity::PublicKey::from_base58_string(&bond.gateway.identity_key)?, + sphinx_key: encryption::PublicKey::from_base58_string(&bond.gateway.sphinx_key)?, + version: bond.gateway.version.as_str().into(), + }) + } + let identity = gateway.gateway.identity_key.clone(); - gateway.try_into().map_err(|_| identity) + parse_bond(gateway).map_err(|_| identity) + } + + fn layered_mixes<'a, R: Rng>( + &self, + rng: &mut R, + blacklist: &mut HashSet, + rewarded_set: &RewardedSet, + legacy_mixnodes: Vec, + mixing_nym_nodes: impl Iterator + 'a, + ) -> HashMap> { + let mut layered_mixes = HashMap::new(); + for mix in legacy_mixnodes { + let layer = mix.layer; + let layer_mixes = layered_mixes.entry(layer).or_insert_with(Vec::new); + let Ok(parsed_node) = self.try_parse_mix_bond(&mix) else { + blacklist.insert(mix.mix_id); + continue; + }; + layer_mixes.push(parsed_node) + } + + for mixing_nym_node in mixing_nym_nodes { + let Some(parsed_node) = self.nym_node_to_legacy_mix(rng, rewarded_set, mixing_nym_node) + else { + continue; + }; + let layer = parsed_node.layer; + let layer_mixes = layered_mixes.entry(layer).or_insert_with(Vec::new); + layer_mixes.push(parsed_node) + } + + layered_mixes + } + + fn all_gateways<'a>( + &self, + blacklist: &mut HashSet, + legacy_gateways: Vec, + gateway_capable_nym_nodes: impl Iterator + 'a, + ) -> Vec { + let mut gateways = Vec::new(); + for gateway in legacy_gateways { + let Ok(parsed_node) = self.try_parse_gateway_bond(&gateway) else { + blacklist.insert(gateway.node_id); + continue; + }; + gateways.push(parsed_node) + } + + for gateway_capable_node in gateway_capable_nym_nodes { + let Some(parsed_node) = self.nym_node_to_legacy_gateway(gateway_capable_node) else { + continue; + }; + gateways.push(parsed_node) + } + + gateways } - // gets rewarded nodes // chooses n random nodes from each layer (and gateway) such that they are not on the blacklist - // if failed to parsed => onto the blacklist they go + // if failed to get parsed => onto the blacklist they go // if generated fewer than n, blacklist will be updated by external function with correctly generated // routes so that they wouldn't be reused pub(crate) async fn prepare_test_routes( &self, n: usize, - blacklist: &mut HashSet, + blacklist: &mut HashSet, ) -> Option> { - let (mixnodes, gateways) = self.filtered_mixnodes_and_gateways().await; - // separate mixes into layers for easier selection - let mut layered_mixes = HashMap::new(); - for mix in mixnodes { - let layer = mix.layer; - let mixes = layered_mixes.entry(layer).or_insert_with(Vec::new); - mixes.push(mix) - } + let (legacy_mixnodes, legacy_gateways) = self.filtered_legacy_mixnodes_and_gateways().await; + let rewarded_set = self.contract_cache.rewarded_set().await?; - // get all nodes from each layer... - let l1 = layered_mixes.get(&Layer::One)?; - let l2 = layered_mixes.get(&Layer::Two)?; - let l3 = layered_mixes.get(&Layer::Three)?; + let descriptions = self.described_cache.get().await.ok()?; + + let mixing_nym_nodes = descriptions.mixing_nym_nodes(); + // last I checked `gatewaying` wasn't a word : ) + let gateway_capable_nym_nodes = descriptions.gateway_capable_nym_nodes(); - // try to choose n nodes from each of them (+ gateways)... let mut rng = thread_rng(); + // separate mixes into layers for easier selection + let layered_mixes = self.layered_mixes( + &mut rng, + blacklist, + &rewarded_set, + legacy_mixnodes, + mixing_nym_nodes, + ); + let gateways = self.all_gateways(blacklist, legacy_gateways, gateway_capable_nym_nodes); + + // get all nodes from each layer... + let l1 = layered_mixes.get(&LegacyMixLayer::One)?; + let l2 = layered_mixes.get(&LegacyMixLayer::Two)?; + let l3 = layered_mixes.get(&LegacyMixLayer::Three)?; + + // try to choose n nodes from each of them (+ gateways)... let rand_l1 = l1.choose_multiple(&mut rng, n).collect::>(); let rand_l2 = l2.choose_multiple(&mut rng, n).collect::>(); let rand_l3 = l3.choose_multiple(&mut rng, n).collect::>(); @@ -258,36 +388,18 @@ impl PacketPreparer { trace!("Generating test routes..."); let mut routes = Vec::new(); for i in 0..most_available { - let Ok(node_1) = self.try_parse_mix_bond(rand_l1[i]) else { - blacklist.insert(rand_l1[i].identity().to_owned()); - continue; - }; - - let Ok(node_2) = self.try_parse_mix_bond(rand_l2[i]) else { - blacklist.insert(rand_l2[i].identity().to_owned()); - continue; - }; - - let Ok(node_3) = self.try_parse_mix_bond(rand_l3[i]) else { - blacklist.insert(rand_l3[i].identity().to_owned()); - continue; - }; - - let Ok(gateway) = self.try_parse_gateway_bond(rand_gateways[i]) else { - blacklist.insert(rand_gateways[i].identity().to_owned()); - continue; - }; + let node_1 = rand_l1[i].clone(); + let node_2 = rand_l2[i].clone(); + let node_3 = rand_l3[i].clone(); + let gateway = rand_gateways[i].clone(); routes.push(TestRoute::new(rng.gen(), node_1, node_2, node_3, gateway)) } - info!( - "The following routes will be used for testing: {:#?}", - routes - ); + info!("The following routes will be used for testing: {routes:#?}"); Some(routes) } - fn create_packet_sender(&self, gateway: &gateway::Node) -> Recipient { + fn create_packet_sender(&self, gateway: &gateway::LegacyNode) -> Recipient { Recipient::new( self.self_public_identity, self.self_public_encryption, @@ -325,20 +437,16 @@ impl PacketPreparer { fn filter_outdated_and_malformed_mixnodes( &self, - nodes: Vec, - ) -> (Vec, Vec) { + nodes: Vec, + ) -> (Vec, Vec) { let mut parsed_nodes = Vec::new(); let mut invalid_nodes = Vec::new(); for mixnode in nodes { - if let Ok(parsed_node) = (&mixnode).try_into() { + if let Ok(parsed_node) = self.try_parse_mix_bond(&mixnode) { parsed_nodes.push(parsed_node) } else { invalid_nodes.push(InvalidNode::Malformed { - node: TestableNode::new_mixnode( - mixnode.identity().to_owned(), - mixnode.owner.clone().into_string(), - mixnode.mix_id, - ), + node: TestableNode::new_mixnode(mixnode.identity().to_owned(), mixnode.mix_id), }); } } @@ -347,18 +455,18 @@ impl PacketPreparer { fn filter_outdated_and_malformed_gateways( &self, - nodes: Vec, - ) -> (Vec, Vec) { + nodes: Vec, + ) -> (Vec<(gateway::LegacyNode, NodeId)>, Vec) { let mut parsed_nodes = Vec::new(); let mut invalid_nodes = Vec::new(); for gateway in nodes { - if let Ok(parsed_node) = (&gateway).try_into() { - parsed_nodes.push(parsed_node) + if let Ok(parsed_node) = self.try_parse_gateway_bond(&gateway) { + parsed_nodes.push((parsed_node, gateway.node_id)) } else { invalid_nodes.push(InvalidNode::Malformed { node: TestableNode::new_gateway( - gateway.identity().to_owned(), - gateway.owner.clone().into_string(), + gateway.bond.identity().to_owned(), + gateway.node_id, ), }); } @@ -366,6 +474,43 @@ impl PacketPreparer { (parsed_nodes, invalid_nodes) } + fn nym_node_to_legacy_mix( + &self, + rng: &mut R, + rewarded_set: &RewardedSet, + mixing_nym_node: &NymNodeDescription, + ) -> Option { + let maybe_explicit_layer = rewarded_set + .try_get_mix_layer(&mixing_nym_node.node_id) + .and_then(|layer| LegacyMixLayer::try_from(layer).ok()); + + let layer = match maybe_explicit_layer { + Some(layer) => layer, + None => { + let layer_choices = [ + LegacyMixLayer::One, + LegacyMixLayer::Two, + LegacyMixLayer::Three, + ]; + + // if nym-node doesn't have a layer assigned, since it's either standby or inactive, + // we have to choose one randomly for the testing purposes + // SAFETY: the slice is not empty so the unwrap is fine + #[allow(clippy::unwrap_used)] + layer_choices.choose(rng).copied().unwrap() + } + }; + + mixing_nym_node.try_to_topology_mix_node(layer).ok() + } + + fn nym_node_to_legacy_gateway( + &self, + gateway_capable_node: &NymNodeDescription, + ) -> Option { + gateway_capable_node.try_to_topology_gateway().ok() + } + pub(super) async fn prepare_test_packets( &mut self, test_nonce: u64, @@ -373,17 +518,38 @@ impl PacketPreparer { // TODO: Maybe do this _packet_type: PacketType, ) -> PreparedPackets { - // only test mixnodes that are rewarded, i.e. that will be rewarded in this interval. - // (remember that "idle" nodes are still part of that set) - // we don't care about other nodes, i.e. nodes that are bonded but will not get - // any reward during the current rewarding interval - let (mixnodes, gateways) = self.all_mixnodes_and_gateways().await; + let (mixnodes, gateways) = self.all_legacy_mixnodes_and_gateways().await; + let rewarded_set = self.contract_cache.rewarded_set().await; + + let descriptions = self + .described_cache + .get() + .await + .expect("the cache must have been initialised!"); + let mixing_nym_nodes = descriptions.mixing_nym_nodes(); + let gateway_capable_nym_nodes = descriptions.gateway_capable_nym_nodes(); let (mixnodes, invalid_mixnodes) = self.filter_outdated_and_malformed_mixnodes(mixnodes); let (gateways, invalid_gateways) = self.filter_outdated_and_malformed_gateways(gateways); - let tested_mixnodes = mixnodes.iter().map(|node| node.into()).collect::>(); - let tested_gateways = gateways.iter().map(|node| node.into()).collect::>(); + let mut tested_mixnodes = mixnodes.iter().map(|node| node.into()).collect::>(); + let mut tested_gateways = gateways.iter().map(|node| node.into()).collect::>(); + + // try to add nym-nodes into the fold + if let Some(rewarded_set) = rewarded_set { + let mut rng = thread_rng(); + for mix in mixing_nym_nodes { + if let Some(parsed) = self.nym_node_to_legacy_mix(&mut rng, &rewarded_set, &mix) { + tested_mixnodes.push(TestableNode::from(&parsed)); + } + } + } + + for gateway in gateway_capable_nym_nodes { + if let Some(parsed) = self.nym_node_to_legacy_gateway(&gateway) { + tested_gateways.push((&parsed, gateway.node_id).into()) + } + } let packets_to_create = (test_routes.len() * self.per_node_test_packets) * (tested_mixnodes.len() + tested_gateways.len()); @@ -405,6 +571,7 @@ impl PacketPreparer { // 1. the topology is definitely valid (otherwise we wouldn't be here) // 2. the recipient is specified (by calling **mix**_tester) // 3. the test message is not too long, i.e. when serialized it will fit in a single sphinx packet + #[allow(clippy::unwrap_used)] let mixnode_test_packets = mix_tester .mixnodes_test_packets( &mixnodes, @@ -421,7 +588,7 @@ impl PacketPreparer { gateway_packets.push_packets(mix_packets); // and generate test packets for gateways (note the variable recipient) - for gateway in &gateways { + for (gateway, node_id) in &gateways { let recipient = self.create_packet_sender(gateway); let gateway_identity = gateway.identity_key; let gateway_address = gateway.clients_address(); @@ -430,9 +597,11 @@ impl PacketPreparer { // 1. the topology is definitely valid (otherwise we wouldn't be here) // 2. the recipient is specified // 3. the test message is not too long, i.e. when serialized it will fit in a single sphinx packet + #[allow(clippy::unwrap_used)] let gateway_test_packets = mix_tester - .gateway_test_packets( + .legacy_gateway_test_packets( gateway, + *node_id, route_ext, self.per_node_test_packets as u32, Some(recipient), diff --git a/nym-api/src/network_monitor/monitor/processor.rs b/nym-api/src/network_monitor/monitor/processor.rs index 84ac4b3aeb..0008bfa901 100644 --- a/nym-api/src/network_monitor/monitor/processor.rs +++ b/nym-api/src/network_monitor/monitor/processor.rs @@ -7,7 +7,6 @@ use crate::network_monitor::ROUTE_TESTING_TEST_NONCE; use futures::channel::mpsc; use futures::lock::{Mutex, MutexGuard}; use futures::{SinkExt, StreamExt}; -use log::warn; use nym_crypto::asymmetric::encryption; use nym_node_tester_utils::error::NetworkTestingError; use nym_node_tester_utils::processor::TestPacketProcessor; @@ -16,6 +15,7 @@ use nym_sphinx::receiver::{MessageReceiver, MessageRecoveryError}; use std::mem; use std::sync::Arc; use thiserror::Error; +use tracing::{debug, error, trace, warn}; pub(crate) type ReceivedProcessorSender = mpsc::UnboundedSender; pub(crate) type ReceivedProcessorReceiver = mpsc::UnboundedReceiver; diff --git a/nym-api/src/network_monitor/monitor/receiver.rs b/nym-api/src/network_monitor/monitor/receiver.rs index be835fcc41..d6dcbf4b85 100644 --- a/nym-api/src/network_monitor/monitor/receiver.rs +++ b/nym-api/src/network_monitor/monitor/receiver.rs @@ -8,6 +8,7 @@ use futures::StreamExt; use nym_crypto::asymmetric::identity; use nym_gateway_client::{AcknowledgementReceiver, MixnetMessageReceiver}; use nym_task::TaskClient; +use tracing::trace; pub(crate) type GatewayClientUpdateSender = mpsc::UnboundedSender; pub(crate) type GatewayClientUpdateReceiver = mpsc::UnboundedReceiver; diff --git a/nym-api/src/network_monitor/monitor/sender.rs b/nym-api/src/network_monitor/monitor/sender.rs index e2c5bf8e4d..0b371f1003 100644 --- a/nym-api/src/network_monitor/monitor/sender.rs +++ b/nym-api/src/network_monitor/monitor/sender.rs @@ -11,7 +11,6 @@ use futures::channel::mpsc; use futures::stream::{self, FuturesUnordered, StreamExt}; use futures::task::Context; use futures::{Future, Stream}; -use log::{debug, info, trace, warn}; use nym_bandwidth_controller::BandwidthController; use nym_credential_storage::persistent_storage::PersistentStorage; use nym_crypto::asymmetric::identity::{self, PUBLIC_KEY_LENGTH}; @@ -30,6 +29,7 @@ use std::pin::Pin; use std::sync::Arc; use std::task::Poll; use std::time::Duration; +use tracing::{debug, info, trace, warn}; const TIME_CHUNK_SIZE: Duration = Duration::from_millis(50); diff --git a/nym-api/src/network_monitor/monitor/summary_producer.rs b/nym-api/src/network_monitor/monitor/summary_producer.rs index 0c09e196f6..5f8449f75f 100644 --- a/nym-api/src/network_monitor/monitor/summary_producer.rs +++ b/nym-api/src/network_monitor/monitor/summary_producer.rs @@ -5,7 +5,7 @@ use crate::network_monitor::monitor::preparer::InvalidNode; use crate::network_monitor::test_packet::NodeTestMessage; use crate::network_monitor::test_route::TestRoute; use nym_node_tester_utils::node::{NodeType, TestableNode}; -use nym_types::monitoring::{GatewayResult, MixnodeResult}; +use nym_types::monitoring::NodeResult; use std::collections::HashMap; use std::fmt::{Display, Formatter}; @@ -60,8 +60,8 @@ impl TestReport { fn new( total_sent: usize, total_received: usize, - mixnode_results: &[MixnodeResult], - gateway_results: &[GatewayResult], + mixnode_results: &[NodeResult], + gateway_results: &[NodeResult], route_results: &[RouteResult], ) -> Self { let mut exceptional_mixnodes = 0; @@ -206,8 +206,8 @@ impl Display for TestReport { } pub(crate) struct TestSummary { - pub(crate) mixnode_results: Vec, - pub(crate) gateway_results: Vec, + pub(crate) mixnode_results: Vec, + pub(crate) gateway_results: Vec, pub(crate) route_results: Vec, } @@ -291,16 +291,10 @@ impl SummaryProducer { let performance = received as f32 / per_node_expected as f32 * 100.0; let reliability = performance.round() as u8; + let result = NodeResult::new(node.node_id, node.encoded_identity, reliability); match node.typ { - NodeType::Mixnode { mix_id } => { - let res = - MixnodeResult::new(mix_id, node.encoded_identity, node.owner, reliability); - mixnode_results.push(res) - } - NodeType::Gateway => { - let res = GatewayResult::new(node.encoded_identity, node.owner, reliability); - gateway_results.push(res) - } + NodeType::Mixnode => mixnode_results.push(result), + NodeType::Gateway => gateway_results.push(result), } } diff --git a/nym-api/src/network_monitor/test_packet.rs b/nym-api/src/network_monitor/test_packet.rs index 6e5c433ff8..1b1bfbbeda 100644 --- a/nym-api/src/network_monitor/test_packet.rs +++ b/nym-api/src/network_monitor/test_packet.rs @@ -24,7 +24,7 @@ impl NymApiTestMessageExt { pub fn mix_plaintexts( &self, - node: &mix::Node, + node: &mix::LegacyNode, test_packets: u32, ) -> Result>, NetworkTestingError> { NodeTestMessage::mix_plaintexts(node, test_packets, *self) diff --git a/nym-api/src/network_monitor/test_route/mod.rs b/nym-api/src/network_monitor/test_route/mod.rs index 477251f80d..224f751357 100644 --- a/nym-api/src/network_monitor/test_route/mod.rs +++ b/nym-api/src/network_monitor/test_route/mod.rs @@ -16,10 +16,10 @@ pub(crate) struct TestRoute { impl TestRoute { pub(crate) fn new( id: u64, - l1_mix: mix::Node, - l2_mix: mix::Node, - l3_mix: mix::Node, - gateway: gateway::Node, + l1_mix: mix::LegacyNode, + l2_mix: mix::LegacyNode, + l3_mix: mix::LegacyNode, + gateway: gateway::LegacyNode, ) -> Self { let layered_mixes = [ (1u8, vec![l1_mix]), @@ -39,19 +39,19 @@ impl TestRoute { self.id } - pub(crate) fn gateway(&self) -> &gateway::Node { + pub(crate) fn gateway(&self) -> &gateway::LegacyNode { &self.nodes.gateways()[0] } - pub(crate) fn layer_one_mix(&self) -> &mix::Node { + pub(crate) fn layer_one_mix(&self) -> &mix::LegacyNode { &self.nodes.mixes().get(&1).unwrap()[0] } - pub(crate) fn layer_two_mix(&self) -> &mix::Node { + pub(crate) fn layer_two_mix(&self) -> &mix::LegacyNode { &self.nodes.mixes().get(&2).unwrap()[0] } - pub(crate) fn layer_three_mix(&self) -> &mix::Node { + pub(crate) fn layer_three_mix(&self) -> &mix::LegacyNode { &self.nodes.mixes().get(&3).unwrap()[0] } diff --git a/nym-api/src/node_describe_cache/mod.rs b/nym-api/src/node_describe_cache/mod.rs index f56646e6fc..2b9b8d0564 100644 --- a/nym-api/src/node_describe_cache/mod.rs +++ b/nym-api/src/node_describe_cache/mod.rs @@ -1,26 +1,29 @@ -// Copyright 2023 - Nym Technologies SA +// Copyright 2023-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use crate::node_describe_cache::query_helpers::query_for_described_data; use crate::nym_contract_cache::cache::NymContractCache; use crate::support::caching::cache::{SharedCache, UninitialisedCache}; use crate::support::caching::refresher::{CacheItemProvider, CacheRefresher}; use crate::support::config; use crate::support::config::DEFAULT_NODE_DESCRIBE_BATCH_SIZE; +use async_trait::async_trait; use futures::{stream, StreamExt}; -use nym_api_requests::models::{ - AuthenticatorDetails, IpPacketRouterDetails, NetworkRequesterDetails, NymNodeDescription, - WireguardDetails, -}; -use nym_api_requests::nym_nodes::NodeRole; -use nym_config::defaults::{mainnet, DEFAULT_NYM_NODE_HTTP_PORT}; -use nym_contracts_common::IdentityKey; +use nym_api_requests::models::{DescribedNodeType, NymNodeData, NymNodeDescription}; +use nym_config::defaults::DEFAULT_NYM_NODE_HTTP_PORT; +use nym_crypto::asymmetric::{ed25519, encryption, identity, x25519}; +use nym_mixnet_contract_common::{LegacyMixLayer, NodeId}; use nym_node_requests::api::client::{NymNodeApiClientError, NymNodeApiClientExt}; +use nym_topology::gateway::GatewayConversionError; +use nym_topology::mix::MixnodeConversionError; +use nym_topology::{gateway, mix, NetworkAddress}; use std::collections::HashMap; +use std::net::SocketAddr; +use std::time::Duration; use thiserror::Error; -use time::OffsetDateTime; +use tracing::{debug, error}; -// type alias for ease of use -pub type DescribedNodes = HashMap; +mod query_helpers; #[derive(Debug, Error)] pub enum NodeDescribeCacheError { @@ -30,30 +33,155 @@ pub enum NodeDescribeCacheError { source: UninitialisedCache, }, - #[error("gateway {gateway} has provided malformed host information ({host}: {source}")] + #[error("node {node_id} has provided malformed host information ({host}: {source}")] MalformedHost { host: String, - gateway: IdentityKey, + node_id: NodeId, #[source] source: NymNodeApiClientError, }, - #[error("gateway '{gateway}' with host '{host}' doesn't seem to expose any of the standard API ports, i.e.: 80, 443 or {}", DEFAULT_NYM_NODE_HTTP_PORT)] - NoHttpPortsAvailable { host: String, gateway: IdentityKey }, + #[error("node {node_id} with host '{host}' doesn't seem to expose its declared http port nor any of the standard API ports, i.e.: 80, 443 or {}", DEFAULT_NYM_NODE_HTTP_PORT)] + NoHttpPortsAvailable { host: String, node_id: NodeId }, - #[error("failed to query gateway '{gateway}': {source}")] + #[error("failed to query node {node_id}: {source}")] ApiFailure { - gateway: IdentityKey, + node_id: NodeId, #[source] source: NymNodeApiClientError, }, // TODO: perhaps include more details here like whether key/signature/payload was malformed - #[error("could not verify signed host information for gateway '{gateway}'")] - MissignedHostInformation { gateway: IdentityKey }, + #[error("could not verify signed host information for node {node_id}")] + MissignedHostInformation { node_id: NodeId }, +} + +// this exists because I've been moving things around quite a lot and now the place that holds the type +// doesn't have relevant dependencies for proper impl +pub(crate) trait NodeDescriptionTopologyExt { + fn try_to_topology_mix_node( + &self, + layer: LegacyMixLayer, + ) -> Result; + + fn try_to_topology_gateway(&self) -> Result; +} + +impl NodeDescriptionTopologyExt for NymNodeDescription { + // TODO: this might have to be moved around + fn try_to_topology_mix_node( + &self, + layer: LegacyMixLayer, + ) -> Result { + let keys = &self.description.host_information.keys; + let ips = &self.description.host_information.ip_address; + if ips.is_empty() { + return Err(MixnodeConversionError::NoIpAddressesProvided { + mixnode: keys.ed25519.clone(), + }); + } + + let host = match &self.description.host_information.hostname { + None => NetworkAddress::IpAddr(ips[0]), + Some(hostname) => NetworkAddress::Hostname(hostname.clone()), + }; + + // get ip from the self-reported values so we wouldn't need to do any hostname resolution + // (which doesn't really work in wasm) + let mix_host = SocketAddr::new(ips[0], self.description.mix_port()); + + Ok(mix::LegacyNode { + mix_id: self.node_id, + host, + mix_host, + identity_key: ed25519::PublicKey::from_base58_string(&keys.ed25519)?, + sphinx_key: x25519::PublicKey::from_base58_string(&keys.x25519)?, + layer, + version: self + .description + .build_information + .build_version + .as_str() + .into(), + }) + } + + fn try_to_topology_gateway(&self) -> Result { + let keys = &self.description.host_information.keys; + + let ips = &self.description.host_information.ip_address; + if ips.is_empty() { + return Err(GatewayConversionError::NoIpAddressesProvided { + gateway: keys.ed25519.clone(), + }); + } + + let host = match &self.description.host_information.hostname { + None => NetworkAddress::IpAddr(ips[0]), + Some(hostname) => NetworkAddress::Hostname(hostname.clone()), + }; + + // get ip from the self-reported values so we wouldn't need to do any hostname resolution + // (which doesn't really work in wasm) + let mix_host = SocketAddr::new(ips[0], self.description.mix_port()); + + Ok(gateway::LegacyNode { + node_id: self.node_id, + host, + mix_host, + clients_ws_port: self.description.mixnet_websockets.ws_port, + clients_wss_port: self.description.mixnet_websockets.wss_port, + identity_key: identity::PublicKey::from_base58_string( + &self.description.host_information.keys.ed25519, + )?, + sphinx_key: encryption::PublicKey::from_base58_string( + &self.description.host_information.keys.x25519, + )?, + version: self + .description + .build_information + .build_version + .as_str() + .into(), + }) + } +} + +pub struct DescribedNodes { + nodes: HashMap, +} + +impl DescribedNodes { + pub fn get_description(&self, node_id: &NodeId) -> Option<&NymNodeData> { + self.nodes.get(node_id).map(|n| &n.description) + } + + pub fn get_node(&self, node_id: &NodeId) -> Option<&NymNodeDescription> { + self.nodes.get(node_id) + } + + pub fn all_nodes<'a>(&'a self) -> impl Iterator + 'a { + self.nodes.values() + } + + pub fn mixing_nym_nodes<'a>(&'a self) -> impl Iterator + 'a { + self.nodes + .values() + .filter(|n| n.contract_node_type == DescribedNodeType::NymNode) + .filter(|n| n.description.declared_role.can_operate_mixnode()) + } + + pub fn gateway_capable_nym_nodes<'a>( + &'a self, + ) -> impl Iterator + 'a { + self.nodes + .values() + .filter(|n| n.contract_node_type == DescribedNodeType::NymNode) + .filter(|n| n.description.declared_role.can_operate_entry_gateway()) + } } pub struct NodeDescriptionProvider { @@ -79,35 +207,42 @@ impl NodeDescriptionProvider { async fn try_get_client( host: &str, - identity_key: &IdentityKey, - port: Option, + node_id: NodeId, + custom_port: Option, ) -> Result { // first try the standard port in case the operator didn't put the node behind the proxy, // then default https (443) // finally default http (80) let mut addresses_to_try = vec![ - format!("http://{host}:{DEFAULT_NYM_NODE_HTTP_PORT}"), - format!("http://{host}:8000"), - format!("https://{host}"), - format!("http://{host}"), + format!("http://{host}:{DEFAULT_NYM_NODE_HTTP_PORT}"), // 'standard' nym-node + format!("https://{host}"), // node behind https proxy (443) + format!("http://{host}"), // node behind http proxy (80) ]; - if let Some(port) = port { + // note: I removed 'standard' legacy mixnode port because it should now be automatically pulled via + // the 'custom_port' since it should have been present in the contract. + + if let Some(port) = custom_port { addresses_to_try.insert(0, format!("http://{host}:{port}")); } for address in addresses_to_try { // if provided host was malformed, no point in continuing - let client = match nym_node_requests::api::Client::new_url(address, None) { + let client = match nym_node_requests::api::Client::builder(address).and_then(|b| { + b.with_timeout(Duration::from_secs(5)) + .with_user_agent("nym-api-describe-cache") + .build() + }) { Ok(client) => client, Err(err) => { return Err(NodeDescribeCacheError::MalformedHost { host: host.to_string(), - gateway: identity_key.clone(), + node_id, source: err, }); } }; + if let Ok(health) = client.get_health().await { if health.status.is_up() { return Ok(client); @@ -117,149 +252,75 @@ async fn try_get_client( Err(NodeDescribeCacheError::NoHttpPortsAvailable { host: host.to_string(), - gateway: identity_key.to_string(), + node_id, }) } async fn try_get_description( data: RefreshData, -) -> Result<(IdentityKey, NymNodeDescription), NodeDescribeCacheError> { - let client = try_get_client(&data.host(), &data.identity_key(), data.port()).await?; +) -> Result { + let client = try_get_client(&data.host, data.node_id, data.port).await?; - let host_info = - client - .get_host_information() - .await - .map_err(|err| NodeDescribeCacheError::ApiFailure { - gateway: data.identity_key().to_string(), - source: err, - })?; + let map_query_err = |err| NodeDescribeCacheError::ApiFailure { + node_id: data.node_id, + source: err, + }; + + let host_info = client.get_host_information().await.map_err(map_query_err)?; if !host_info.verify_host_information() { return Err(NodeDescribeCacheError::MissignedHostInformation { - gateway: data.identity_key().clone(), + node_id: data.node_id, }); } - let build_info = - client - .get_build_information() - .await - .map_err(|err| NodeDescribeCacheError::ApiFailure { - gateway: data.identity_key().clone(), - source: err, - })?; + let node_info = query_for_described_data(&client, data.node_id).await?; + let description = node_info.into_node_description(host_info.data); - // this can be an old node that hasn't yet exposed this - let auxiliary_details = client.get_auxiliary_details().await.inspect_err(|err| { - debug!("could not obtain auxiliary details of node {}: {err} is it running an old version?", data.identity_key()); - }).unwrap_or_default(); - - let websockets = - client - .get_mixnet_websockets() - .await - .map_err(|err| NodeDescribeCacheError::ApiFailure { - gateway: data.identity_key().clone(), - source: err, - })?; - - let network_requester = - if let Ok(nr) = client.get_network_requester().await { - let exit_policy = client.get_exit_policy().await.map_err(|err| { - NodeDescribeCacheError::ApiFailure { - gateway: data.identity_key().clone(), - source: err, - } - })?; - let uses_nym_exit_policy = exit_policy.upstream_source == mainnet::EXIT_POLICY_URL; - - Some(NetworkRequesterDetails { - address: nr.address, - uses_exit_policy: exit_policy.enabled && uses_nym_exit_policy, - }) - } else { - None - }; - - let ip_packet_router = if let Ok(ipr) = client.get_ip_packet_router().await { - Some(IpPacketRouterDetails { - address: ipr.address, - }) - } else { - None - }; - - let authenticator = if let Ok(auth) = client.get_authenticator().await { - Some(AuthenticatorDetails { - address: auth.address, - }) - } else { - None - }; - - let wireguard = if let Ok(wg) = client.get_wireguard().await { - Some(WireguardDetails { - port: wg.port, - public_key: wg.public_key, - }) - } else { - None - }; - - let description = NymNodeDescription { - host_information: host_info.data.into(), - last_polled: OffsetDateTime::now_utc().into(), - build_information: build_info, - network_requester, - ip_packet_router, - authenticator, - wireguard, - mixnet_websockets: websockets.into(), - auxiliary_details, - role: data.role(), - }; - - Ok((data.identity_key().clone(), description)) + Ok(NymNodeDescription { + node_id: data.node_id, + contract_node_type: data.node_type, + description, + }) } struct RefreshData { host: String, - identity_key: IdentityKey, - role: NodeRole, + node_id: NodeId, + node_type: DescribedNodeType, + port: Option, } impl RefreshData { - pub fn new(host: String, identity_key: IdentityKey, role: NodeRole, port: Option) -> Self { + pub fn new( + host: impl Into, + node_type: DescribedNodeType, + node_id: NodeId, + port: Option, + ) -> Self { RefreshData { - host, - identity_key, - role, + host: host.into(), + node_id, + node_type, port, } } - pub fn host(&self) -> String { - self.host.clone() - } - - pub fn identity_key(&self) -> IdentityKey { - self.identity_key.clone() - } - - pub fn port(&self) -> Option { - self.port - } - - pub fn role(&self) -> NodeRole { - self.role.clone() + async fn try_refresh(self) -> Option { + match try_get_description(self).await { + Ok(description) => Some(description), + Err(err) => { + debug!("failed to obtain node self-described data: {err}"); + None + } + } } } #[async_trait] impl CacheItemProvider for NodeDescriptionProvider { - type Item = HashMap; + type Item = DescribedNodes; type Error = NodeDescribeCacheError; async fn wait_until_ready(&self) { @@ -267,58 +328,62 @@ impl CacheItemProvider for NodeDescriptionProvider { } async fn try_refresh(&self) -> Result { - let mut host_id_pairs = self - .contract_cache - .gateways_all() - .await - .into_iter() - .map(|full| { - RefreshData::new( - full.gateway.host, - full.gateway.identity_key, - NodeRole::EntryGateway, - None, - ) - }) - .collect::>(); + // we need to query: + // - legacy mixnodes (because they might already be running nym-nodes, but haven't updated contract info) + // - legacy gateways (because they might already be running nym-nodes, but haven't updated contract info) + // - nym-nodes - host_id_pairs.extend( - self.contract_cache - .mixnodes_all() - .await - .into_iter() - .map(|full| { - RefreshData::new( - full.bond_information.mix_node.host, - full.bond_information.mix_node.identity_key, - NodeRole::Mixnode { - layer: full.bond_information.layer.into(), - }, - Some(full.bond_information.mix_node.mix_port), - ) - }) - .collect::>(), - ); + let mut nodes_to_query = Vec::new(); - if host_id_pairs.is_empty() { - return Ok(HashMap::new()); + match self.contract_cache.all_cached_legacy_mixnodes().await { + None => error!("failed to obtain mixnodes information from the cache"), + Some(legacy_mixnodes) => { + for node in &**legacy_mixnodes { + nodes_to_query.push(RefreshData::new( + &node.bond_information.mix_node.host, + DescribedNodeType::LegacyMixnode, + node.mix_id(), + Some(node.bond_information.mix_node.http_api_port), + )) + } + } } - let node_description = stream::iter(host_id_pairs.into_iter().map(try_get_description)) - .buffer_unordered(self.batch_size) - .filter_map(|res| async move { - match res { - Ok((identity, description)) => Some((identity, description)), - Err(err) => { - debug!("failed to obtain gateway self-described data: {err}"); - None - } + match self.contract_cache.all_cached_legacy_gateways().await { + None => error!("failed to obtain gateways information from the cache"), + Some(legacy_gateways) => { + for node in &**legacy_gateways { + nodes_to_query.push(RefreshData::new( + &node.bond.gateway.host, + DescribedNodeType::LegacyGateway, + node.node_id, + None, + )) } - }) + } + } + + match self.contract_cache.all_cached_nym_nodes().await { + None => error!("failed to obtain nym-nodes information from the cache"), + Some(nym_nodes) => { + for node in &**nym_nodes { + nodes_to_query.push(RefreshData::new( + &node.bond_information.node.host, + DescribedNodeType::NymNode, + node.node_id(), + node.bond_information.node.custom_http_port, + )) + } + } + } + + let nodes = stream::iter(nodes_to_query.into_iter().map(|n| n.try_refresh())) + .buffer_unordered(self.batch_size) + .filter_map(|x| async move { x.map(|d| (d.node_id, d)) }) .collect::>() .await; - Ok(node_description) + Ok(DescribedNodes { nodes }) } } @@ -327,8 +392,6 @@ impl CacheItemProvider for NodeDescriptionProvider { pub(crate) fn new_refresher( config: &config::TopologyCacher, contract_cache: NymContractCache, - // hehe. we can't do that yet - // network_gateways: SharedCache>, ) -> CacheRefresher { CacheRefresher::new( Box::new( @@ -342,8 +405,6 @@ pub(crate) fn new_refresher( pub(crate) fn new_refresher_with_initial_value( config: &config::TopologyCacher, contract_cache: NymContractCache, - // hehe. we can't do that yet - // network_gateways: SharedCache>, initial: SharedCache, ) -> CacheRefresher { CacheRefresher::new_with_initial_value( diff --git a/nym-api/src/node_describe_cache/query_helpers.rs b/nym-api/src/node_describe_cache/query_helpers.rs new file mode 100644 index 0000000000..cdb9e04d4e --- /dev/null +++ b/nym-api/src/node_describe_cache/query_helpers.rs @@ -0,0 +1,242 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::node_describe_cache::NodeDescribeCacheError; +use futures::future::{maybe_done, MaybeDone}; +use futures::{FutureExt, TryFutureExt}; +use nym_api_requests::models::{ + AuthenticatorDetails, HostInformation, IpPacketRouterDetails, NetworkRequesterDetails, + NymNodeData, WebSockets, WireguardDetails, +}; +use nym_bin_common::build_information::BinaryBuildInformationOwned; +use nym_config::defaults::mainnet; +use nym_mixnet_contract_common::NodeId; +use nym_node_requests::api::client::{NymNodeApiClientError, NymNodeApiClientExt}; +use nym_node_requests::api::v1::node::models::{AuxiliaryDetails, NodeRoles}; +use nym_node_requests::api::Client; +use pin_project::pin_project; +use std::future::Future; +use std::pin::Pin; +use std::task::{Context, Poll}; +use time::OffsetDateTime; +use tracing::debug; + +async fn network_requester_future( + client: &Client, +) -> Result, NymNodeApiClientError> { + let Ok(nr) = client.get_network_requester().await else { + return Ok(None); + }; + + client.get_exit_policy().await.map(|exit_policy| { + let uses_nym_exit_policy = exit_policy.upstream_source == mainnet::EXIT_POLICY_URL; + Some(NetworkRequesterDetails { + address: nr.address, + uses_exit_policy: exit_policy.enabled && uses_nym_exit_policy, + }) + }) +} + +pub(crate) async fn query_for_described_data( + client: &Client, + node_id: NodeId, +) -> Result { + let map_query_err = |source| NodeDescribeCacheError::ApiFailure { node_id, source }; + + // all of those should be happening concurrently. + NodeDescribedInfoMegaFuture::new( + client.get_build_information().map_err(map_query_err), + client.get_roles().map_err(map_query_err), + client.get_auxiliary_details() + .inspect_err(|err| { + // old nym-nodes will not have this field, so use the default instead + debug!("could not obtain auxiliary details of node {node_id}: {err} is it running an old version?") + }) + .unwrap_or_else(|_| AuxiliaryDetails::default()), + client.get_mixnet_websockets().ok_into().map_err(map_query_err), + network_requester_future(client).map_err(map_query_err), + // `ok_into` ultimately calls `IpPacketRouter::into` to transform it into `IpPacketRouterDetails` + client.get_ip_packet_router().ok_into().map(Result::ok), + client.get_authenticator().ok_into().map(Result::ok), + client.get_wireguard().ok_into().map(Result::ok) + ) + .await +} + +// just a helper to have named fields as opposed to a mega tuple +// could I have used something more sophisticated? sure. +// is this code disgusting? yes. does it work? also yes +// (note: I've just mostly copied code from `futures-util::generate` macro where +// they derive code for `join2`, `join3`, etc.) +#[pin_project] +struct NodeDescribedInfoMegaFuture +where + F1: Future, + F2: Future, + F3: Future, + F4: Future, + F5: Future, + F6: Future, + F7: Future, + F8: Future, +{ + #[pin] + build_info: MaybeDone, + #[pin] + roles: MaybeDone, + #[pin] + auxiliary_details: MaybeDone, + #[pin] + websockets: MaybeDone, + #[pin] + network_requester: MaybeDone, + #[pin] + ipr: MaybeDone, + #[pin] + authenticator: MaybeDone, + #[pin] + wireguard: MaybeDone, +} + +impl Future + for NodeDescribedInfoMegaFuture +where + F1: Future>, + F2: Future>, + F3: Future, + F4: Future>, + F5: Future, NodeDescribeCacheError>>, + F6: Future>, + F7: Future>, + F8: Future>, +{ + type Output = Result; + + // SAFETY: we've explicitly checked all futures have completed thus the unwraps are fine + #[allow(clippy::unwrap_used)] + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let mut all_done = true; + let mut futures = self.project(); + + all_done &= futures.build_info.as_mut().poll(cx).is_ready(); + all_done &= futures.roles.as_mut().poll(cx).is_ready(); + all_done &= futures.auxiliary_details.as_mut().poll(cx).is_ready(); + all_done &= futures.websockets.as_mut().poll(cx).is_ready(); + all_done &= futures.network_requester.as_mut().poll(cx).is_ready(); + all_done &= futures.ipr.as_mut().poll(cx).is_ready(); + all_done &= futures.authenticator.as_mut().poll(cx).is_ready(); + all_done &= futures.wireguard.as_mut().poll(cx).is_ready(); + + if all_done { + Poll::Ready( + ResolvedNodeDescribedInfo { + build_info: futures.build_info.take_output().unwrap(), + roles: futures.roles.take_output().unwrap(), + auxiliary_details: futures.auxiliary_details.take_output().unwrap(), + websockets: futures.websockets.take_output().unwrap(), + network_requester: futures.network_requester.take_output().unwrap(), + ipr: futures.ipr.take_output().unwrap(), + authenticator: futures.authenticator.take_output().unwrap(), + wireguard: futures.wireguard.take_output().unwrap(), + } + .try_unwrap(), + ) + } else { + Poll::Pending + } + } +} + +impl NodeDescribedInfoMegaFuture +where + F1: Future, + F2: Future, + F3: Future, + F4: Future, + F5: Future, + F6: Future, + F7: Future, + F8: Future, +{ + // okay. the fact I have to bypass clippy here means it wasn't a good idea to create this abomination after all + #[allow(clippy::too_many_arguments)] + fn new( + build_info: F1, + roles: F2, + auxiliary_details: F3, + websockets: F4, + network_requester: F5, + ipr: F6, + authenticator: F7, + wireguard: F8, + ) -> Self { + NodeDescribedInfoMegaFuture { + build_info: maybe_done(build_info), + roles: maybe_done(roles), + auxiliary_details: maybe_done(auxiliary_details), + websockets: maybe_done(websockets), + network_requester: maybe_done(network_requester), + ipr: maybe_done(ipr), + authenticator: maybe_done(authenticator), + wireguard: maybe_done(wireguard), + } + } +} + +struct ResolvedNodeDescribedInfo { + build_info: Result, + roles: Result, + // TODO: in the future make it return a Result as well. + auxiliary_details: AuxiliaryDetails, + websockets: Result, + network_requester: Result, NodeDescribeCacheError>, + ipr: Option, + authenticator: Option, + wireguard: Option, +} + +impl ResolvedNodeDescribedInfo { + fn try_unwrap(self) -> Result { + Ok(UnwrappedResolvedNodeDescribedInfo { + build_info: self.build_info?, + roles: self.roles?, + auxiliary_details: self.auxiliary_details, + websockets: self.websockets?, + network_requester: self.network_requester?, + ipr: self.ipr, + authenticator: self.authenticator, + wireguard: self.wireguard, + }) + } +} + +pub(crate) struct UnwrappedResolvedNodeDescribedInfo { + pub(crate) build_info: BinaryBuildInformationOwned, + pub(crate) roles: NodeRoles, + pub(crate) auxiliary_details: AuxiliaryDetails, + pub(crate) websockets: WebSockets, + pub(crate) network_requester: Option, + pub(crate) ipr: Option, + pub(crate) authenticator: Option, + pub(crate) wireguard: Option, +} + +impl UnwrappedResolvedNodeDescribedInfo { + pub(crate) fn into_node_description( + self, + host_info: impl Into, + ) -> NymNodeData { + NymNodeData { + host_information: host_info.into(), + last_polled: OffsetDateTime::now_utc().into(), + build_information: self.build_info, + network_requester: self.network_requester, + ip_packet_router: self.ipr, + authenticator: self.authenticator, + wireguard: self.wireguard, + mixnet_websockets: self.websockets, + auxiliary_details: self.auxiliary_details, + declared_role: self.roles, + } + } +} diff --git a/nym-api/src/node_status_api/cache/data.rs b/nym-api/src/node_status_api/cache/data.rs index b2e6512417..d8b2cda51e 100644 --- a/nym-api/src/node_status_api/cache/data.rs +++ b/nym-api/src/node_status_api/cache/data.rs @@ -1,9 +1,9 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use nym_api_requests::models::{GatewayBondAnnotated, MixNodeBondAnnotated}; +use nym_api_requests::models::{GatewayBondAnnotated, MixNodeBondAnnotated, NodeAnnotation}; use nym_contracts_common::IdentityKey; -use nym_mixnet_contract_common::MixId; +use nym_mixnet_contract_common::NodeId; use std::collections::HashMap; use crate::support::caching::Cache; @@ -12,11 +12,14 @@ use super::inclusion_probabilities::InclusionProbabilities; #[derive(Default)] pub(crate) struct NodeStatusCacheData { - pub(crate) mixnodes_annotated: Cache>, - pub(crate) rewarded_set_annotated: Cache>, - pub(crate) active_set_annotated: Cache>, + pub(crate) legacy_gateway_mapping: Cache>, - pub(crate) gateways_annotated: Cache>, + /// Basic annotation for **all** nodes, i.e. legacy + nym-nodes + pub(crate) node_annotations: Cache>, + + /// Annotations as before, just for legacy things + pub(crate) mixnodes_annotated: Cache>, + pub(crate) gateways_annotated: Cache>, // Estimated active set inclusion probabilities from Monte Carlo simulation pub(crate) inclusion_probabilities: Cache, diff --git a/nym-api/src/node_status_api/cache/inclusion_probabilities.rs b/nym-api/src/node_status_api/cache/inclusion_probabilities.rs index 4d53793d36..28bde0e1e2 100644 --- a/nym-api/src/node_status_api/cache/inclusion_probabilities.rs +++ b/nym-api/src/node_status_api/cache/inclusion_probabilities.rs @@ -1,12 +1,13 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use nym_api_requests::legacy::LegacyMixNodeDetailsWithLayer; use nym_api_requests::models::InclusionProbability; use nym_contracts_common::truncate_decimal; -use nym_mixnet_contract_common::{MixId, MixNodeDetails, RewardingParams}; +use nym_mixnet_contract_common::{NodeId, RewardingParams}; use serde::Serialize; use std::time::Duration; -use tap::TapFallible; +use tracing::error; const MAX_SIMULATION_SAMPLES: u64 = 5000; const MAX_SIMULATION_TIME_SEC: u64 = 15; @@ -22,13 +23,13 @@ pub(crate) struct InclusionProbabilities { impl InclusionProbabilities { pub(crate) fn compute( - mixnodes: &[MixNodeDetails], + mixnodes: &[LegacyMixNodeDetailsWithLayer], params: RewardingParams, ) -> Option { compute_inclusion_probabilities(mixnodes, params) } - pub(crate) fn node(&self, mix_id: MixId) -> Option<&InclusionProbability> { + pub(crate) fn node(&self, mix_id: NodeId) -> Option<&InclusionProbability> { self.inclusion_probabilities .iter() .find(|x| x.mix_id == mix_id) @@ -36,11 +37,11 @@ impl InclusionProbabilities { } fn compute_inclusion_probabilities( - mixnodes: &[MixNodeDetails], + mixnodes: &[LegacyMixNodeDetailsWithLayer], params: RewardingParams, ) -> Option { - let active_set_size = params.active_set_size; - let standby_set_size = params.rewarded_set_size - active_set_size; + let active_set_size = params.active_set_size(); + let standby_set_size = params.rewarded_set.standby; // Unzip list of total bonds into ids and bonds. // We need to go through this zip/unzip procedure to make sure we have matching identities @@ -57,7 +58,7 @@ fn compute_inclusion_probabilities( Duration::from_secs(MAX_SIMULATION_TIME_SEC), &mut rng, ) - .tap_err(|err| error!("{err}")) + .inspect_err(|err| error!("{err}")) .ok()?; Some(InclusionProbabilities { @@ -69,7 +70,9 @@ fn compute_inclusion_probabilities( }) } -fn unzip_into_mixnode_ids_and_total_bonds(mixnodes: &[MixNodeDetails]) -> (Vec, Vec) { +fn unzip_into_mixnode_ids_and_total_bonds( + mixnodes: &[LegacyMixNodeDetailsWithLayer], +) -> (Vec, Vec) { mixnodes .iter() .map(|m| (m.mix_id(), truncate_decimal(m.total_stake()).u128())) @@ -77,7 +80,7 @@ fn unzip_into_mixnode_ids_and_total_bonds(mixnodes: &[MixNodeDetails]) -> (Vec Vec { ids.iter() diff --git a/nym-api/src/node_status_api/cache/mod.rs b/nym-api/src/node_status_api/cache/mod.rs index 99d4358802..262755ef3b 100644 --- a/nym-api/src/node_status_api/cache/mod.rs +++ b/nym-api/src/node_status_api/cache/mod.rs @@ -4,15 +4,15 @@ use self::data::NodeStatusCacheData; use self::inclusion_probabilities::InclusionProbabilities; use crate::support::caching::Cache; -use nym_api_requests::models::{GatewayBondAnnotated, MixNodeBondAnnotated, MixnodeStatus}; -use nym_contracts_common::{IdentityKey, IdentityKeyRef}; -use nym_mixnet_contract_common::MixId; -use rocket::fairing::AdHoc; +use nym_api_requests::models::{GatewayBondAnnotated, MixNodeBondAnnotated, NodeAnnotation}; +use nym_contracts_common::IdentityKey; +use nym_mixnet_contract_common::NodeId; use std::collections::HashMap; use std::{sync::Arc, time::Duration}; use thiserror::Error; use tokio::sync::RwLockReadGuard; use tokio::{sync::RwLock, time}; +use tracing::error; const CACHE_TIMEOUT_MS: u64 = 100; @@ -47,27 +47,22 @@ impl NodeStatusCache { } } - #[deprecated(note = "TODO rocket: obsolete because it's used for Rocket")] - pub fn stage() -> AdHoc { - AdHoc::on_ignite("Node Status Cache", |rocket| async { - rocket.manage(Self::new()) - }) - } - /// Updates the cache with the latest data. async fn update( &self, - mixnodes: HashMap, - rewarded_set: Vec, - active_set: Vec, - gateways: HashMap, + legacy_gateway_mapping: HashMap, + node_annotations: HashMap, + mixnodes: HashMap, + gateways: HashMap, inclusion_probabilities: InclusionProbabilities, ) { match time::timeout(Duration::from_millis(CACHE_TIMEOUT_MS), self.inner.write()).await { Ok(mut cache) => { cache.mixnodes_annotated.unchecked_update(mixnodes); - cache.rewarded_set_annotated.unchecked_update(rewarded_set); - cache.active_set_annotated.unchecked_update(active_set); + cache + .legacy_gateway_mapping + .unchecked_update(legacy_gateway_mapping); + cache.node_annotations.unchecked_update(node_annotations); cache.gateways_annotated.unchecked_update(gateways); cache .inclusion_probabilities @@ -104,10 +99,25 @@ impl NodeStatusCache { } } - pub(crate) async fn active_mixnodes_cache( + pub(crate) async fn node_annotations( &self, - ) -> Option>>> { - self.get(|c| &c.active_set_annotated).await + ) -> Option>>> { + self.get(|c| &c.node_annotations).await + } + + pub(crate) async fn map_identity_to_node_id(&self, identity: &str) -> Option { + self.inner + .read() + .await + .legacy_gateway_mapping + .get(identity) + .copied() + } + + pub(crate) async fn annotated_legacy_mixnodes( + &self, + ) -> Option>>> { + self.get(|c| &c.mixnodes_annotated).await } pub(crate) async fn mixnodes_annotated_full(&self) -> Option> { @@ -122,24 +132,14 @@ impl NodeStatusCache { Some(full.iter().filter(|m| !m.blacklisted).cloned().collect()) } - pub(crate) async fn mixnode_annotated(&self, mix_id: MixId) -> Option { + pub(crate) async fn mixnode_annotated(&self, mix_id: NodeId) -> Option { let mixnodes = self.get(|c| &c.mixnodes_annotated).await?; mixnodes.get(&mix_id).cloned() } - pub(crate) async fn rewarded_set_annotated(&self) -> Option>> { - self.get_owned(|c| c.rewarded_set_annotated.clone_cache()) - .await - } - - pub(crate) async fn active_set_annotated(&self) -> Option>> { - self.get_owned(|c| c.active_set_annotated.clone_cache()) - .await - } - - pub(crate) async fn gateways_cache( + pub(crate) async fn annotated_legacy_gateways( &self, - ) -> Option>>> { + ) -> Option>>> { self.get(|c| &c.gateways_annotated).await } @@ -155,41 +155,13 @@ impl NodeStatusCache { Some(full.iter().filter(|m| !m.blacklisted).cloned().collect()) } - pub(crate) async fn gateway_annotated( - &self, - gateway_id: IdentityKeyRef<'_>, - ) -> Option { + pub(crate) async fn gateway_annotated(&self, node_id: NodeId) -> Option { let gateways = self.get(|c| &c.gateways_annotated).await?; - gateways.get(gateway_id).cloned() + gateways.get(&node_id).cloned() } pub(crate) async fn inclusion_probabilities(&self) -> Option> { self.get_owned(|c| c.inclusion_probabilities.clone_cache()) .await } - - pub async fn mixnode_details( - &self, - mix_id: MixId, - ) -> (Option, MixnodeStatus) { - // it might not be the most optimal to possibly iterate the entire vector to find (or not) - // the relevant value. However, the vectors are relatively small (< 10_000 elements, < 1000 for active set) - - let active_set = &self.active_set_annotated().await.unwrap().into_inner(); - if let Some(bond) = active_set.iter().find(|mix| mix.mix_id() == mix_id) { - return (Some(bond.clone()), MixnodeStatus::Active); - } - - let rewarded_set = &self.rewarded_set_annotated().await.unwrap().into_inner(); - if let Some(bond) = rewarded_set.iter().find(|mix| mix.mix_id() == mix_id) { - return (Some(bond.clone()), MixnodeStatus::Standby); - } - - let all_bonded = &self.mixnodes_annotated_filtered().await.unwrap(); - if let Some(bond) = all_bonded.iter().find(|mix| mix.mix_id() == mix_id) { - (Some(bond.clone()), MixnodeStatus::Inactive) - } else { - (None, MixnodeStatus::NotFound) - } - } } diff --git a/nym-api/src/node_status_api/cache/node_sets.rs b/nym-api/src/node_status_api/cache/node_sets.rs index d20f3329b4..b60f4d34a6 100644 --- a/nym-api/src/node_status_api/cache/node_sets.rs +++ b/nym-api/src/node_status_api/cache/node_sets.rs @@ -1,59 +1,23 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use crate::node_status_api::helpers::RewardedSetStatus; use crate::node_status_api::reward_estimate::{compute_apy_from_reward, compute_reward_estimate}; use crate::support::storage::NymApiStorage; -use nym_api_requests::models::{GatewayBondAnnotated, MixNodeBondAnnotated, NodePerformance}; -use nym_mixnet_contract_common::families::FamilyHead; -use nym_mixnet_contract_common::{reward_params::Performance, Interval, MixId}; -use nym_mixnet_contract_common::{ - GatewayBond, IdentityKey, MixNodeDetails, RewardedSetNodeStatus, RewardingParams, +use nym_api_requests::legacy::{LegacyGatewayBondWithId, LegacyMixNodeDetailsWithLayer}; +use nym_api_requests::models::{ + GatewayBondAnnotated, MixNodeBondAnnotated, NodeAnnotation, NodePerformance, }; +use nym_mixnet_contract_common::{reward_params::Performance, Interval, NodeId, RewardedSet}; +use nym_mixnet_contract_common::{NymNodeDetails, RewardingParams}; use nym_topology::NetworkAddress; use std::collections::{HashMap, HashSet}; use std::net::ToSocketAddrs; use std::str::FromStr; -pub(super) fn to_rewarded_set_node_status( - rewarded_set: &[MixNodeDetails], - active_set: &[MixNodeDetails], -) -> HashMap { - let mut rewarded_set_node_status: HashMap = rewarded_set - .iter() - .map(|m| (m.mix_id(), RewardedSetNodeStatus::Standby)) - .collect(); - for mixnode in active_set { - *rewarded_set_node_status - .get_mut(&mixnode.mix_id()) - .expect("All active nodes are rewarded nodes") = RewardedSetNodeStatus::Active; - } - rewarded_set_node_status -} - -pub(super) fn split_into_active_and_rewarded_set( - mixnodes_annotated: &HashMap, - rewarded_set_node_status: &HashMap, -) -> (Vec, Vec) { - let rewarded_set: Vec<_> = mixnodes_annotated - .values() - .filter(|mixnode| rewarded_set_node_status.get(&mixnode.mix_id()).is_some()) - .cloned() - .collect(); - let active_set: Vec<_> = rewarded_set - .iter() - .filter(|mixnode| { - rewarded_set_node_status - .get(&mixnode.mix_id()) - .map_or(false, RewardedSetNodeStatus::is_active) - }) - .cloned() - .collect(); - (rewarded_set, active_set) -} - pub(super) async fn get_mixnode_performance_from_storage( storage: &NymApiStorage, - mix_id: MixId, + mix_id: NodeId, epoch: Interval, ) -> Option { storage @@ -68,12 +32,12 @@ pub(super) async fn get_mixnode_performance_from_storage( pub(super) async fn get_gateway_performance_from_storage( storage: &NymApiStorage, - gateway_id: &str, + node_id: NodeId, epoch: Interval, ) -> Option { storage .get_average_gateway_uptime_in_the_last_24hrs( - gateway_id, + node_id, epoch.current_epoch_end_unix_timestamp(), ) .await @@ -81,19 +45,25 @@ pub(super) async fn get_gateway_performance_from_storage( .map(Into::into) } -pub(super) async fn annotate_nodes_with_details( +// TODO: this might have to be moved to a different file if other places also rely on this functionality +fn get_rewarded_set_status(rewarded_set: &RewardedSet, node_id: NodeId) -> RewardedSetStatus { + if rewarded_set.is_standby(&node_id) { + RewardedSetStatus::Standby + } else if rewarded_set.is_active_mixnode(&node_id) { + RewardedSetStatus::Active + } else { + RewardedSetStatus::Inactive + } +} + +pub(super) async fn annotate_legacy_mixnodes_nodes_with_details( storage: &NymApiStorage, - mixnodes: Vec, + mixnodes: Vec, interval_reward_params: RewardingParams, current_interval: Interval, - rewarded_set: &HashMap, - mix_to_family: Vec<(IdentityKey, FamilyHead)>, - blacklist: &HashSet, -) -> HashMap { - let mix_to_family = mix_to_family - .into_iter() - .collect::>(); - + rewarded_set: &RewardedSet, + blacklist: &HashSet, +) -> HashMap { let mut annotated = HashMap::new(); for mixnode in mixnodes { let stake_saturation = mixnode @@ -104,7 +74,7 @@ pub(super) async fn annotate_nodes_with_details( .rewarding_details .uncapped_bond_saturation(&interval_reward_params); - let rewarded_set_status = rewarded_set.get(&mixnode.mix_id()).copied(); + let rewarded_set_status = get_rewarded_set_status(rewarded_set, mixnode.mix_id()); // If the performance can't be obtained, because the nym-api was not started with // the monitoring (and hence, storage), then reward estimates will be all zero @@ -147,10 +117,6 @@ pub(super) async fn annotate_nodes_with_details( let (estimated_operator_apy, estimated_delegators_apy) = compute_apy_from_reward(&mixnode, reward_estimate, current_interval); - let family = mix_to_family - .get(mixnode.bond_information.identity()) - .cloned(); - annotated.insert( mixnode.mix_id(), MixNodeBondAnnotated { @@ -162,7 +128,6 @@ pub(super) async fn annotate_nodes_with_details( node_performance, estimated_operator_apy, estimated_delegators_apy, - family, ip_addresses, }, ); @@ -170,35 +135,33 @@ pub(super) async fn annotate_nodes_with_details( annotated } -pub(crate) async fn annotate_gateways_with_details( +pub(crate) async fn annotate_legacy_gateways_with_details( storage: &NymApiStorage, - gateway_bonds: Vec, + gateway_bonds: Vec, current_interval: Interval, - blacklist: &HashSet, -) -> HashMap { + blacklist: &HashSet, +) -> HashMap { let mut annotated = HashMap::new(); for gateway_bond in gateway_bonds { - let performance = get_gateway_performance_from_storage( - storage, - gateway_bond.identity(), - current_interval, - ) - .await - .unwrap_or_default(); + let performance = + get_gateway_performance_from_storage(storage, gateway_bond.node_id, current_interval) + .await + .unwrap_or_default(); let node_performance = storage - .construct_gateway_report(gateway_bond.identity()) + .construct_gateway_report(gateway_bond.node_id) .await .map(NodePerformance::from) .ok() .unwrap_or_default(); // safety: this conversion is infallible - let ip_addresses = match NetworkAddress::from_str(&gateway_bond.gateway.host).unwrap() { + let ip_addresses = match NetworkAddress::from_str(&gateway_bond.bond.gateway.host).unwrap() + { NetworkAddress::IpAddr(ip) => vec![ip], NetworkAddress::Hostname(hostname) => { // try to resolve it - (hostname.as_str(), gateway_bond.gateway.mix_port) + (hostname.as_str(), gateway_bond.bond.gateway.mix_port) .to_socket_addrs() .map(|iter| iter.map(|s| s.ip()).collect::>()) .unwrap_or_default() @@ -206,9 +169,9 @@ pub(crate) async fn annotate_gateways_with_details( }; annotated.insert( - gateway_bond.identity().to_string(), + gateway_bond.node_id, GatewayBondAnnotated { - blacklisted: blacklist.contains(&gateway_bond.gateway.identity_key), + blacklisted: blacklist.contains(&gateway_bond.node_id), gateway_bond, self_described: None, performance, @@ -219,3 +182,72 @@ pub(crate) async fn annotate_gateways_with_details( } annotated } + +pub(crate) async fn produce_node_annotations( + storage: &NymApiStorage, + legacy_mixnodes: &[LegacyMixNodeDetailsWithLayer], + legacy_gateways: &[LegacyGatewayBondWithId], + nym_nodes: &[NymNodeDetails], + current_interval: Interval, +) -> HashMap { + let mut annotations = HashMap::new(); + + for legacy_mix in legacy_mixnodes { + let perf = storage + .get_average_mixnode_uptime_in_the_last_24hrs( + legacy_mix.mix_id(), + current_interval.current_epoch_end_unix_timestamp(), + ) + .await + .ok() + .unwrap_or_default() + .into(); + + annotations.insert( + legacy_mix.mix_id(), + NodeAnnotation { + last_24h_performance: perf, + }, + ); + } + + for legacy_gateway in legacy_gateways { + let perf = storage + .get_average_gateway_uptime_in_the_last_24hrs( + legacy_gateway.node_id, + current_interval.current_epoch_end_unix_timestamp(), + ) + .await + .ok() + .unwrap_or_default() + .into(); + + annotations.insert( + legacy_gateway.node_id, + NodeAnnotation { + last_24h_performance: perf, + }, + ); + } + + for nym_node in nym_nodes { + let perf = storage + .get_average_node_uptime_in_the_last_24hrs( + nym_node.node_id(), + current_interval.current_epoch_end_unix_timestamp(), + ) + .await + .ok() + .unwrap_or_default() + .into(); + + annotations.insert( + nym_node.node_id(), + NodeAnnotation { + last_24h_performance: perf, + }, + ); + } + + annotations +} diff --git a/nym-api/src/node_status_api/cache/refresher.rs b/nym-api/src/node_status_api/cache/refresher.rs index ad07919e62..aa664885a9 100644 --- a/nym-api/src/node_status_api/cache/refresher.rs +++ b/nym-api/src/node_status_api/cache/refresher.rs @@ -2,12 +2,12 @@ // SPDX-License-Identifier: GPL-3.0-only use super::NodeStatusCache; +use crate::node_status_api::cache::node_sets::produce_node_annotations; use crate::{ node_status_api::cache::{ inclusion_probabilities::InclusionProbabilities, node_sets::{ - annotate_gateways_with_details, annotate_nodes_with_details, - split_into_active_and_rewarded_set, to_rewarded_set_node_status, + annotate_legacy_gateways_with_details, annotate_legacy_mixnodes_nodes_with_details, }, NodeStatusCacheError, }, @@ -16,9 +16,11 @@ use crate::{ support::caching::CacheNotification, }; use nym_task::TaskClient; +use std::collections::HashMap; use std::time::Duration; use tokio::sync::watch; use tokio::time; +use tracing::{debug, error, info, trace}; // Long running task responsible for keeping the node status cache up-to-date. pub struct NodeStatusCacheRefresher { @@ -56,14 +58,14 @@ impl NodeStatusCacheRefresher { tokio::select! { biased; _ = shutdown.recv() => { - log::trace!("NodeStatusCacheRefresher: Received shutdown"); + trace!("NodeStatusCacheRefresher: Received shutdown"); } // Update node status cache when the contract cache / validator cache is updated Ok(_) = self.contract_cache_listener.changed() => { tokio::select! { _ = self.update_on_notify(&mut fallback_interval) => (), _ = shutdown.recv() => { - log::trace!("NodeStatusCacheRefresher: Received shutdown"); + trace!("NodeStatusCacheRefresher: Received shutdown"); } } } @@ -73,18 +75,18 @@ impl NodeStatusCacheRefresher { tokio::select! { _ = self.update_on_timer() => (), _ = shutdown.recv() => { - log::trace!("NodeStatusCacheRefresher: Received shutdown"); + trace!("NodeStatusCacheRefresher: Received shutdown"); } } } } } - log::info!("NodeStatusCacheRefresher: Exiting"); + info!("NodeStatusCacheRefresher: Exiting"); } /// Updates the node status cache when the contract cache / validator cache is updated async fn update_on_notify(&self, fallback_interval: &mut time::Interval) { - log::debug!( + debug!( "Validator cache event detected: {:?}", &*self.contract_cache_listener.borrow(), ); @@ -94,31 +96,28 @@ impl NodeStatusCacheRefresher { /// Updates the node status cache when the fallback interval is reached async fn update_on_timer(&self) { - log::debug!("Timed trigger for the node status cache"); + debug!("Timed trigger for the node status cache"); let have_contract_cache_data = *self.contract_cache_listener.borrow() != CacheNotification::Start; if have_contract_cache_data { let _ = self.refresh().await; } else { - log::trace!( - "Skipping updating node status cache, is the contract cache not yet available?" - ); + trace!("Skipping updating node status cache, is the contract cache not yet available?"); } } /// Refreshes the node status cache by fetching the latest data from the contract cache async fn refresh(&self) -> Result<(), NodeStatusCacheError> { - log::info!("Updating node status cache"); + info!("Updating node status cache"); // Fetch contract cache data to work with - let mixnode_details = self.contract_cache.mixnodes_all().await; + let mixnode_details = self.contract_cache.legacy_mixnodes_all().await; let interval_reward_params = self.contract_cache.interval_reward_params().await; let current_interval = self.contract_cache.current_interval().await; - let rewarded_set = self.contract_cache.rewarded_set().await; - let active_set = self.contract_cache.active_set().await; - let mix_to_family = self.contract_cache.mix_to_family().await; - let gateway_bonds = self.contract_cache.gateways_all().await; + let rewarded_set = self.contract_cache.rewarded_set_owned().await; + let gateway_bonds = self.contract_cache.legacy_gateways_all().await; + let nym_nodes = self.contract_cache.nym_nodes().await; // get blacklists let mixnodes_blacklist = self.contract_cache.mixnodes_blacklist().await; @@ -138,24 +137,33 @@ impl NodeStatusCacheRefresher { NodeStatusCacheError::SimulationFailed })?; + let mut legacy_gateway_mapping = HashMap::new(); + for gateway in &gateway_bonds { + legacy_gateway_mapping.insert(gateway.identity().clone(), gateway.node_id); + } + // Create annotated data - let rewarded_set_node_status = to_rewarded_set_node_status(&rewarded_set, &active_set); - let mixnodes_annotated = annotate_nodes_with_details( + + let node_annotations = produce_node_annotations( + &self.storage, + &mixnode_details, + &gateway_bonds, + &nym_nodes, + current_interval, + ) + .await; + + let mixnodes_annotated = annotate_legacy_mixnodes_nodes_with_details( &self.storage, mixnode_details, interval_reward_params, current_interval, - &rewarded_set_node_status, - mix_to_family.to_vec(), + &rewarded_set, &mixnodes_blacklist, ) .await; - // Create the annotated rewarded and active sets - let (rewarded_set, active_set) = - split_into_active_and_rewarded_set(&mixnodes_annotated, &rewarded_set_node_status); - - let gateways_annotated = annotate_gateways_with_details( + let gateways_annotated = annotate_legacy_gateways_with_details( &self.storage, gateway_bonds, current_interval, @@ -166,9 +174,9 @@ impl NodeStatusCacheRefresher { // Update the cache self.cache .update( + legacy_gateway_mapping, + node_annotations, mixnodes_annotated, - rewarded_set, - active_set, gateways_annotated, inclusion_probabilities, ) diff --git a/nym-api/src/node_status_api/handlers/mod.rs b/nym-api/src/node_status_api/handlers/mod.rs index 2a385b2812..f42cd59d84 100644 --- a/nym-api/src/node_status_api/handlers/mod.rs +++ b/nym-api/src/node_status_api/handlers/mod.rs @@ -1,9 +1,9 @@ // Copyright 2021-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::v2::AxumAppState; +use crate::support::http::state::AppState; use axum::Router; -use nym_mixnet_contract_common::MixId; +use nym_mixnet_contract_common::NodeId; use serde::Deserialize; use utoipa::IntoParams; @@ -11,7 +11,7 @@ pub(crate) mod network_monitor; pub(crate) mod unstable; pub(crate) mod without_monitor; -pub(crate) fn node_status_routes(network_monitor: bool) -> Router { +pub(crate) fn node_status_routes(network_monitor: bool) -> Router { // in the minimal variant we would not have access to endpoints relying on existence // of the network monitor and the associated storage let without_network_monitor = without_monitor::mandatory_routes(); @@ -28,5 +28,5 @@ pub(crate) fn node_status_routes(network_monitor: bool) -> Router #[derive(Deserialize, IntoParams)] #[into_params(parameter_in = Path)] struct MixIdParam { - mix_id: MixId, + mix_id: NodeId, } diff --git a/nym-api/src/node_status_api/handlers/network_monitor.rs b/nym-api/src/node_status_api/handlers/network_monitor.rs index ea38422c17..5f71eac079 100644 --- a/nym-api/src/node_status_api/handlers/network_monitor.rs +++ b/nym-api/src/node_status_api/handlers/network_monitor.rs @@ -4,13 +4,13 @@ use crate::node_status_api::handlers::MixIdParam; use crate::node_status_api::helpers::{ _compute_mixnode_reward_estimation, _gateway_core_status_count, _gateway_report, - _gateway_uptime_history, _get_gateway_avg_uptime, _get_gateways_detailed, - _get_gateways_detailed_unfiltered, _get_mixnode_avg_uptime, _get_mixnode_reward_estimation, - _get_mixnodes_detailed_unfiltered, _mixnode_core_status_count, _mixnode_report, - _mixnode_uptime_history, + _gateway_uptime_history, _get_gateway_avg_uptime, _get_legacy_gateways_detailed, + _get_legacy_gateways_detailed_unfiltered, _get_mixnode_avg_uptime, + _get_mixnode_reward_estimation, _get_mixnodes_detailed_unfiltered, _mixnode_core_status_count, + _mixnode_report, _mixnode_uptime_history, }; use crate::node_status_api::models::AxumResult; -use crate::v2::AxumAppState; +use crate::support::http::state::AppState; use axum::extract::{Path, Query, State}; use axum::Json; use axum::Router; @@ -25,7 +25,9 @@ use utoipa::IntoParams; use super::unstable; -pub(super) fn network_monitor_routes() -> Router { +// we want to mark the routes as deprecated in swagger, but still expose them +#[allow(deprecated)] +pub(super) fn network_monitor_routes() -> Router { Router::new() .nest( "/gateway/:identity", @@ -92,9 +94,10 @@ pub(super) fn network_monitor_routes() -> Router { (status = 200, body = GatewayStatusReportResponse) ) )] +#[deprecated] async fn gateway_report( Path(identity): Path, - State(state): State, + State(state): State, ) -> AxumResult> { Ok(Json( _gateway_report(state.node_status_cache(), &identity).await?, @@ -109,12 +112,13 @@ async fn gateway_report( (status = 200, body = GatewayUptimeHistoryResponse) ) )] +#[deprecated] async fn gateway_uptime_history( Path(identity): Path, - State(state): State, + State(state): State, ) -> AxumResult> { Ok(Json( - _gateway_uptime_history(state.storage(), &identity).await?, + _gateway_uptime_history(state.storage(), state.nym_contract_cache(), &identity).await?, )) } @@ -135,10 +139,11 @@ struct SinceQueryParams { (status = 200, body = GatewayCoreStatusResponse) ) )] +#[deprecated] async fn gateway_core_status_count( Path(identity): Path, Query(SinceQueryParams { since }): Query, - State(state): State, + State(state): State, ) -> AxumResult> { Ok(Json( _gateway_core_status_count(state.storage(), &identity, since).await?, @@ -153,9 +158,10 @@ async fn gateway_core_status_count( (status = 200, body = GatewayUptimeResponse) ) )] +#[deprecated] async fn get_gateway_avg_uptime( Path(identity): Path, - State(state): State, + State(state): State, ) -> AxumResult> { Ok(Json( _get_gateway_avg_uptime(state.node_status_cache(), &identity).await?, @@ -173,9 +179,10 @@ async fn get_gateway_avg_uptime( (status = 200, body = MixnodeStatusReportResponse) ) )] +#[deprecated] async fn mixnode_report( Path(MixIdParam { mix_id }): Path, - State(state): State, + State(state): State, ) -> AxumResult> { Ok(Json( _mixnode_report(state.node_status_cache(), mix_id).await?, @@ -193,12 +200,13 @@ async fn mixnode_report( (status = 200, body = MixnodeUptimeHistoryResponse) ) )] +#[deprecated] async fn mixnode_uptime_history( Path(MixIdParam { mix_id }): Path, - State(state): State, + State(state): State, ) -> AxumResult> { Ok(Json( - _mixnode_uptime_history(state.storage(), mix_id).await?, + _mixnode_uptime_history(state.storage(), state.nym_contract_cache(), mix_id).await?, )) } @@ -213,10 +221,11 @@ async fn mixnode_uptime_history( (status = 200, body = MixnodeCoreStatusResponse) ) )] +#[deprecated] async fn mixnode_core_status_count( Path(MixIdParam { mix_id }): Path, Query(SinceQueryParams { since }): Query, - State(state): State, + State(state): State, ) -> AxumResult> { Ok(Json( _mixnode_core_status_count(state.storage(), mix_id, since).await?, @@ -234,9 +243,10 @@ async fn mixnode_core_status_count( (status = 200, body = RewardEstimationResponse) ) )] +#[deprecated] async fn get_mixnode_reward_estimation( Path(MixIdParam { mix_id }): Path, - State(state): State, + State(state): State, ) -> AxumResult> { Ok(Json( _get_mixnode_reward_estimation( @@ -260,9 +270,10 @@ async fn get_mixnode_reward_estimation( (status = 200, body = RewardEstimationResponse) ) )] +#[deprecated] async fn compute_mixnode_reward_estimation( Path(MixIdParam { mix_id }): Path, - State(state): State, + State(state): State, Json(user_reward_param): Json, ) -> AxumResult> { Ok(Json( @@ -287,9 +298,10 @@ async fn compute_mixnode_reward_estimation( (status = 200, body = UptimeResponse) ) )] +#[deprecated] async fn get_mixnode_avg_uptime( Path(MixIdParam { mix_id }): Path, - State(state): State, + State(state): State, ) -> AxumResult> { Ok(Json( _get_mixnode_avg_uptime(state.node_status_cache(), mix_id).await?, @@ -304,8 +316,9 @@ async fn get_mixnode_avg_uptime( (status = 200, body = MixNodeBondAnnotated) ) )] +#[deprecated] pub async fn get_mixnodes_detailed_unfiltered( - State(state): State, + State(state): State, ) -> Json> { Json(_get_mixnodes_detailed_unfiltered(state.node_status_cache()).await) } @@ -318,10 +331,11 @@ pub async fn get_mixnodes_detailed_unfiltered( (status = 200, body = GatewayBondAnnotated) ) )] +#[deprecated] pub async fn get_gateways_detailed( - State(state): State, + State(state): State, ) -> Json> { - Json(_get_gateways_detailed(state.node_status_cache()).await) + Json(_get_legacy_gateways_detailed(state.node_status_cache()).await) } #[utoipa::path( @@ -332,8 +346,9 @@ pub async fn get_gateways_detailed( (status = 200, body = GatewayBondAnnotated) ) )] +#[deprecated] pub async fn get_gateways_detailed_unfiltered( - State(state): State, + State(state): State, ) -> Json> { - Json(_get_gateways_detailed_unfiltered(state.node_status_cache()).await) + Json(_get_legacy_gateways_detailed_unfiltered(state.node_status_cache()).await) } diff --git a/nym-api/src/node_status_api/handlers/unstable.rs b/nym-api/src/node_status_api/handlers/unstable.rs index b8a30639b4..7e30a4ca31 100644 --- a/nym-api/src/node_status_api/handlers/unstable.rs +++ b/nym-api/src/node_status_api/handlers/unstable.rs @@ -3,19 +3,20 @@ use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; use crate::support::http::helpers::PaginationRequest; +use crate::support::http::state::AppState; use crate::support::storage::NymApiStorage; -use crate::v2::AxumAppState; use axum::extract::{Path, Query, State}; use axum::Json; use nym_api_requests::models::{ GatewayTestResultResponse, MixnodeTestResultResponse, PartialTestResult, TestNode, TestRoute, }; use nym_api_requests::pagination::Pagination; -use nym_mixnet_contract_common::MixId; +use nym_mixnet_contract_common::NodeId; use std::cmp::min; use std::collections::HashMap; use std::sync::Arc; use tokio::sync::RwLock; +use tracing::{error, trace}; pub type DbId = i64; @@ -103,7 +104,7 @@ const MAX_TEST_RESULTS_PAGE_SIZE: u32 = 100; const DEFAULT_TEST_RESULTS_PAGE_SIZE: u32 = 50; async fn _mixnode_test_results( - mix_id: MixId, + mix_id: NodeId, page: u32, per_page: u32, info_cache: &NodeInfoCache, @@ -162,9 +163,9 @@ async fn _mixnode_test_results( } pub async fn mixnode_test_results( - Path(mix_id): Path, + Path(mix_id): Path, Query(pagination): Query, - State(state): State, + State(state): State, ) -> AxumResult> { let page = pagination.page.unwrap_or_default(); let per_page = min( @@ -252,7 +253,7 @@ async fn _gateway_test_results( pub async fn gateway_test_results( Path(gateway_identity): Path, Query(pagination): Query, - State(state): State, + State(state): State, ) -> AxumResult> { let page = pagination.page.unwrap_or_default(); let per_page = min( diff --git a/nym-api/src/node_status_api/handlers/without_monitor.rs b/nym-api/src/node_status_api/handlers/without_monitor.rs index b431d38913..c981075a00 100644 --- a/nym-api/src/node_status_api/handlers/without_monitor.rs +++ b/nym-api/src/node_status_api/handlers/without_monitor.rs @@ -3,34 +3,44 @@ use crate::node_status_api::handlers::MixIdParam; use crate::node_status_api::helpers::{ - _get_active_set_detailed, _get_mixnode_inclusion_probabilities, - _get_mixnode_inclusion_probability, _get_mixnode_stake_saturation, _get_mixnode_status, - _get_mixnodes_detailed, _get_rewarded_set_detailed, + _get_active_set_legacy_mixnodes_detailed, _get_legacy_mixnodes_detailed, + _get_mixnode_inclusion_probabilities, _get_mixnode_inclusion_probability, + _get_mixnode_stake_saturation, _get_mixnode_status, _get_rewarded_set_legacy_mixnodes_detailed, }; -use crate::node_status_api::models::AxumResult; -use crate::v2::AxumAppState; +use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; +use crate::support::http::state::AppState; use axum::extract::{Path, State}; +use axum::routing::{get, post}; use axum::Json; use axum::Router; use nym_api_requests::models::{ AllInclusionProbabilitiesResponse, InclusionProbabilityResponse, MixNodeBondAnnotated, MixnodeStatusResponse, StakeSaturationResponse, }; -use nym_mixnet_contract_common::MixId; +use nym_mixnet_contract_common::NodeId; +use nym_types::monitoring::MonitorMessage; +use tracing::error; -pub(super) fn mandatory_routes() -> Router { +// we want to mark the routes as deprecated in swagger, but still expose them +#[allow(deprecated)] +pub(super) fn mandatory_routes() -> Router { Router::new() + .route( + "/submit-gateway-monitoring-results", + post(submit_gateway_monitoring_results), + ) + .route( + "/submit-node-monitoring-results", + post(submit_node_monitoring_results), + ) .nest( "/mixnode/:mix_id", Router::new() - .route("/status", axum::routing::get(get_mixnode_status)) - .route( - "/stake-saturation", - axum::routing::get(get_mixnode_stake_saturation), - ) + .route("/status", get(get_mixnode_status)) + .route("/stake-saturation", get(get_mixnode_stake_saturation)) .route( "/inclusion-probability", - axum::routing::get(get_mixnode_inclusion_probability), + get(get_mixnode_inclusion_probability), ), ) .merge( @@ -39,21 +49,105 @@ pub(super) fn mandatory_routes() -> Router { Router::new() .route( "/inclusion-probability", - axum::routing::get(get_mixnode_inclusion_probabilities), + get(get_mixnode_inclusion_probabilities), ) - .route("/detailed", axum::routing::get(get_mixnodes_detailed)) - .route( - "/rewarded/detailed", - axum::routing::get(get_rewarded_set_detailed), - ) - .route( - "/active/detailed", - axum::routing::get(get_active_set_detailed), - ), + .route("/detailed", get(get_mixnodes_detailed)) + .route("/rewarded/detailed", get(get_rewarded_set_detailed)) + .route("/active/detailed", get(get_active_set_detailed)), ), ) } +#[utoipa::path( + tag = "status", + post, + path = "/v1/status/submit-gateway-monitoring-results", + responses( + (status = 200), + (status = 400, body = ErrorResponse, description = "TBD"), + (status = 403, body = ErrorResponse, description = "TBD"), + (status = 500, body = ErrorResponse, description = "TBD"), + ), +)] +pub(crate) async fn submit_gateway_monitoring_results( + State(state): State, + Json(message): Json, +) -> AxumResult<()> { + if !message.is_in_allowed() { + return Err(AxumErrorResponse::forbidden( + "Monitor not registered to submit results", + )); + } + + if !message.timely() { + return Err(AxumErrorResponse::bad_request("Message is too old")); + } + + if !message.verify() { + return Err(AxumErrorResponse::bad_request("invalid signature")); + } + + match state + .storage + .manager + .submit_gateway_statuses_v2(message.results()) + .await + { + Ok(_) => Ok(()), + Err(err) => { + error!("failed to submit gateway monitoring results: {err}"); + Err(AxumErrorResponse::internal_msg( + "failed to submit gateway monitoring results", + )) + } + } +} + +#[utoipa::path( + tag = "status", + post, + path = "/v1/status/submit-node-monitoring-results", + responses( + (status = 200), + (status = 400, body = ErrorResponse, description = "TBD"), + (status = 403, body = ErrorResponse, description = "TBD"), + (status = 500, body = ErrorResponse, description = "TBD"), + ), +)] +pub(crate) async fn submit_node_monitoring_results( + State(state): State, + Json(message): Json, +) -> AxumResult<()> { + if !message.is_in_allowed() { + return Err(AxumErrorResponse::forbidden( + "Monitor not registered to submit results", + )); + } + + if !message.timely() { + return Err(AxumErrorResponse::bad_request("Message is too old")); + } + + if !message.verify() { + return Err(AxumErrorResponse::bad_request("invalid signature")); + } + + match state + .storage + .manager + .submit_mixnode_statuses_v2(message.results()) + .await + { + Ok(_) => Ok(()), + Err(err) => { + error!("failed to submit node monitoring results: {err}"); + Err(AxumErrorResponse::internal_msg( + "failed to submit node monitoring results", + )) + } + } +} + #[utoipa::path( tag = "status", get, @@ -65,9 +159,10 @@ pub(super) fn mandatory_routes() -> Router { (status = 200, body = MixnodeStatusResponse) ) )] +#[deprecated] async fn get_mixnode_status( Path(MixIdParam { mix_id }): Path, - State(state): State, + State(state): State, ) -> Json { Json(_get_mixnode_status(state.nym_contract_cache(), mix_id).await) } @@ -83,9 +178,10 @@ async fn get_mixnode_status( (status = 200, body = StakeSaturationResponse) ) )] +#[deprecated] async fn get_mixnode_stake_saturation( - Path(mix_id): Path, - State(state): State, + Path(mix_id): Path, + State(state): State, ) -> AxumResult> { Ok(Json( _get_mixnode_stake_saturation( @@ -108,9 +204,10 @@ async fn get_mixnode_stake_saturation( (status = 200, body = InclusionProbabilityResponse) ) )] +#[deprecated] async fn get_mixnode_inclusion_probability( - Path(mix_id): Path, - State(state): State, + Path(mix_id): Path, + State(state): State, ) -> AxumResult> { Ok(Json( _get_mixnode_inclusion_probability(state.node_status_cache(), mix_id).await?, @@ -125,8 +222,9 @@ async fn get_mixnode_inclusion_probability( (status = 200, body = AllInclusionProbabilitiesResponse) ) )] +#[deprecated] async fn get_mixnode_inclusion_probabilities( - State(state): State, + State(state): State, ) -> AxumResult> { Ok(Json( _get_mixnode_inclusion_probabilities(state.node_status_cache()).await?, @@ -141,10 +239,11 @@ async fn get_mixnode_inclusion_probabilities( (status = 200, body = MixNodeBondAnnotated) ) )] +#[deprecated] pub async fn get_mixnodes_detailed( - State(state): State, + State(state): State, ) -> Json> { - Json(_get_mixnodes_detailed(state.node_status_cache()).await) + Json(_get_legacy_mixnodes_detailed(state.node_status_cache()).await) } #[utoipa::path( @@ -155,10 +254,17 @@ pub async fn get_mixnodes_detailed( (status = 200, body = MixNodeBondAnnotated) ) )] +#[deprecated] pub async fn get_rewarded_set_detailed( - State(state): State, + State(state): State, ) -> Json> { - Json(_get_rewarded_set_detailed(state.node_status_cache()).await) + Json( + _get_rewarded_set_legacy_mixnodes_detailed( + state.node_status_cache(), + state.nym_contract_cache(), + ) + .await, + ) } #[utoipa::path( @@ -169,8 +275,15 @@ pub async fn get_rewarded_set_detailed( (status = 200, body = MixNodeBondAnnotated) ) )] +#[deprecated] pub async fn get_active_set_detailed( - State(state): State, + State(state): State, ) -> Json> { - Json(_get_active_set_detailed(state.node_status_cache()).await) + Json( + _get_active_set_legacy_mixnodes_detailed( + state.node_status_cache(), + state.nym_contract_cache(), + ) + .await, + ) } diff --git a/nym-api/src/node_status_api/helpers.rs b/nym-api/src/node_status_api/helpers.rs index 0d706b1156..c3308eaee4 100644 --- a/nym-api/src/node_status_api/helpers.rs +++ b/nym-api/src/node_status_api/helpers.rs @@ -11,25 +11,62 @@ use nym_api_requests::models::{ AllInclusionProbabilitiesResponse, ComputeRewardEstParam, GatewayBondAnnotated, GatewayCoreStatusResponse, GatewayStatusReportResponse, GatewayUptimeHistoryResponse, GatewayUptimeResponse, InclusionProbabilityResponse, MixNodeBondAnnotated, - MixnodeCoreStatusResponse, MixnodeStatusReportResponse, MixnodeStatusResponse, + MixnodeCoreStatusResponse, MixnodeStatus, MixnodeStatusReportResponse, MixnodeStatusResponse, MixnodeUptimeHistoryResponse, RewardEstimationResponse, StakeSaturationResponse, UptimeResponse, }; -use nym_mixnet_contract_common::{MixId, RewardedSetNodeStatus}; +use nym_mixnet_contract_common::NodeId; + +pub(crate) enum RewardedSetStatus { + Active, + Standby, + Inactive, +} + +impl From for RewardedSetStatus { + fn from(value: MixnodeStatus) -> Self { + match value { + MixnodeStatus::Active => RewardedSetStatus::Active, + MixnodeStatus::Standby => RewardedSetStatus::Standby, + // for all intents and purposes, missing node is treated as inactive for rewarding (since it wouldn't get anything + MixnodeStatus::Inactive => RewardedSetStatus::Inactive, + MixnodeStatus::NotFound => RewardedSetStatus::Inactive, + } + } +} + +async fn gateway_identity_to_node_id( + cache: &NodeStatusCache, + identity: &str, +) -> AxumResult { + let node_id = cache + .map_identity_to_node_id(identity) + .await + .ok_or(AxumErrorResponse::not_found("gateway bond not found"))?; + Ok(node_id) +} async fn get_gateway_bond_annotated( cache: &NodeStatusCache, - identity: &str, + node_id: NodeId, ) -> AxumResult { cache - .gateway_annotated(identity) + .gateway_annotated(node_id) .await .ok_or(AxumErrorResponse::not_found("gateway bond not found")) } +async fn get_gateway_bond_annotated_by_identity( + cache: &NodeStatusCache, + identity: &str, +) -> AxumResult { + let node_id = gateway_identity_to_node_id(cache, identity).await?; + get_gateway_bond_annotated(cache, node_id).await +} + async fn get_mixnode_bond_annotated( cache: &NodeStatusCache, - mix_id: MixId, + mix_id: NodeId, ) -> AxumResult { cache .mixnode_annotated(mix_id) @@ -41,7 +78,7 @@ pub(crate) async fn _gateway_report( cache: &NodeStatusCache, identity: &str, ) -> AxumResult { - let gateway = get_gateway_bond_annotated(cache, identity).await?; + let gateway = get_gateway_bond_annotated_by_identity(cache, identity).await?; Ok(GatewayStatusReportResponse { identity: gateway.identity().to_owned(), @@ -54,13 +91,24 @@ pub(crate) async fn _gateway_report( pub(crate) async fn _gateway_uptime_history( storage: &NymApiStorage, + nym_contract_cache: &NymContractCache, identity: &str, ) -> AxumResult { - storage - .get_gateway_uptime_history(identity) + let history = storage + .get_gateway_uptime_history_by_identity(identity) .await - .map(GatewayUptimeHistoryResponse::from) - .map_err(AxumErrorResponse::not_found) + .map_err(AxumErrorResponse::not_found)?; + + let owner = nym_contract_cache + .legacy_gateway_owner(history.node_id) + .await + .ok_or_else(|| AxumErrorResponse::not_found("could not determine gateway owner"))?; + + Ok(GatewayUptimeHistoryResponse { + identity: history.identity, + owner, + history: history.history.into_iter().map(Into::into).collect(), + }) } pub(crate) async fn _gateway_core_status_count( @@ -69,7 +117,7 @@ pub(crate) async fn _gateway_core_status_count( since: Option, ) -> AxumResult { let count = storage - .get_core_gateway_status_count(identity, since) + .get_core_gateway_status_count_by_identity(identity, since) .await .map_err(AxumErrorResponse::not_found)?; @@ -81,7 +129,7 @@ pub(crate) async fn _gateway_core_status_count( pub(crate) async fn _mixnode_report( cache: &NodeStatusCache, - mix_id: MixId, + mix_id: NodeId, ) -> AxumResult { let mixnode = get_mixnode_bond_annotated(cache, mix_id).await?; @@ -97,18 +145,30 @@ pub(crate) async fn _mixnode_report( pub(crate) async fn _mixnode_uptime_history( storage: &NymApiStorage, - mix_id: MixId, + nym_contract_cache: &NymContractCache, + mix_id: NodeId, ) -> AxumResult { - storage + let history = storage .get_mixnode_uptime_history(mix_id) .await - .map(MixnodeUptimeHistoryResponse::from) - .map_err(AxumErrorResponse::not_found) + .map_err(AxumErrorResponse::not_found)?; + + let owner = nym_contract_cache + .legacy_gateway_owner(mix_id) + .await + .ok_or_else(|| AxumErrorResponse::not_found("could not determine mixnode owner"))?; + + Ok(MixnodeUptimeHistoryResponse { + mix_id, + identity: history.identity, + owner, + history: history.history.into_iter().map(Into::into).collect(), + }) } pub(crate) async fn _mixnode_core_status_count( storage: &NymApiStorage, - mix_id: MixId, + mix_id: NodeId, since: Option, ) -> AxumResult { let count = storage @@ -121,7 +181,7 @@ pub(crate) async fn _mixnode_core_status_count( pub(crate) async fn _get_mixnode_status( cache: &NymContractCache, - mix_id: MixId, + mix_id: NodeId, ) -> MixnodeStatusResponse { MixnodeStatusResponse { status: cache.mixnode_status(mix_id).await, @@ -129,157 +189,161 @@ pub(crate) async fn _get_mixnode_status( } pub(crate) async fn _get_mixnode_reward_estimation( - cache: &NodeStatusCache, - validator_cache: &NymContractCache, - mix_id: MixId, + status_cache: &NodeStatusCache, + contract_cache: &NymContractCache, + mix_id: NodeId, ) -> AxumResult { - let (mixnode, status) = cache.mixnode_details(mix_id).await; - if let Some(mixnode) = mixnode { - let reward_params = validator_cache.interval_reward_params().await; - let as_at = reward_params.timestamp(); - let reward_params = reward_params - .into_inner() - .ok_or_else(AxumErrorResponse::internal)?; - let current_interval = validator_cache - .current_interval() - .await - .into_inner() - .ok_or_else(AxumErrorResponse::internal)?; + let status = contract_cache.mixnode_status(mix_id).await; + let mixnode = status_cache + .mixnode_annotated(mix_id) + .await + .ok_or_else(|| AxumErrorResponse::not_found("mixnode bond not found"))?; - let reward_estimation = compute_reward_estimate( - &mixnode.mixnode_details, - mixnode.performance, - status.into(), - reward_params, - current_interval, - ); + let reward_params = contract_cache.interval_reward_params().await; + let as_at = reward_params.timestamp(); + let reward_params = reward_params + .into_inner() + .ok_or_else(AxumErrorResponse::internal)?; + let current_interval = contract_cache + .current_interval() + .await + .into_inner() + .ok_or_else(AxumErrorResponse::internal)?; - Ok(RewardEstimationResponse { - estimation: reward_estimation, - reward_params, - epoch: current_interval, - as_at: as_at.unix_timestamp(), - }) - } else { - Err(AxumErrorResponse::not_found("mixnode bond not found")) - } + let reward_estimation = compute_reward_estimate( + &mixnode.mixnode_details, + mixnode.performance, + status.into(), + reward_params, + current_interval, + ); + + Ok(RewardEstimationResponse { + estimation: reward_estimation, + reward_params, + epoch: current_interval, + as_at: as_at.unix_timestamp(), + }) } pub(crate) async fn _compute_mixnode_reward_estimation( user_reward_param: &ComputeRewardEstParam, - cache: &NodeStatusCache, - validator_cache: &NymContractCache, - mix_id: MixId, + status_cache: &NodeStatusCache, + contract_cache: &NymContractCache, + mix_id: NodeId, ) -> AxumResult { - let (mixnode, actual_status) = cache.mixnode_details(mix_id).await; - if let Some(mut mixnode) = mixnode { - let reward_params = validator_cache.interval_reward_params().await; - let as_at = reward_params.timestamp(); - let reward_params = reward_params - .into_inner() - .ok_or_else(AxumErrorResponse::internal)?; - let current_interval = validator_cache - .current_interval() - .await - .into_inner() - .ok_or_else(AxumErrorResponse::internal)?; + let mut mixnode = status_cache + .mixnode_annotated(mix_id) + .await + .ok_or_else(|| AxumErrorResponse::not_found("mixnode bond not found"))?; - // For these parameters we either use the provided ones, or fall back to the system ones - let performance = user_reward_param.performance.unwrap_or(mixnode.performance); + let reward_params = contract_cache.interval_reward_params().await; + let as_at = reward_params.timestamp(); + let reward_params = reward_params + .into_inner() + .ok_or_else(AxumErrorResponse::internal)?; + let current_interval = contract_cache + .current_interval() + .await + .into_inner() + .ok_or_else(AxumErrorResponse::internal)?; - let status = match user_reward_param.active_in_rewarded_set { - Some(true) => Some(RewardedSetNodeStatus::Active), - Some(false) => Some(RewardedSetNodeStatus::Standby), - None => actual_status.into(), - }; + // For these parameters we either use the provided ones, or fall back to the system ones + let performance = user_reward_param.performance.unwrap_or(mixnode.performance); - if let Some(pledge_amount) = user_reward_param.pledge_amount { - mixnode.mixnode_details.rewarding_details.operator = - Decimal::from_ratio(pledge_amount, 1u64); - } - if let Some(total_delegation) = user_reward_param.total_delegation { - mixnode.mixnode_details.rewarding_details.delegates = - Decimal::from_ratio(total_delegation, 1u64); + let status = match user_reward_param.active_in_rewarded_set { + Some(true) => RewardedSetStatus::Active, + Some(false) => RewardedSetStatus::Standby, + None => { + let actual_status = contract_cache.mixnode_status(mix_id).await; + actual_status.into() } + }; - if let Some(profit_margin_percent) = user_reward_param.profit_margin_percent { - mixnode - .mixnode_details - .rewarding_details - .cost_params - .profit_margin_percent = profit_margin_percent; - } - - if let Some(interval_operating_cost) = &user_reward_param.interval_operating_cost { - mixnode - .mixnode_details - .rewarding_details - .cost_params - .interval_operating_cost = interval_operating_cost.clone(); - } - - if mixnode.mixnode_details.rewarding_details.operator - + mixnode.mixnode_details.rewarding_details.delegates - > reward_params.interval.staking_supply - { - return Err(AxumErrorResponse::unprocessable_entity( - "Pledge plus delegation too large", - )); - } - - let reward_estimation = compute_reward_estimate( - &mixnode.mixnode_details, - performance, - status, - reward_params, - current_interval, - ); - - Ok(RewardEstimationResponse { - estimation: reward_estimation, - reward_params, - epoch: current_interval, - as_at: as_at.unix_timestamp(), - }) - } else { - Err(AxumErrorResponse::not_found("mixnode bond not found")) + if let Some(pledge_amount) = user_reward_param.pledge_amount { + mixnode.mixnode_details.rewarding_details.operator = + Decimal::from_ratio(pledge_amount, 1u64); } + if let Some(total_delegation) = user_reward_param.total_delegation { + mixnode.mixnode_details.rewarding_details.delegates = + Decimal::from_ratio(total_delegation, 1u64); + } + + if let Some(profit_margin_percent) = user_reward_param.profit_margin_percent { + mixnode + .mixnode_details + .rewarding_details + .cost_params + .profit_margin_percent = profit_margin_percent; + } + + if let Some(interval_operating_cost) = &user_reward_param.interval_operating_cost { + mixnode + .mixnode_details + .rewarding_details + .cost_params + .interval_operating_cost = interval_operating_cost.clone(); + } + + if mixnode.mixnode_details.rewarding_details.operator + + mixnode.mixnode_details.rewarding_details.delegates + > reward_params.interval.staking_supply + { + return Err(AxumErrorResponse::unprocessable_entity( + "Pledge plus delegation too large", + )); + } + + let reward_estimation = compute_reward_estimate( + &mixnode.mixnode_details, + performance, + status, + reward_params, + current_interval, + ); + + Ok(RewardEstimationResponse { + estimation: reward_estimation, + reward_params, + epoch: current_interval, + as_at: as_at.unix_timestamp(), + }) } pub(crate) async fn _get_mixnode_stake_saturation( - cache: &NodeStatusCache, - validator_cache: &NymContractCache, - mix_id: MixId, + status_cache: &NodeStatusCache, + contract_cache: &NymContractCache, + mix_id: NodeId, ) -> AxumResult { - let (mixnode, _) = cache.mixnode_details(mix_id).await; - if let Some(mixnode) = mixnode { - // Recompute the stake saturation just so that we can confidently state that the `as_at` - // field is consistent and correct. Luckily this is very cheap. - let reward_params = validator_cache.interval_reward_params().await; - let as_at = reward_params.timestamp(); - let rewarding_params = reward_params - .into_inner() - .ok_or_else(AxumErrorResponse::internal)?; + let mixnode = status_cache + .mixnode_annotated(mix_id) + .await + .ok_or_else(|| AxumErrorResponse::not_found("mixnode bond not found"))?; - Ok(StakeSaturationResponse { - saturation: mixnode - .mixnode_details - .rewarding_details - .bond_saturation(&rewarding_params), - uncapped_saturation: mixnode - .mixnode_details - .rewarding_details - .uncapped_bond_saturation(&rewarding_params), - as_at: as_at.unix_timestamp(), - }) - } else { - Err(AxumErrorResponse::not_found("mixnode bond not found")) - } + // Recompute the stake saturation just so that we can confidently state that the `as_at` + // field is consistent and correct. Luckily this is very cheap. + let reward_params = contract_cache.interval_reward_params().await; + let as_at = reward_params.timestamp(); + let rewarding_params = reward_params + .into_inner() + .ok_or_else(AxumErrorResponse::internal)?; + + Ok(StakeSaturationResponse { + saturation: mixnode + .mixnode_details + .rewarding_details + .bond_saturation(&rewarding_params), + uncapped_saturation: mixnode + .mixnode_details + .rewarding_details + .uncapped_bond_saturation(&rewarding_params), + as_at: as_at.unix_timestamp(), + }) } pub(crate) async fn _get_mixnode_inclusion_probability( cache: &NodeStatusCache, - mix_id: MixId, + mix_id: NodeId, ) -> AxumResult { cache .inclusion_probabilities() @@ -295,7 +359,7 @@ pub(crate) async fn _get_mixnode_inclusion_probability( pub(crate) async fn _get_mixnode_avg_uptime( cache: &NodeStatusCache, - mix_id: MixId, + mix_id: NodeId, ) -> AxumResult { let mixnode = get_mixnode_bond_annotated(cache, mix_id).await?; @@ -310,7 +374,7 @@ pub(crate) async fn _get_gateway_avg_uptime( cache: &NodeStatusCache, identity: &str, ) -> AxumResult { - let gateway = get_gateway_bond_annotated(cache, identity).await?; + let gateway = get_gateway_bond_annotated_by_identity(cache, identity).await?; Ok(GatewayUptimeResponse { identity: identity.to_string(), @@ -338,7 +402,9 @@ pub(crate) async fn _get_mixnode_inclusion_probabilities( } } -pub(crate) async fn _get_mixnodes_detailed(cache: &NodeStatusCache) -> Vec { +pub(crate) async fn _get_legacy_mixnodes_detailed( + cache: &NodeStatusCache, +) -> Vec { cache .mixnodes_annotated_filtered() .await @@ -351,32 +417,50 @@ pub(crate) async fn _get_mixnodes_detailed_unfiltered( cache.mixnodes_annotated_full().await.unwrap_or_default() } -pub(crate) async fn _get_rewarded_set_detailed( - cache: &NodeStatusCache, +pub(crate) async fn _get_rewarded_set_legacy_mixnodes_detailed( + status_cache: &NodeStatusCache, + contract_cache: &NymContractCache, ) -> Vec { - cache - .rewarded_set_annotated() - .await - .unwrap_or_default() - .into_inner() + let Some(rewarded_set) = contract_cache.rewarded_set().await else { + return Vec::new(); + }; + let Some(mixnodes) = status_cache.mixnodes_annotated_full().await else { + return Vec::new(); + }; + mixnodes + .into_iter() + .filter(|m| { + rewarded_set.is_active_mixnode(&m.mix_id()) || rewarded_set.is_standby(&m.mix_id()) + }) + .collect() } -pub(crate) async fn _get_active_set_detailed(cache: &NodeStatusCache) -> Vec { - cache - .active_set_annotated() - .await - .unwrap_or_default() - .into_inner() +pub(crate) async fn _get_active_set_legacy_mixnodes_detailed( + status_cache: &NodeStatusCache, + contract_cache: &NymContractCache, +) -> Vec { + let Some(rewarded_set) = contract_cache.rewarded_set().await else { + return Vec::new(); + }; + let Some(mixnodes) = status_cache.mixnodes_annotated_full().await else { + return Vec::new(); + }; + mixnodes + .into_iter() + .filter(|m| rewarded_set.is_active_mixnode(&m.mix_id())) + .collect() } -pub(crate) async fn _get_gateways_detailed(cache: &NodeStatusCache) -> Vec { +pub(crate) async fn _get_legacy_gateways_detailed( + cache: &NodeStatusCache, +) -> Vec { cache .gateways_annotated_filtered() .await .unwrap_or_default() } -pub(crate) async fn _get_gateways_detailed_unfiltered( +pub(crate) async fn _get_legacy_gateways_detailed_unfiltered( cache: &NodeStatusCache, ) -> Vec { cache.gateways_annotated_full().await.unwrap_or_default() diff --git a/nym-api/src/node_status_api/helpers_deprecated.rs b/nym-api/src/node_status_api/helpers_deprecated.rs deleted file mode 100644 index 0788cd2cb3..0000000000 --- a/nym-api/src/node_status_api/helpers_deprecated.rs +++ /dev/null @@ -1,405 +0,0 @@ -// Copyright 2021-2023 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::node_status_api::models::RocketErrorResponse; -use crate::storage::NymApiStorage; -use crate::support::caching::Cache; -use crate::{NodeStatusCache, NymContractCache}; -use cosmwasm_std::Decimal; -use nym_api_requests::models::{ - AllInclusionProbabilitiesResponse, ComputeRewardEstParam, GatewayBondAnnotated, - GatewayCoreStatusResponse, GatewayStatusReportResponse, GatewayUptimeHistoryResponse, - GatewayUptimeResponse, InclusionProbabilityResponse, MixNodeBondAnnotated, - MixnodeCoreStatusResponse, MixnodeStatusReportResponse, MixnodeStatusResponse, - MixnodeUptimeHistoryResponse, RewardEstimationResponse, StakeSaturationResponse, - UptimeResponse, -}; -use nym_mixnet_contract_common::{MixId, RewardedSetNodeStatus}; -use rocket::http::Status; -use rocket::State; - -use super::reward_estimate::compute_reward_estimate; - -async fn get_gateway_bond_annotated( - cache: &NodeStatusCache, - identity: &str, -) -> Result { - cache - .gateway_annotated(identity) - .await - .ok_or(RocketErrorResponse::new( - "gateway bond not found", - Status::NotFound, - )) -} - -async fn get_mixnode_bond_annotated( - cache: &NodeStatusCache, - mix_id: MixId, -) -> Result { - cache - .mixnode_annotated(mix_id) - .await - .ok_or(RocketErrorResponse::new( - "mixnode bond not found", - Status::NotFound, - )) -} - -pub(crate) async fn _gateway_report( - cache: &NodeStatusCache, - identity: &str, -) -> Result { - let gateway = get_gateway_bond_annotated(cache, identity).await?; - - Ok(GatewayStatusReportResponse { - identity: gateway.identity().to_owned(), - owner: gateway.owner().to_string(), - most_recent: gateway.node_performance.most_recent.round_to_integer(), - last_hour: gateway.node_performance.last_hour.round_to_integer(), - last_day: gateway.node_performance.last_24h.round_to_integer(), - }) -} - -pub(crate) async fn _gateway_uptime_history( - storage: &NymApiStorage, - identity: &str, -) -> Result { - storage - .get_gateway_uptime_history(identity) - .await - .map(GatewayUptimeHistoryResponse::from) - .map_err(|err| RocketErrorResponse::new(err.to_string(), Status::NotFound)) -} - -pub(crate) async fn _gateway_core_status_count( - storage: &State, - identity: &str, - since: Option, -) -> Result { - let count = storage - .get_core_gateway_status_count(identity, since) - .await - .map_err(|err| RocketErrorResponse::new(err.to_string(), Status::NotFound))?; - - Ok(GatewayCoreStatusResponse { - identity: identity.to_string(), - count, - }) -} - -pub(crate) async fn _mixnode_report( - cache: &NodeStatusCache, - mix_id: MixId, -) -> Result { - let mixnode = get_mixnode_bond_annotated(cache, mix_id).await?; - - Ok(MixnodeStatusReportResponse { - mix_id, - identity: mixnode.identity_key().to_owned(), - owner: mixnode.owner().to_string(), - most_recent: mixnode.node_performance.most_recent.round_to_integer(), - last_hour: mixnode.node_performance.last_hour.round_to_integer(), - last_day: mixnode.node_performance.last_24h.round_to_integer(), - }) -} - -pub(crate) async fn _mixnode_uptime_history( - storage: &NymApiStorage, - mix_id: MixId, -) -> Result { - storage - .get_mixnode_uptime_history(mix_id) - .await - .map(MixnodeUptimeHistoryResponse::from) - .map_err(|err| RocketErrorResponse::new(err.to_string(), Status::NotFound)) -} - -pub(crate) async fn _mixnode_core_status_count( - storage: &State, - mix_id: MixId, - since: Option, -) -> Result { - let count = storage - .get_core_mixnode_status_count(mix_id, since) - .await - .map_err(|err| RocketErrorResponse::new(err.to_string(), Status::NotFound))?; - - Ok(MixnodeCoreStatusResponse { mix_id, count }) -} - -pub(crate) async fn _get_mixnode_status( - cache: &NymContractCache, - mix_id: MixId, -) -> MixnodeStatusResponse { - MixnodeStatusResponse { - status: cache.mixnode_status(mix_id).await, - } -} - -pub(crate) async fn _get_mixnode_reward_estimation( - cache: &State, - validator_cache: &State, - mix_id: MixId, -) -> Result { - let (mixnode, status) = cache.mixnode_details(mix_id).await; - if let Some(mixnode) = mixnode { - let reward_params = validator_cache.interval_reward_params().await; - let as_at = reward_params.timestamp(); - let reward_params = reward_params - .into_inner() - .ok_or_else(|| RocketErrorResponse::new("server error", Status::InternalServerError))?; - let current_interval = validator_cache - .current_interval() - .await - .into_inner() - .ok_or_else(|| RocketErrorResponse::new("server error", Status::InternalServerError))?; - - let reward_estimation = compute_reward_estimate( - &mixnode.mixnode_details, - mixnode.performance, - status.into(), - reward_params, - current_interval, - ); - - Ok(RewardEstimationResponse { - estimation: reward_estimation, - reward_params, - epoch: current_interval, - as_at: as_at.unix_timestamp(), - }) - } else { - Err(RocketErrorResponse::new( - "mixnode bond not found", - Status::NotFound, - )) - } -} - -pub(crate) async fn _compute_mixnode_reward_estimation( - user_reward_param: ComputeRewardEstParam, - cache: &NodeStatusCache, - validator_cache: &NymContractCache, - mix_id: MixId, -) -> Result { - let (mixnode, actual_status) = cache.mixnode_details(mix_id).await; - if let Some(mut mixnode) = mixnode { - let reward_params = validator_cache.interval_reward_params().await; - let as_at = reward_params.timestamp(); - let reward_params = reward_params - .into_inner() - .ok_or_else(|| RocketErrorResponse::new("server error", Status::InternalServerError))?; - let current_interval = validator_cache - .current_interval() - .await - .into_inner() - .ok_or_else(|| RocketErrorResponse::new("server error", Status::InternalServerError))?; - - // For these parameters we either use the provided ones, or fall back to the system ones - let performance = user_reward_param.performance.unwrap_or(mixnode.performance); - - let status = match user_reward_param.active_in_rewarded_set { - Some(true) => Some(RewardedSetNodeStatus::Active), - Some(false) => Some(RewardedSetNodeStatus::Standby), - None => actual_status.into(), - }; - - if let Some(pledge_amount) = user_reward_param.pledge_amount { - mixnode.mixnode_details.rewarding_details.operator = - Decimal::from_ratio(pledge_amount, 1u64); - } - if let Some(total_delegation) = user_reward_param.total_delegation { - mixnode.mixnode_details.rewarding_details.delegates = - Decimal::from_ratio(total_delegation, 1u64); - } - - if let Some(profit_margin_percent) = user_reward_param.profit_margin_percent { - mixnode - .mixnode_details - .rewarding_details - .cost_params - .profit_margin_percent = profit_margin_percent; - } - - if let Some(interval_operating_cost) = user_reward_param.interval_operating_cost { - mixnode - .mixnode_details - .rewarding_details - .cost_params - .interval_operating_cost = interval_operating_cost; - } - - if mixnode.mixnode_details.rewarding_details.operator - + mixnode.mixnode_details.rewarding_details.delegates - > reward_params.interval.staking_supply - { - return Err(RocketErrorResponse::new( - "Pledge plus delegation too large", - Status::UnprocessableEntity, - )); - } - - let reward_estimation = compute_reward_estimate( - &mixnode.mixnode_details, - performance, - status, - reward_params, - current_interval, - ); - - Ok(RewardEstimationResponse { - estimation: reward_estimation, - reward_params, - epoch: current_interval, - as_at: as_at.unix_timestamp(), - }) - } else { - Err(RocketErrorResponse::new( - "mixnode bond not found", - Status::NotFound, - )) - } -} - -pub(crate) async fn _get_mixnode_stake_saturation( - cache: &NodeStatusCache, - validator_cache: &NymContractCache, - mix_id: MixId, -) -> Result { - let (mixnode, _) = cache.mixnode_details(mix_id).await; - if let Some(mixnode) = mixnode { - // Recompute the stake saturation just so that we can confidently state that the `as_at` - // field is consistent and correct. Luckily this is very cheap. - let reward_params = validator_cache.interval_reward_params().await; - let as_at = reward_params.timestamp(); - let rewarding_params = reward_params - .into_inner() - .ok_or_else(|| RocketErrorResponse::new("server error", Status::InternalServerError))?; - - Ok(StakeSaturationResponse { - saturation: mixnode - .mixnode_details - .rewarding_details - .bond_saturation(&rewarding_params), - uncapped_saturation: mixnode - .mixnode_details - .rewarding_details - .uncapped_bond_saturation(&rewarding_params), - as_at: as_at.unix_timestamp(), - }) - } else { - Err(RocketErrorResponse::new( - "mixnode bond not found", - Status::NotFound, - )) - } -} - -pub(crate) async fn _get_mixnode_inclusion_probability( - cache: &NodeStatusCache, - mix_id: MixId, -) -> Result { - cache - .inclusion_probabilities() - .await - .map(Cache::into_inner) - .and_then(|p| p.node(mix_id).cloned()) - .map(|p| InclusionProbabilityResponse { - in_active: p.in_active.into(), - in_reserve: p.in_reserve.into(), - }) - .ok_or_else(|| RocketErrorResponse::new("mixnode bond not found", Status::NotFound)) -} - -pub(crate) async fn _get_mixnode_avg_uptime( - cache: &NodeStatusCache, - mix_id: MixId, -) -> Result { - let mixnode = get_mixnode_bond_annotated(cache, mix_id).await?; - - Ok(UptimeResponse { - mix_id, - avg_uptime: mixnode.node_performance.last_24h.round_to_integer(), - node_performance: mixnode.node_performance, - }) -} - -pub(crate) async fn _get_gateway_avg_uptime( - cache: &NodeStatusCache, - identity: &str, -) -> Result { - let gateway = get_gateway_bond_annotated(cache, identity).await?; - - Ok(GatewayUptimeResponse { - identity: identity.to_string(), - avg_uptime: gateway.node_performance.last_24h.round_to_integer(), - node_performance: gateway.node_performance, - }) -} - -pub(crate) async fn _get_mixnode_inclusion_probabilities( - cache: &NodeStatusCache, -) -> Result { - if let Some(prob) = cache.inclusion_probabilities().await { - let as_at = prob.timestamp(); - let prob = prob.into_inner(); - Ok(AllInclusionProbabilitiesResponse { - inclusion_probabilities: prob.inclusion_probabilities, - samples: prob.samples, - elapsed: prob.elapsed, - delta_max: prob.delta_max, - delta_l2: prob.delta_l2, - as_at: as_at.unix_timestamp(), - }) - } else { - Err(RocketErrorResponse::new( - "No data available", - Status::ServiceUnavailable, - )) - } -} - -pub(crate) async fn _get_mixnodes_detailed(cache: &NodeStatusCache) -> Vec { - cache - .mixnodes_annotated_filtered() - .await - .unwrap_or_default() -} - -pub(crate) async fn _get_mixnodes_detailed_unfiltered( - cache: &NodeStatusCache, -) -> Vec { - cache.mixnodes_annotated_full().await.unwrap_or_default() -} - -pub(crate) async fn _get_rewarded_set_detailed( - cache: &NodeStatusCache, -) -> Vec { - cache - .rewarded_set_annotated() - .await - .unwrap_or_default() - .into_inner() -} - -pub(crate) async fn _get_active_set_detailed(cache: &NodeStatusCache) -> Vec { - cache - .active_set_annotated() - .await - .unwrap_or_default() - .into_inner() -} - -pub(crate) async fn _get_gateways_detailed(cache: &NodeStatusCache) -> Vec { - cache - .gateways_annotated_filtered() - .await - .unwrap_or_default() -} - -pub(crate) async fn _get_gateways_detailed_unfiltered( - cache: &NodeStatusCache, -) -> Vec { - cache.gateways_annotated_full().await.unwrap_or_default() -} diff --git a/nym-api/src/node_status_api/mod.rs b/nym-api/src/node_status_api/mod.rs index 5f50e62964..3f7ed14b16 100644 --- a/nym-api/src/node_status_api/mod.rs +++ b/nym-api/src/node_status_api/mod.rs @@ -9,20 +9,14 @@ use crate::{ }; pub(crate) use cache::NodeStatusCache; use nym_task::TaskManager; -use okapi::openapi3::OpenApi; -use rocket::Route; -use rocket_okapi::{openapi_get_routes_spec, settings::OpenApiSettings}; use std::time::Duration; pub(crate) mod cache; -#[cfg(feature = "axum")] pub(crate) mod handlers; -#[cfg(feature = "axum")] pub(crate) mod helpers; -pub(crate) mod helpers_deprecated; pub(crate) mod models; pub(crate) mod reward_estimate; -pub(crate) mod routes_deprecated; +// pub(crate) mod routes_deprecated; pub(crate) mod uptime_updater; pub(crate) mod utils; @@ -30,51 +24,51 @@ pub(crate) const FIFTEEN_MINUTES: Duration = Duration::from_secs(900); pub(crate) const ONE_HOUR: Duration = Duration::from_secs(3600); pub(crate) const ONE_DAY: Duration = Duration::from_secs(86400); -pub(crate) fn node_status_routes( - settings: &OpenApiSettings, - enabled: bool, -) -> (Vec, OpenApi) { - if enabled { - openapi_get_routes_spec![ - settings: routes_deprecated::gateway_report, - routes_deprecated::gateway_uptime_history, - routes_deprecated::gateway_core_status_count, - routes_deprecated::mixnode_report, - routes_deprecated::mixnode_uptime_history, - routes_deprecated::mixnode_core_status_count, - routes_deprecated::get_mixnode_status, - routes_deprecated::get_mixnode_reward_estimation, - routes_deprecated::compute_mixnode_reward_estimation, - routes_deprecated::get_mixnode_stake_saturation, - routes_deprecated::get_mixnode_inclusion_probability, - routes_deprecated::get_mixnode_avg_uptime, - routes_deprecated::get_gateway_avg_uptime, - routes_deprecated::get_mixnode_inclusion_probabilities, - routes_deprecated::get_mixnodes_detailed, - routes_deprecated::get_mixnodes_detailed_unfiltered, - routes_deprecated::get_rewarded_set_detailed, - routes_deprecated::get_active_set_detailed, - routes_deprecated::get_gateways_detailed, - routes_deprecated::get_gateways_detailed_unfiltered, - routes_deprecated::unstable::mixnode_test_results, - routes_deprecated::unstable::gateway_test_results, - routes_deprecated::submit_gateway_monitoring_results, - routes_deprecated::submit_node_monitoring_results, - ] - } else { - // in the minimal variant we would not have access to endpoints relying on existence - // of the network monitor and the associated storage - openapi_get_routes_spec![ - settings: routes_deprecated::get_mixnode_status, - routes_deprecated::get_mixnode_stake_saturation, - routes_deprecated::get_mixnode_inclusion_probability, - routes_deprecated::get_mixnode_inclusion_probabilities, - routes_deprecated::get_mixnodes_detailed, - routes_deprecated::get_rewarded_set_detailed, - routes_deprecated::get_active_set_detailed, - ] - } -} +// pub(crate) fn node_status_routes( +// settings: &OpenApiSettings, +// enabled: bool, +// ) -> (Vec, OpenApi) { +// if enabled { +// openapi_get_routes_spec![ +// settings: routes_deprecated::gateway_report, +// routes_deprecated::gateway_uptime_history, +// routes_deprecated::gateway_core_status_count, +// routes_deprecated::mixnode_report, +// routes_deprecated::mixnode_uptime_history, +// routes_deprecated::mixnode_core_status_count, +// routes_deprecated::get_mixnode_status, +// routes_deprecated::get_mixnode_reward_estimation, +// routes_deprecated::compute_mixnode_reward_estimation, +// routes_deprecated::get_mixnode_stake_saturation, +// routes_deprecated::get_mixnode_inclusion_probability, +// routes_deprecated::get_mixnode_avg_uptime, +// routes_deprecated::get_gateway_avg_uptime, +// routes_deprecated::get_mixnode_inclusion_probabilities, +// routes_deprecated::get_mixnodes_detailed, +// routes_deprecated::get_mixnodes_detailed_unfiltered, +// routes_deprecated::get_rewarded_set_detailed, +// routes_deprecated::get_active_set_detailed, +// routes_deprecated::get_gateways_detailed, +// routes_deprecated::get_gateways_detailed_unfiltered, +// routes_deprecated::unstable::mixnode_test_results, +// routes_deprecated::unstable::gateway_test_results, +// routes_deprecated::submit_gateway_monitoring_results, +// routes_deprecated::submit_node_monitoring_results, +// ] +// } else { +// // in the minimal variant we would not have access to endpoints relying on existence +// // of the network monitor and the associated storage +// openapi_get_routes_spec![ +// settings: routes_deprecated::get_mixnode_status, +// routes_deprecated::get_mixnode_stake_saturation, +// routes_deprecated::get_mixnode_inclusion_probability, +// routes_deprecated::get_mixnode_inclusion_probabilities, +// routes_deprecated::get_mixnodes_detailed, +// routes_deprecated::get_rewarded_set_detailed, +// routes_deprecated::get_active_set_detailed, +// ] +// } +// } /// Spawn the node status cache refresher. /// diff --git a/nym-api/src/node_status_api/models.rs b/nym-api/src/node_status_api/models.rs index 480fae65d2..ce4c0eb1b1 100644 --- a/nym-api/src/node_status_api/models.rs +++ b/nym-api/src/node_status_api/models.rs @@ -1,29 +1,25 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use crate::ecash::error::{EcashError, RedemptionError}; use crate::node_status_api::utils::NodeUptimes; use crate::storage::models::NodeStatus; +use crate::support::caching::cache::UninitialisedCache; use nym_api_requests::models::{ - GatewayStatusReportResponse, GatewayUptimeHistoryResponse, HistoricalUptimeResponse, - MixnodeStatusReportResponse, MixnodeUptimeHistoryResponse, NodePerformance, RequestError, + HistoricalPerformanceResponse, HistoricalUptimeResponse, NodePerformance, + OldHistoricalUptimeResponse, RequestError, }; +use nym_contracts_common::NaiveFloat; use nym_mixnet_contract_common::reward_params::Performance; -use nym_mixnet_contract_common::{IdentityKey, MixId}; -use okapi::openapi3::{Responses, SchemaObject}; -use rocket::http::Status; -use rocket::response::{self, Responder, Response}; -use rocket::serde::json::Json; -use rocket::Request; -use rocket_okapi::gen::OpenApiGenerator; -use rocket_okapi::response::OpenApiResponderInner; -use rocket_okapi::util::ensure_status_code_exists; -use schemars::gen::SchemaGenerator; -use schemars::schema::{InstanceType, Schema}; +use nym_mixnet_contract_common::{IdentityKey, NodeId}; +use nym_serde_helpers::date::DATE_FORMAT; +use reqwest::StatusCode; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; - +use std::fmt::Display; use thiserror::Error; -use time::OffsetDateTime; +use time::{Date, OffsetDateTime}; +use tracing::error; #[derive(Error, Debug)] #[error("Received uptime value was within 0-100 range (got {received})")] @@ -40,6 +36,10 @@ impl Uptime { Uptime(0) } + pub fn is_zero(&self) -> bool { + self.0 == 0 + } + pub fn new(uptime: f32) -> Self { if uptime > 100f32 { error!("Got uptime {}, max is 100, returning 0", uptime); @@ -128,9 +128,8 @@ impl From for Performance { #[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)] pub struct MixnodeStatusReport { - pub(crate) mix_id: MixId, + pub(crate) mix_id: NodeId, pub(crate) identity: IdentityKey, - pub(crate) owner: String, pub(crate) most_recent: Uptime, @@ -141,9 +140,8 @@ pub struct MixnodeStatusReport { impl MixnodeStatusReport { pub(crate) fn construct_from_last_day_reports( report_time: OffsetDateTime, - mix_id: MixId, + mix_id: NodeId, identity: IdentityKey, - owner: String, last_day: Vec, last_hour_test_runs: usize, last_day_test_runs: usize, @@ -158,7 +156,6 @@ impl MixnodeStatusReport { MixnodeStatusReport { mix_id, identity, - owner, most_recent: node_uptimes.most_recent, last_hour: node_uptimes.last_hour, last_day: node_uptimes.last_day, @@ -166,19 +163,6 @@ impl MixnodeStatusReport { } } -impl From for MixnodeStatusReportResponse { - fn from(status: MixnodeStatusReport) -> Self { - MixnodeStatusReportResponse { - mix_id: status.mix_id, - identity: status.identity, - owner: status.owner, - most_recent: status.most_recent.0, - last_hour: status.last_hour.0, - last_day: status.last_day.0, - } - } -} - impl From for NodePerformance { fn from(report: MixnodeStatusReport) -> Self { NodePerformance { @@ -191,8 +175,8 @@ impl From for NodePerformance { #[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)] pub struct GatewayStatusReport { + pub(crate) node_id: NodeId, pub(crate) identity: String, - pub(crate) owner: String, pub(crate) most_recent: Uptime, @@ -203,8 +187,8 @@ pub struct GatewayStatusReport { impl GatewayStatusReport { pub(crate) fn construct_from_last_day_reports( report_time: OffsetDateTime, + node_id: NodeId, identity: String, - owner: String, last_day: Vec, last_hour_test_runs: usize, last_day_test_runs: usize, @@ -218,7 +202,7 @@ impl GatewayStatusReport { GatewayStatusReport { identity, - owner, + node_id, most_recent: node_uptimes.most_recent, last_hour: node_uptimes.last_hour, last_day: node_uptimes.last_day, @@ -226,18 +210,6 @@ impl GatewayStatusReport { } } -impl From for GatewayStatusReportResponse { - fn from(status: GatewayStatusReport) -> Self { - GatewayStatusReportResponse { - identity: status.identity, - owner: status.owner, - most_recent: status.most_recent.0, - last_hour: status.last_hour.0, - last_day: status.last_day.0, - } - } -} - impl From for NodePerformance { fn from(report: GatewayStatusReport) -> Self { NodePerformance { @@ -250,68 +222,44 @@ impl From for NodePerformance { #[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)] pub struct MixnodeUptimeHistory { - pub(crate) mix_id: MixId, + pub(crate) mix_id: NodeId, pub(crate) identity: String, - pub(crate) owner: String, pub(crate) history: Vec, } impl MixnodeUptimeHistory { - pub(crate) fn new( - mix_id: MixId, - identity: String, - owner: String, - history: Vec, - ) -> Self { + pub(crate) fn new(mix_id: NodeId, identity: String, history: Vec) -> Self { MixnodeUptimeHistory { mix_id, identity, - owner, history, } } } -impl From for MixnodeUptimeHistoryResponse { - fn from(history: MixnodeUptimeHistory) -> Self { - MixnodeUptimeHistoryResponse { - mix_id: history.mix_id, - identity: history.identity, - owner: history.owner, - history: history.history.into_iter().map(Into::into).collect(), - } - } -} - -#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)] +#[derive(Default, Clone, Serialize, Deserialize, Debug, JsonSchema)] pub struct GatewayUptimeHistory { pub(crate) identity: String, - pub(crate) owner: String, + pub(crate) node_id: NodeId, pub(crate) history: Vec, } impl GatewayUptimeHistory { - pub(crate) fn new(identity: String, owner: String, history: Vec) -> Self { + pub(crate) fn new( + node_id: NodeId, + identity: impl Into, + history: Vec, + ) -> Self { GatewayUptimeHistory { - identity, - owner, + node_id, + identity: identity.into(), history, } } } -impl From for GatewayUptimeHistoryResponse { - fn from(history: GatewayUptimeHistory) -> Self { - GatewayUptimeHistoryResponse { - identity: history.identity, - owner: history.owner, - history: history.history.into_iter().map(Into::into).collect(), - } - } -} - #[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)] pub struct HistoricalUptime { // ISO 8601 date string @@ -321,188 +269,172 @@ pub struct HistoricalUptime { pub(crate) uptime: Uptime, } -impl From for HistoricalUptimeResponse { +#[derive(Error, Debug)] +pub enum InvalidHistoricalPerformance { + #[error("the provided date could not be parsed")] + UnparsableDate, + + #[error("the provided uptime could not be parsed")] + MalformedPerformance, +} + +impl TryFrom for HistoricalPerformanceResponse { + type Error = InvalidHistoricalPerformance; + fn try_from(value: HistoricalUptime) -> Result { + Ok(HistoricalPerformanceResponse { + date: Date::parse(&value.date, DATE_FORMAT) + .map_err(|_| InvalidHistoricalPerformance::UnparsableDate)?, + performance: Performance::from_percentage_value(value.uptime.u8() as u64) + .map_err(|_| InvalidHistoricalPerformance::MalformedPerformance)? + .naive_to_f64(), + }) + } +} + +impl TryFrom for HistoricalUptimeResponse { + type Error = InvalidHistoricalPerformance; + fn try_from(value: HistoricalUptime) -> Result { + Ok(HistoricalUptimeResponse { + date: Date::parse(&value.date, DATE_FORMAT) + .map_err(|_| InvalidHistoricalPerformance::UnparsableDate)?, + uptime: value.uptime.u8(), + }) + } +} + +impl From for OldHistoricalUptimeResponse { fn from(uptime: HistoricalUptime) -> Self { - HistoricalUptimeResponse { + OldHistoricalUptimeResponse { date: uptime.date, uptime: uptime.uptime.0, } } } -#[deprecated(note = "TODO rocket remove once Rocket is phased out")] -pub(crate) struct RocketErrorResponse { - error_message: RequestError, - status: Status, +// TODO rocket remove smurf name after eliminating `rocket` +pub(crate) type AxumResult = Result; +pub(crate) struct AxumErrorResponse { + message: RequestError, + status: StatusCode, } -impl RocketErrorResponse { - pub(crate) fn new(error_message: impl Into, status: Status) -> Self { - RocketErrorResponse { - error_message: RequestError::new(error_message), - status, +impl AxumErrorResponse { + pub(crate) fn internal_msg(msg: impl Display) -> Self { + Self { + message: RequestError::new(msg.to_string()), + status: StatusCode::INTERNAL_SERVER_ERROR, + } + } + + pub(crate) fn internal() -> Self { + Self { + message: RequestError::new("Internal server error"), + status: StatusCode::INTERNAL_SERVER_ERROR, + } + } + + pub(crate) fn not_implemented() -> Self { + Self { + message: RequestError::empty(), + status: StatusCode::NOT_IMPLEMENTED, + } + } + + pub(crate) fn not_found(msg: impl Display) -> Self { + Self { + message: RequestError::new(msg.to_string()), + status: StatusCode::NOT_FOUND, + } + } + + pub(crate) fn service_unavailable() -> Self { + Self { + message: RequestError::empty(), + status: StatusCode::SERVICE_UNAVAILABLE, + } + } + + pub(crate) fn unprocessable_entity(msg: impl Display) -> Self { + Self { + message: RequestError::new(msg.to_string()), + status: StatusCode::UNPROCESSABLE_ENTITY, + } + } + + pub(crate) fn forbidden(msg: impl Display) -> Self { + Self { + message: RequestError::new(msg.to_string()), + status: StatusCode::FORBIDDEN, + } + } + + pub(crate) fn bad_request(msg: impl Display) -> Self { + Self { + message: RequestError::new(msg.to_string()), + status: StatusCode::BAD_REQUEST, } } } -impl<'r, 'o: 'r> Responder<'r, 'o> for RocketErrorResponse { - fn respond_to(self, req: &'r Request<'_>) -> response::Result<'o> { - // piggyback on the existing implementation - // also prefer json over plain for ease of use in frontend - Response::build() - .merge(Json(self.error_message).respond_to(req)?) - .status(self.status) - .ok() - } -} - -impl JsonSchema for RocketErrorResponse { - fn schema_name() -> String { - "ErrorResponse".to_owned() - } - - fn json_schema(gen: &mut SchemaGenerator) -> Schema { - let mut schema_object = SchemaObject { - instance_type: Some(InstanceType::Object.into()), - ..SchemaObject::default() - }; - - let object_validation = schema_object.object(); - object_validation - .properties - .insert("error_message".to_owned(), gen.subschema_for::()); - object_validation - .required - .insert("error_message".to_owned()); - - // Status does not implement JsonSchema so we just explicitly specify the inner type. - object_validation - .properties - .insert("status".to_owned(), gen.subschema_for::()); - object_validation.required.insert("status".to_owned()); - - Schema::Object(schema_object) - } -} - -impl OpenApiResponderInner for RocketErrorResponse { - fn responses(_gen: &mut OpenApiGenerator) -> rocket_okapi::Result { - let mut responses = Responses::default(); - ensure_status_code_exists(&mut responses, 404); - Ok(responses) - } -} - -#[cfg(feature = "axum")] -pub(crate) use axum_error::{AxumErrorResponse, AxumResult}; - -#[cfg(feature = "axum")] -/// TODO rocket: extract types from this module when axum becomes the only server in Nym API -mod axum_error { - pub use super::*; - use crate::ecash::error::{EcashError, RedemptionError}; - use std::fmt::Display; - - // TODO rocket remove smurf name after eliminating `rocket` - pub(crate) type AxumResult = Result; - pub(crate) struct AxumErrorResponse { - message: RequestError, - status: axum::http::StatusCode, - } - - impl AxumErrorResponse { - pub(crate) fn internal_msg(msg: impl Display) -> Self { - Self { - message: RequestError::new(msg.to_string()), - status: axum::http::StatusCode::INTERNAL_SERVER_ERROR, - } - } - - pub(crate) fn internal() -> Self { - Self { - message: RequestError::new("Internal server error"), - status: axum::http::StatusCode::INTERNAL_SERVER_ERROR, - } - } - - pub(crate) fn not_implemented() -> Self { - Self { - message: RequestError::empty(), - status: axum::http::StatusCode::NOT_IMPLEMENTED, - } - } - - pub(crate) fn not_found(msg: impl Display) -> Self { - Self { - message: RequestError::new(msg.to_string()), - status: axum::http::StatusCode::NOT_FOUND, - } - } - - pub(crate) fn service_unavailable() -> Self { - Self { - message: RequestError::empty(), - status: axum::http::StatusCode::SERVICE_UNAVAILABLE, - } - } - - pub(crate) fn unprocessable_entity(msg: impl Display) -> Self { - Self { - message: RequestError::new(msg.to_string()), - status: axum::http::StatusCode::UNPROCESSABLE_ENTITY, - } - } - } - - impl axum::response::IntoResponse for AxumErrorResponse { - fn into_response(self) -> axum::response::Response { - (self.status, self.message.message().to_string()).into_response() - } - } - - impl From for AxumErrorResponse { - fn from(value: NymApiStorageError) -> Self { - error!("{value}"); - Self { - message: RequestError::empty(), - status: axum::http::StatusCode::INTERNAL_SERVER_ERROR, - } - } - } - - impl From for AxumErrorResponse { - fn from(value: EcashError) -> Self { - Self { - message: RequestError::new(value.to_string()), - status: axum::http::StatusCode::BAD_REQUEST, - } - } - } - - #[cfg(feature = "axum")] - impl From for AxumErrorResponse { - fn from(value: RedemptionError) -> Self { - Self { - message: RequestError::new(value.to_string()), - status: axum::http::StatusCode::BAD_REQUEST, - } +impl From for AxumErrorResponse { + fn from(_: UninitialisedCache) -> Self { + AxumErrorResponse { + message: RequestError::new("relevant cache hasn't been initialised yet"), + status: StatusCode::SERVICE_UNAVAILABLE, } } } -#[derive(Debug, thiserror::Error)] +impl axum::response::IntoResponse for AxumErrorResponse { + fn into_response(self) -> axum::response::Response { + (self.status, self.message.message().to_string()).into_response() + } +} + +impl From for AxumErrorResponse { + fn from(value: NymApiStorageError) -> Self { + error!("{value}"); + Self { + message: RequestError::empty(), + status: StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +impl From for AxumErrorResponse { + fn from(value: EcashError) -> Self { + Self { + message: RequestError::new(value.to_string()), + status: StatusCode::BAD_REQUEST, + } + } +} + +impl From for AxumErrorResponse { + fn from(value: RedemptionError) -> Self { + Self { + message: RequestError::new(value.to_string()), + status: StatusCode::BAD_REQUEST, + } + } +} + +#[derive(Debug, Error)] pub enum NymApiStorageError { #[error("could not find status report associated with mixnode {mix_id}")] - MixnodeReportNotFound { mix_id: MixId }, + MixnodeReportNotFound { mix_id: NodeId }, - #[error("Could not find status report associated with gateway {identity}")] - GatewayReportNotFound { identity: IdentityKey }, + #[error("Could not find status report associated with gateway {node_id}")] + GatewayReportNotFound { node_id: NodeId }, #[error("could not find uptime history associated with mixnode {mix_id}")] - MixnodeUptimeHistoryNotFound { mix_id: MixId }, + MixnodeUptimeHistoryNotFound { mix_id: NodeId }, - #[error("could not find uptime history associated with gateway {identity}")] - GatewayUptimeHistoryNotFound { identity: IdentityKey }, + #[error("could not find uptime history associated with gateway {node_id}")] + GatewayUptimeHistoryNotFound { node_id: NodeId }, + + #[error("could not find gateway {identity} in the storage")] + GatewayNotFound { identity: String }, // I don't think we want to expose errors to the user about what really happened #[error("experienced internal database error")] @@ -524,3 +456,11 @@ impl NymApiStorageError { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn uptime_response_conversion() {} +} diff --git a/nym-api/src/node_status_api/reward_estimate.rs b/nym-api/src/node_status_api/reward_estimate.rs index e9bd60ba09..6b1798227d 100644 --- a/nym-api/src/node_status_api/reward_estimate.rs +++ b/nym-api/src/node_status_api/reward_estimate.rs @@ -1,11 +1,14 @@ // Copyright 2021-2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use crate::node_status_api::helpers::RewardedSetStatus; use cosmwasm_std::Decimal; -use nym_mixnet_contract_common::mixnode::MixNodeDetails; -use nym_mixnet_contract_common::reward_params::{NodeRewardParams, Performance, RewardingParams}; +use nym_api_requests::legacy::LegacyMixNodeDetailsWithLayer; +use nym_mixnet_contract_common::reward_params::{ + NodeRewardingParameters, Performance, RewardingParams, +}; use nym_mixnet_contract_common::rewarding::RewardEstimate; -use nym_mixnet_contract_common::{Interval, RewardedSetNodeStatus}; +use nym_mixnet_contract_common::Interval; fn compute_apy(epochs_in_year: Decimal, reward: Decimal, pledge_amount: Decimal) -> Decimal { if pledge_amount.is_zero() { @@ -17,9 +20,9 @@ fn compute_apy(epochs_in_year: Decimal, reward: Decimal, pledge_amount: Decimal) } pub fn compute_reward_estimate( - mixnode: &MixNodeDetails, + mixnode: &LegacyMixNodeDetailsWithLayer, performance: Performance, - rewarded_set_status: Option, + rewarded_set_status: RewardedSetStatus, rewarding_params: RewardingParams, interval: Interval, ) -> RewardEstimate { @@ -31,13 +34,22 @@ pub fn compute_reward_estimate( return Default::default(); } - let node_status = match rewarded_set_status { - Some(status) => status, - // if node is not in the rewarded set, it's not going to get anything - None => return Default::default(), + let is_active = match rewarded_set_status { + RewardedSetStatus::Active => true, + RewardedSetStatus::Standby => false, + RewardedSetStatus::Inactive => return Default::default(), }; - let node_reward_params = NodeRewardParams::new(performance, node_status.is_active()); + let work_factor = if is_active { + rewarding_params.active_node_work() + } else { + rewarding_params.standby_node_work() + }; + + let node_reward_params = NodeRewardingParameters { + performance, + work_factor, + }; let node_reward = mixnode .rewarding_details .node_reward(&rewarding_params, node_reward_params); @@ -63,7 +75,7 @@ pub fn compute_reward_estimate( } pub fn compute_apy_from_reward( - mixnode: &MixNodeDetails, + mixnode: &LegacyMixNodeDetailsWithLayer, reward_estimate: RewardEstimate, interval: Interval, ) -> (Decimal, Decimal) { diff --git a/nym-api/src/node_status_api/routes_deprecated.rs b/nym-api/src/node_status_api/routes_deprecated.rs index 29ab89fb4d..fa49ad692a 100644 --- a/nym-api/src/node_status_api/routes_deprecated.rs +++ b/nym-api/src/node_status_api/routes_deprecated.rs @@ -1,6 +1,18 @@ // Copyright 2021-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use super::NodeStatusCache; +use crate::node_status_api::helpers_deprecated::{ + _compute_mixnode_reward_estimation, _gateway_core_status_count, _gateway_report, + _gateway_uptime_history, _get_gateway_avg_uptime, _get_mixnode_avg_uptime, + _get_mixnode_inclusion_probabilities, _get_mixnode_inclusion_probability, + _get_mixnode_reward_estimation, _get_mixnode_stake_saturation, _get_mixnode_status, + _get_mixnodes_detailed_unfiltered, _mixnode_core_status_count, _mixnode_report, + _mixnode_uptime_history, +}; +use crate::node_status_api::models::RocketErrorResponse; +use crate::storage::NymApiStorage; +use crate::NymContractCache; use nym_api_requests::models::{ AllInclusionProbabilitiesResponse, ComputeRewardEstParam, GatewayBondAnnotated, GatewayCoreStatusResponse, GatewayStatusReportResponse, GatewayUptimeHistoryResponse, @@ -9,68 +21,54 @@ use nym_api_requests::models::{ MixnodeUptimeHistoryResponse, RewardEstimationResponse, StakeSaturationResponse, UptimeResponse, }; -use nym_mixnet_contract_common::MixId; +use nym_mixnet_contract_common::NodeId; use nym_types::monitoring::MonitorMessage; use rocket::serde::json::Json; use rocket::State; use rocket_okapi::openapi; -use super::helpers_deprecated::_get_gateways_detailed; -use super::NodeStatusCache; -use crate::node_status_api::helpers_deprecated::{ - _compute_mixnode_reward_estimation, _gateway_core_status_count, _gateway_report, - _gateway_uptime_history, _get_active_set_detailed, _get_gateway_avg_uptime, - _get_gateways_detailed_unfiltered, _get_mixnode_avg_uptime, - _get_mixnode_inclusion_probabilities, _get_mixnode_inclusion_probability, - _get_mixnode_reward_estimation, _get_mixnode_stake_saturation, _get_mixnode_status, - _get_mixnodes_detailed, _get_mixnodes_detailed_unfiltered, _get_rewarded_set_detailed, - _mixnode_core_status_count, _mixnode_report, _mixnode_uptime_history, -}; -use crate::node_status_api::models::RocketErrorResponse; -use crate::storage::NymApiStorage; -use crate::NymContractCache; - -#[openapi(tag = "status")] +#[openapi(tag = "status", deprecated = true)] #[post("/submit-gateway-monitoring-results", data = "")] pub(crate) async fn submit_gateway_monitoring_results( message: Json, storage: &State, ) -> Result<(), RocketErrorResponse> { - if !message.from_allowed() { - return Err(RocketErrorResponse::new( - "Monitor not registered to submit results".to_string(), - rocket::http::Status::Forbidden, - )); - } - - if !message.timely() { - return Err(RocketErrorResponse::new( - "Message is too old".to_string(), - rocket::http::Status::BadRequest, - )); - } - - if !message.verify() { - return Err(RocketErrorResponse::new( - "Invalid signature".to_string(), - rocket::http::Status::BadRequest, - )); - } - - match storage - .manager - .submit_gateway_statuses_v2(message.results()) - .await - { - Ok(_) => Ok(()), - Err(err) => { - error!("failed to submit gateway monitoring results: {}", err); - Err(RocketErrorResponse::new( - "failed to submit gateway monitoring results".to_string(), - rocket::http::Status::InternalServerError, - )) - } - } + todo!("rebasing"); + // if !message.from_allowed() { + // return Err(RocketErrorResponse::new( + // "Monitor not registered to submit results".to_string(), + // rocket::http::Status::Forbidden, + // )); + // } + // + // if !message.timely() { + // return Err(RocketErrorResponse::new( + // "Message is too old".to_string(), + // rocket::http::Status::BadRequest, + // )); + // } + // + // if !message.verify() { + // return Err(RocketErrorResponse::new( + // "Invalid signature".to_string(), + // rocket::http::Status::BadRequest, + // )); + // } + // + // match storage + // .manager + // .submit_gateway_statuses_v2(message.results()) + // .await + // { + // Ok(_) => Ok(()), + // Err(err) => { + // error!("failed to submit gateway monitoring results: {}", err); + // Err(RocketErrorResponse::new( + // "failed to submit gateway monitoring results".to_string(), + // rocket::http::Status::InternalServerError, + // )) + // } + // } } #[openapi(tag = "status")] @@ -79,62 +77,71 @@ pub(crate) async fn submit_node_monitoring_results( message: Json, storage: &State, ) -> Result<(), RocketErrorResponse> { - if !message.from_allowed() { - return Err(RocketErrorResponse::new( - "Monitor not registered to submit results".to_string(), - rocket::http::Status::Forbidden, - )); - } + todo!("rebasing"); - if !message.timely() { - return Err(RocketErrorResponse::new( - "Message is too old".to_string(), - rocket::http::Status::BadRequest, - )); - } - - if !message.verify() { - return Err(RocketErrorResponse::new( - "Invalid signature".to_string(), - rocket::http::Status::BadRequest, - )); - } - - match storage - .manager - .submit_mixnode_statuses_v2(message.results()) - .await - { - Ok(_) => Ok(()), - Err(err) => { - error!("failed to submit node monitoring results: {}", err); - Err(RocketErrorResponse::new( - "failed to submit node monitoring results".to_string(), - rocket::http::Status::InternalServerError, - )) - } - } + // if !message.from_allowed() { + // return Err(RocketErrorResponse::new( + // "Monitor not registered to submit results".to_string(), + // rocket::http::Status::Forbidden, + // )); + // } + // + // if !message.timely() { + // return Err(RocketErrorResponse::new( + // "Message is too old".to_string(), + // rocket::http::Status::BadRequest, + // )); + // } + // + // if !message.verify() { + // return Err(RocketErrorResponse::new( + // "Invalid signature".to_string(), + // rocket::http::Status::BadRequest, + // )); + // } + // + // match storage + // .manager + // .submit_mixnode_statuses_v2(message.results()) + // .await + // { + // Ok(_) => Ok(()), + // Err(err) => { + // error!("failed to submit node monitoring results: {}", err); + // Err(RocketErrorResponse::new( + // "failed to submit node monitoring results".to_string(), + // rocket::http::Status::InternalServerError, + // )) + // } + // } } -#[openapi(tag = "status")] +#[openapi(tag = "status", deprecated = true)] #[get("/gateway//report")] pub(crate) async fn gateway_report( cache: &State, identity: &str, ) -> Result, RocketErrorResponse> { - Ok(Json(_gateway_report(cache, identity).await?)) + todo!("rebasing"); + + // Ok(Json(_gateway_report(cache, identity).await?)) } -#[openapi(tag = "status")] +#[openapi(tag = "status", deprecated = true)] #[get("/gateway//history")] pub(crate) async fn gateway_uptime_history( storage: &State, + nym_contract_cache: &State, identity: &str, ) -> Result, RocketErrorResponse> { - Ok(Json(_gateway_uptime_history(storage, identity).await?)) + todo!("rebasing"); + + // Ok(Json( + // _gateway_uptime_history(storage, nym_contract_cache, identity).await?, + // )) } -#[openapi(tag = "status")] +#[openapi(tag = "status", deprecated = true)] #[get("/gateway//core-status-count?")] pub(crate) async fn gateway_core_status_count( storage: &State, @@ -146,29 +153,34 @@ pub(crate) async fn gateway_core_status_count( )) } -#[openapi(tag = "status")] +#[openapi(tag = "status", deprecated = true)] #[get("/mixnode//report")] pub(crate) async fn mixnode_report( cache: &State, - mix_id: MixId, + mix_id: NodeId, ) -> Result, RocketErrorResponse> { Ok(Json(_mixnode_report(cache, mix_id).await?)) } -#[openapi(tag = "status")] +#[openapi(tag = "status", deprecated = true)] #[get("/mixnode//history")] pub(crate) async fn mixnode_uptime_history( storage: &State, - mix_id: MixId, + nym_contract_cache: &State, + mix_id: NodeId, ) -> Result, RocketErrorResponse> { - Ok(Json(_mixnode_uptime_history(storage, mix_id).await?)) + todo!("rebasing"); + + // Ok(Json( + // _mixnode_uptime_history(storage, nym_contract_cache, mix_id).await?, + // )) } -#[openapi(tag = "status")] +#[openapi(tag = "status", deprecated = true)] #[get("/mixnode//core-status-count?")] pub(crate) async fn mixnode_core_status_count( storage: &State, - mix_id: MixId, + mix_id: NodeId, since: Option, ) -> Result, RocketErrorResponse> { Ok(Json( @@ -176,28 +188,28 @@ pub(crate) async fn mixnode_core_status_count( )) } -#[openapi(tag = "status")] +#[openapi(tag = "status", deprecated = true)] #[get("/mixnode//status")] pub(crate) async fn get_mixnode_status( cache: &State, - mix_id: MixId, + mix_id: NodeId, ) -> Json { Json(_get_mixnode_status(cache, mix_id).await) } -#[openapi(tag = "status")] +#[openapi(tag = "status", deprecated = true)] #[get("/mixnode//reward-estimation")] pub(crate) async fn get_mixnode_reward_estimation( cache: &State, validator_cache: &State, - mix_id: MixId, + mix_id: NodeId, ) -> Result, RocketErrorResponse> { Ok(Json( _get_mixnode_reward_estimation(cache, validator_cache, mix_id).await?, )) } -#[openapi(tag = "status")] +#[openapi(tag = "status", deprecated = true)] #[post( "/mixnode//compute-reward-estimation", data = "" @@ -206,7 +218,7 @@ pub(crate) async fn compute_mixnode_reward_estimation( user_reward_param: Json, cache: &State, validator_cache: &State, - mix_id: MixId, + mix_id: NodeId, ) -> Result, RocketErrorResponse> { Ok(Json( _compute_mixnode_reward_estimation( @@ -219,39 +231,39 @@ pub(crate) async fn compute_mixnode_reward_estimation( )) } -#[openapi(tag = "status")] +#[openapi(tag = "status", deprecated = true)] #[get("/mixnode//stake-saturation")] pub(crate) async fn get_mixnode_stake_saturation( cache: &State, validator_cache: &State, - mix_id: MixId, + mix_id: NodeId, ) -> Result, RocketErrorResponse> { Ok(Json( _get_mixnode_stake_saturation(cache, validator_cache, mix_id).await?, )) } -#[openapi(tag = "status")] +#[openapi(tag = "status", deprecated = true)] #[get("/mixnode//inclusion-probability")] pub(crate) async fn get_mixnode_inclusion_probability( cache: &State, - mix_id: MixId, + mix_id: NodeId, ) -> Result, RocketErrorResponse> { Ok(Json( _get_mixnode_inclusion_probability(cache, mix_id).await?, )) } -#[openapi(tag = "status")] +#[openapi(tag = "status", deprecated = true)] #[get("/mixnode//avg_uptime")] pub(crate) async fn get_mixnode_avg_uptime( cache: &State, - mix_id: MixId, + mix_id: NodeId, ) -> Result, RocketErrorResponse> { Ok(Json(_get_mixnode_avg_uptime(cache, mix_id).await?)) } -#[openapi(tag = "status")] +#[openapi(tag = "status", deprecated = true)] #[get("/gateway//avg_uptime")] pub(crate) async fn get_gateway_avg_uptime( cache: &State, @@ -260,7 +272,7 @@ pub(crate) async fn get_gateway_avg_uptime( Ok(Json(_get_gateway_avg_uptime(cache, identity).await?)) } -#[openapi(tag = "status")] +#[openapi(tag = "status", deprecated = true)] #[get("/mixnodes/inclusion_probability")] pub(crate) async fn get_mixnode_inclusion_probabilities( cache: &State, @@ -268,15 +280,17 @@ pub(crate) async fn get_mixnode_inclusion_probabilities( Ok(Json(_get_mixnode_inclusion_probabilities(cache).await?)) } -#[openapi(tag = "status")] +#[openapi(tag = "status", deprecated = true)] #[get("/mixnodes/detailed")] pub async fn get_mixnodes_detailed( cache: &State, ) -> Json> { - Json(_get_mixnodes_detailed(cache).await) + todo!("rebasing"); + + // Json(_get_legacy_mixnodes_detailed(cache).await) } -#[openapi(tag = "status")] +#[openapi(tag = "status", deprecated = true)] #[get("/mixnodes/detailed-unfiltered")] pub async fn get_mixnodes_detailed_unfiltered( cache: &State, @@ -284,36 +298,45 @@ pub async fn get_mixnodes_detailed_unfiltered( Json(_get_mixnodes_detailed_unfiltered(cache).await) } -#[openapi(tag = "status")] +#[openapi(tag = "status", deprecated = true)] #[get("/mixnodes/rewarded/detailed")] pub async fn get_rewarded_set_detailed( - cache: &State, + status_cache: &State, + contract_cache: &State, ) -> Json> { - Json(_get_rewarded_set_detailed(cache).await) + todo!("rebasing"); + + // Json(_get_rewarded_set_legacy_mixnodes_detailed(status_cache, contract_cache).await) } -#[openapi(tag = "status")] +#[openapi(tag = "status", deprecated = true)] #[get("/mixnodes/active/detailed")] pub async fn get_active_set_detailed( - cache: &State, + status_cache: &State, + contract_cache: &State, ) -> Json> { - Json(_get_active_set_detailed(cache).await) + todo!("rebasing"); + + // Json(_get_active_set_legacy_mixnodes_detailed(status_cache, contract_cache).await) } -#[openapi(tag = "status")] +#[openapi(tag = "status", deprecated = true)] #[get("/gateways/detailed")] pub async fn get_gateways_detailed( cache: &State, ) -> Json> { - Json(_get_gateways_detailed(cache).await) + todo!("rebasing"); + + // Json(_get_legacy_gateways_detailed(cache).await) } -#[openapi(tag = "status")] +#[openapi(tag = "status", deprecated = true)] #[get("/gateways/detailed-unfiltered")] pub async fn get_gateways_detailed_unfiltered( cache: &State, ) -> Json> { - Json(_get_gateways_detailed_unfiltered(cache).await) + todo!("rebasing"); + // Json(_get_legacy_gateways_detailed_unfiltered(cache).await) } pub mod unstable { @@ -325,7 +348,7 @@ pub mod unstable { TestRoute, }; use nym_api_requests::pagination::Pagination; - use nym_mixnet_contract_common::MixId; + use nym_mixnet_contract_common::NodeId; use rocket::http::Status; use rocket::serde::json::Json; use rocket::State; @@ -421,7 +444,7 @@ pub mod unstable { const DEFAULT_TEST_RESULTS_PAGE_SIZE: u32 = 50; async fn _mixnode_test_results( - mix_id: MixId, + mix_id: NodeId, page: u32, per_page: u32, info_cache: &State, @@ -482,7 +505,7 @@ pub mod unstable { #[openapi(tag = "UNSTABLE - DO **NOT** USE")] #[get("/mixnodes/unstable//test-results?")] pub async fn mixnode_test_results( - mix_id: MixId, + mix_id: NodeId, pagination: PaginationRequest, info_cache: &State, storage: &State, diff --git a/nym-api/src/node_status_api/uptime_updater.rs b/nym-api/src/node_status_api/uptime_updater.rs index 2a33a0118f..67de44839b 100644 --- a/nym-api/src/node_status_api/uptime_updater.rs +++ b/nym-api/src/node_status_api/uptime_updater.rs @@ -6,11 +6,12 @@ use crate::node_status_api::models::{ }; use crate::node_status_api::ONE_DAY; use crate::storage::NymApiStorage; -use log::error; use nym_task::{TaskClient, TaskManager}; use std::time::Duration; use time::{OffsetDateTime, PrimitiveDateTime, Time}; use tokio::time::{interval, sleep}; +use tracing::error; +use tracing::{info, trace, warn}; pub(crate) struct HistoricalUptimeUpdater { storage: NymApiStorage, @@ -88,7 +89,7 @@ impl HistoricalUptimeUpdater { // resultant Duration is positive let time_left: Duration = (update_datetime - now).try_into().unwrap(); - log::info!( + info!( "waiting until {update_datetime} to update the historical uptimes for the first time ({} seconds left)", time_left.as_secs() ); diff --git a/nym-api/src/node_status_api/utils.rs b/nym-api/src/node_status_api/utils.rs index 0682cf62ba..4d86be5faa 100644 --- a/nym-api/src/node_status_api/utils.rs +++ b/nym-api/src/node_status_api/utils.rs @@ -4,23 +4,23 @@ use crate::node_status_api::models::Uptime; use crate::node_status_api::{FIFTEEN_MINUTES, ONE_HOUR}; use crate::storage::models::NodeStatus; -use log::warn; -use nym_mixnet_contract_common::MixId; +use nym_mixnet_contract_common::NodeId; +use tracing::warn; use time::OffsetDateTime; // A temporary helper structs used to produce reports for active nodes. pub(crate) struct ActiveMixnodeStatuses { - pub(crate) mix_id: MixId, + pub(crate) mix_id: NodeId, pub(crate) identity: String, - pub(crate) owner: String, pub(crate) statuses: Vec, } pub(crate) struct ActiveGatewayStatuses { + pub(crate) node_id: NodeId, + pub(crate) identity: String, - pub(crate) owner: String, pub(crate) statuses: Vec, } diff --git a/nym-api/src/nym_contract_cache/cache/data.rs b/nym-api/src/nym_contract_cache/cache/data.rs index c04404f2df..2ced76b8fe 100644 --- a/nym-api/src/nym_contract_cache/cache/data.rs +++ b/nym-api/src/nym_contract_cache/cache/data.rs @@ -2,44 +2,41 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::support::caching::Cache; +use nym_api_requests::legacy::{LegacyGatewayBondWithId, LegacyMixNodeDetailsWithLayer}; use nym_contracts_common::ContractBuildInformation; -use nym_mixnet_contract_common::{ - families::FamilyHead, GatewayBond, IdentityKey, Interval, MixId, MixNodeDetails, - RewardingParams, -}; +use nym_mixnet_contract_common::{Interval, NodeId, NymNodeDetails, RewardedSet, RewardingParams}; use nym_validator_client::nyxd::AccountId; use std::collections::{HashMap, HashSet}; pub(crate) struct ValidatorCacheData { - pub(crate) mixnodes: Cache>, - pub(crate) gateways: Cache>, + pub(crate) legacy_mixnodes: Cache>, + pub(crate) legacy_gateways: Cache>, + pub(crate) nym_nodes: Cache>, + pub(crate) rewarded_set: Cache, - pub(crate) mixnodes_blacklist: Cache>, - pub(crate) gateways_blacklist: Cache>, - - pub(crate) rewarded_set: Cache>, - pub(crate) active_set: Cache>, + // this purposely does not deal with nym-nodes as they don't have a concept of a blacklist. + // instead clients are meant to be filtering out them themselves based on the provided scores. + pub(crate) legacy_mixnodes_blacklist: Cache>, + pub(crate) legacy_gateways_blacklist: Cache>, pub(crate) current_reward_params: Cache>, pub(crate) current_interval: Cache>, - pub(crate) mix_to_family: Cache>, - pub(crate) contracts_info: Cache, } impl ValidatorCacheData { pub(crate) fn new() -> Self { ValidatorCacheData { - mixnodes: Cache::default(), - gateways: Cache::default(), + legacy_mixnodes: Cache::default(), + legacy_gateways: Cache::default(), + nym_nodes: Default::default(), rewarded_set: Cache::default(), - active_set: Cache::default(), - mixnodes_blacklist: Cache::default(), - gateways_blacklist: Cache::default(), + + legacy_mixnodes_blacklist: Cache::default(), + legacy_gateways_blacklist: Cache::default(), current_interval: Cache::default(), current_reward_params: Cache::default(), - mix_to_family: Cache::default(), contracts_info: Cache::default(), } } diff --git a/nym-api/src/nym_contract_cache/cache/mod.rs b/nym-api/src/nym_contract_cache/cache/mod.rs index efaeac74a6..8f7e26caa5 100644 --- a/nym-api/src/nym_contract_cache/cache/mod.rs +++ b/nym-api/src/nym_contract_cache/cache/mod.rs @@ -4,12 +4,11 @@ use crate::nym_contract_cache::cache::data::CachedContractsInfo; use crate::support::caching::Cache; use data::ValidatorCacheData; -use nym_api_requests::models::MixnodeStatus; -use nym_mixnet_contract_common::{ - families::FamilyHead, GatewayBond, IdentityKey, Interval, MixId, MixNodeBond, MixNodeDetails, - RewardingParams, +use nym_api_requests::legacy::{ + LegacyGatewayBondWithId, LegacyMixNodeBondWithLayer, LegacyMixNodeDetailsWithLayer, }; -use rocket::fairing::AdHoc; +use nym_api_requests::models::MixnodeStatus; +use nym_mixnet_contract_common::{Interval, NodeId, NymNodeDetails, RewardedSet, RewardingParams}; use std::{ collections::HashSet, sync::{ @@ -18,12 +17,15 @@ use std::{ }, time::Duration, }; -use tokio::sync::RwLock; +use tokio::sync::{RwLock, RwLockReadGuard}; use tokio::time; +use tracing::{debug, error}; mod data; pub(crate) mod refresher; +const CACHE_TIMEOUT_MS: u64 = 100; + #[derive(Clone)] pub struct NymContractCache { pub(crate) initialised: Arc, @@ -38,38 +40,55 @@ impl NymContractCache { } } - #[deprecated(note = "TODO rocket: obsolete because it's used for Rocket")] - pub fn stage() -> AdHoc { - AdHoc::on_ignite("Validator Cache Stage", |rocket| async { - rocket.manage(Self::new()) - }) + /// Returns a copy of the current cache data. + async fn get_owned( + &self, + fn_arg: impl FnOnce(RwLockReadGuard<'_, ValidatorCacheData>) -> Cache, + ) -> Option> { + match time::timeout(Duration::from_millis(CACHE_TIMEOUT_MS), self.inner.read()).await { + Ok(cache) => Some(fn_arg(cache)), + Err(e) => { + error!("{e}"); + None + } + } + } + + async fn get<'a, T: 'a>( + &'a self, + fn_arg: impl FnOnce(&ValidatorCacheData) -> &Cache, + ) -> Option>> { + match time::timeout(Duration::from_millis(CACHE_TIMEOUT_MS), self.inner.read()).await { + Ok(cache) => Some(RwLockReadGuard::map(cache, |item| fn_arg(item))), + Err(e) => { + error!("{e}"); + None + } + } } - #[allow(clippy::too_many_arguments)] pub(crate) async fn update( &self, - mixnodes: Vec, - gateways: Vec, - rewarded_set: Vec, - active_set: Vec, + mixnodes: Vec, + gateways: Vec, + nym_nodes: Vec, + rewarded_set: RewardedSet, rewarding_params: RewardingParams, current_interval: Interval, - mix_to_family: Vec<(IdentityKey, FamilyHead)>, nym_contracts_info: CachedContractsInfo, ) { match time::timeout(Duration::from_millis(100), self.inner.write()).await { Ok(mut cache) => { - cache.mixnodes.unchecked_update(mixnodes); - cache.gateways.unchecked_update(gateways); + cache.legacy_mixnodes.unchecked_update(mixnodes); + cache.legacy_gateways.unchecked_update(gateways); + cache.nym_nodes.unchecked_update(nym_nodes); cache.rewarded_set.unchecked_update(rewarded_set); - cache.active_set.unchecked_update(active_set); cache .current_reward_params .unchecked_update(Some(rewarding_params)); cache .current_interval .unchecked_update(Some(current_interval)); - cache.mix_to_family.unchecked_update(mix_to_family); cache.contracts_info.unchecked_update(nym_contracts_info) } Err(err) => { @@ -78,39 +97,31 @@ impl NymContractCache { } } - pub async fn mixnodes_blacklist(&self) -> Cache> { - match time::timeout(Duration::from_millis(100), self.inner.read()).await { - Ok(cache) => cache.mixnodes_blacklist.clone_cache(), - Err(err) => { - error!("{err}"); - Cache::new(HashSet::new()) - } - } + pub async fn mixnodes_blacklist(&self) -> Cache> { + self.get_owned(|cache| cache.legacy_mixnodes_blacklist.clone_cache()) + .await + .unwrap_or_default() } - pub async fn gateways_blacklist(&self) -> Cache> { - match time::timeout(Duration::from_millis(100), self.inner.read()).await { - Ok(cache) => cache.gateways_blacklist.clone_cache(), - Err(err) => { - error!("{err}"); - Cache::new(HashSet::new()) - } - } + pub async fn gateways_blacklist(&self) -> Cache> { + self.get_owned(|cache| cache.legacy_gateways_blacklist.clone_cache()) + .await + .unwrap_or_default() } - pub async fn update_mixnodes_blacklist(&self, add: HashSet, remove: HashSet) { + pub async fn update_mixnodes_blacklist(&self, add: HashSet, remove: HashSet) { let blacklist = self.mixnodes_blacklist().await; - let mut blacklist = blacklist.union(&add).cloned().collect::>(); + let mut blacklist = blacklist.union(&add).cloned().collect::>(); let to_remove = blacklist .intersection(&remove) .cloned() - .collect::>(); + .collect::>(); for key in to_remove { blacklist.remove(&key); } match time::timeout(Duration::from_millis(100), self.inner.write()).await { Ok(mut cache) => { - cache.mixnodes_blacklist.unchecked_update(blacklist); + cache.legacy_mixnodes_blacklist.unchecked_update(blacklist); } Err(err) => { error!("Failed to update mixnodes blacklist: {err}"); @@ -118,26 +129,19 @@ impl NymContractCache { } } - pub async fn update_gateways_blacklist( - &self, - add: HashSet, - remove: HashSet, - ) { + pub async fn update_gateways_blacklist(&self, add: HashSet, remove: HashSet) { let blacklist = self.gateways_blacklist().await; - let mut blacklist = blacklist - .union(&add) - .cloned() - .collect::>(); + let mut blacklist = blacklist.union(&add).cloned().collect::>(); let to_remove = blacklist .intersection(&remove) .cloned() - .collect::>(); + .collect::>(); for key in to_remove { blacklist.remove(&key); } match time::timeout(Duration::from_millis(100), self.inner.write()).await { Ok(mut cache) => { - cache.gateways_blacklist.unchecked_update(blacklist); + cache.legacy_gateways_blacklist.unchecked_update(blacklist); } Err(err) => { error!("Failed to update gateways blacklist: {err}"); @@ -145,8 +149,8 @@ impl NymContractCache { } } - pub async fn mixnodes_filtered(&self) -> Vec { - let mixnodes = self.mixnodes_all().await; + pub async fn legacy_mixnodes_filtered(&self) -> Vec { + let mixnodes = self.legacy_mixnodes_all().await; if mixnodes.is_empty() { return Vec::new(); } @@ -162,34 +166,65 @@ impl NymContractCache { } } - pub async fn mixnodes_all(&self) -> Vec { - match time::timeout(Duration::from_millis(100), self.inner.read()).await { - Ok(cache) => cache.mixnodes.clone(), - Err(err) => { - error!("{err}"); - Vec::new() - } - } + pub async fn all_cached_legacy_mixnodes( + &self, + ) -> Option>>> { + self.get(|c| &c.legacy_mixnodes).await } - pub async fn mixnodes_filtered_basic(&self) -> Vec { - self.mixnodes_filtered() + pub async fn legacy_gateway_owner(&self, node_id: NodeId) -> Option { + self.get(|c| &c.legacy_gateways) + .await? + .iter() + .find(|g| g.node_id == node_id) + .map(|g| g.owner.to_string()) + } + + pub async fn legacy_mixnode_owner(&self, node_id: NodeId) -> Option { + self.get(|c| &c.legacy_mixnodes) + .await? + .iter() + .find(|m| m.mix_id() == node_id) + .map(|m| m.bond_information.owner.to_string()) + } + + pub async fn all_cached_legacy_gateways( + &self, + ) -> Option>>> { + self.get(|c| &c.legacy_gateways).await + } + + pub async fn all_cached_nym_nodes( + &self, + ) -> Option>>> { + self.get(|c| &c.nym_nodes).await + } + + pub async fn legacy_mixnodes_all(&self) -> Vec { + self.get_owned(|cache| cache.legacy_mixnodes.clone_cache()) + .await + .unwrap_or_default() + .into_inner() + } + + pub async fn legacy_mixnodes_filtered_basic(&self) -> Vec { + self.legacy_mixnodes_filtered() .await .into_iter() .map(|bond| bond.bond_information) .collect() } - pub async fn mixnodes_all_basic(&self) -> Vec { - self.mixnodes_all() + pub async fn legacy_mixnodes_all_basic(&self) -> Vec { + self.legacy_mixnodes_all() .await .into_iter() .map(|bond| bond.bond_information) .collect() } - pub async fn gateways_filtered(&self) -> Vec { - let gateways = self.gateways_all().await; + pub async fn legacy_gateways_filtered(&self) -> Vec { + let gateways = self.legacy_gateways_all().await; if gateways.is_empty() { return Vec::new(); } @@ -199,107 +234,122 @@ impl NymContractCache { if !blacklist.is_empty() { gateways .into_iter() - .filter(|mix| !blacklist.contains(mix.identity())) + .filter(|gw| !blacklist.contains(&gw.node_id)) .collect() } else { gateways } } - pub async fn gateways_all(&self) -> Vec { - match time::timeout(Duration::from_millis(100), self.inner.read()).await { - Ok(cache) => cache.gateways.clone(), - Err(err) => { - error!("{err}"); - Vec::new() - } - } + pub async fn legacy_gateways_all(&self) -> Vec { + self.get_owned(|cache| cache.legacy_gateways.clone_cache()) + .await + .unwrap_or_default() + .into_inner() } - pub async fn rewarded_set(&self) -> Cache> { - match time::timeout(Duration::from_millis(100), self.inner.read()).await { - Ok(cache) => cache.rewarded_set.clone_cache(), - Err(err) => { - error!("{err}"); - Cache::new(Vec::new()) - } - } + pub async fn nym_nodes(&self) -> Vec { + self.get_owned(|cache| cache.nym_nodes.clone_cache()) + .await + .unwrap_or_default() + .into_inner() } - pub async fn active_set(&self) -> Cache> { - match time::timeout(Duration::from_millis(100), self.inner.read()).await { - Ok(cache) => cache.active_set.clone_cache(), - Err(err) => { - error!("{err}"); - Cache::new(Vec::new()) - } - } + pub async fn nym_nodes_filtered(&self) -> Vec { + todo!() } - pub async fn mix_to_family(&self) -> Cache> { - match time::timeout(Duration::from_millis(100), self.inner.read()).await { - Ok(cache) => cache.mix_to_family.clone_cache(), - Err(err) => { - error!("{err}"); - Cache::new(Vec::new()) - } + pub async fn rewarded_set(&self) -> Option>> { + self.get(|cache| &cache.rewarded_set).await + } + + pub async fn rewarded_set_owned(&self) -> Cache { + self.get_owned(|cache| cache.rewarded_set.clone_cache()) + .await + .unwrap_or_default() + } + + pub async fn legacy_v1_rewarded_set_mixnodes(&self) -> Vec { + let Some(rewarded_set) = self.rewarded_set().await else { + return Vec::new(); + }; + + let mut rewarded_nodes = rewarded_set + .active_mixnodes() + .into_iter() + .collect::>(); + + // rewarded mixnode = active or standby + for standby in &rewarded_set.standby { + rewarded_nodes.insert(*standby); } + + self.legacy_mixnodes_all() + .await + .into_iter() + .filter(|m| rewarded_nodes.contains(&m.mix_id())) + .collect() + } + + pub async fn legacy_v1_active_set_mixnodes(&self) -> Vec { + let Some(rewarded_set) = self.rewarded_set().await else { + return Vec::new(); + }; + + let active_nodes = rewarded_set + .active_mixnodes() + .into_iter() + .collect::>(); + + self.legacy_mixnodes_all() + .await + .into_iter() + .filter(|m| active_nodes.contains(&m.mix_id())) + .collect() } pub(crate) async fn interval_reward_params(&self) -> Cache> { - match time::timeout(Duration::from_millis(100), self.inner.read()).await { - Ok(cache) => cache.current_reward_params.clone_cache(), - Err(err) => { - error!("{err}"); - Cache::new(None) - } - } + self.get_owned(|cache| cache.current_reward_params.clone_cache()) + .await + .unwrap_or_default() } pub(crate) async fn current_interval(&self) -> Cache> { - match time::timeout(Duration::from_millis(100), self.inner.read()).await { - Ok(cache) => cache.current_interval.clone_cache(), - Err(err) => { - error!("{err}"); - Cache::new(None) - } - } + self.get_owned(|cache| cache.current_interval.clone_cache()) + .await + .unwrap_or_default() } pub(crate) async fn contract_details(&self) -> Cache { - match time::timeout(Duration::from_millis(100), self.inner.read()).await { - Ok(cache) => cache.contracts_info.clone_cache(), - Err(err) => { - error!("{err}"); - Cache::default() - } - } + self.get_owned(|cache| cache.contracts_info.clone_cache()) + .await + .unwrap_or_default() } - pub async fn mixnode_details(&self, mix_id: MixId) -> (Option, MixnodeStatus) { - // it might not be the most optimal to possibly iterate the entire vector to find (or not) - // the relevant value. However, the vectors are relatively small (< 10_000 elements, < 1000 for active set) + pub async fn legacy_mixnode_details( + &self, + mix_id: NodeId, + ) -> (Option, MixnodeStatus) { + // the old behaviour was to get the nodes from the filtered list, so let's not change it here + let rewarded_set = self.rewarded_set_owned().await; + let all_bonded = &self.legacy_mixnodes_filtered().await; + let Some(bond) = all_bonded.iter().find(|mix| mix.mix_id() == mix_id) else { + return (None, MixnodeStatus::NotFound); + }; - let active_set = &self.active_set().await; - if let Some(bond) = active_set.iter().find(|mix| mix.mix_id() == mix_id) { + if rewarded_set.is_active_mixnode(&mix_id) { return (Some(bond.clone()), MixnodeStatus::Active); } - let rewarded_set = &self.rewarded_set().await; - if let Some(bond) = rewarded_set.iter().find(|mix| mix.mix_id() == mix_id) { + if rewarded_set.is_standby(&mix_id) { return (Some(bond.clone()), MixnodeStatus::Standby); } - let all_bonded = &self.mixnodes_filtered().await; - if let Some(bond) = all_bonded.iter().find(|mix| mix.mix_id() == mix_id) { - (Some(bond.clone()), MixnodeStatus::Inactive) - } else { - (None, MixnodeStatus::NotFound) - } + (Some(bond.clone()), MixnodeStatus::Inactive) } - pub async fn mixnode_status(&self, mix_id: MixId) -> MixnodeStatus { - self.mixnode_details(mix_id).await.1 + pub async fn mixnode_status(&self, mix_id: NodeId) -> MixnodeStatus { + self.legacy_mixnode_details(mix_id).await.1 } pub fn initialised(&self) -> bool { diff --git a/nym-api/src/nym_contract_cache/cache/refresher.rs b/nym-api/src/nym_contract_cache/cache/refresher.rs index d200a3445d..a46ef92afd 100644 --- a/nym-api/src/nym_contract_cache/cache/refresher.rs +++ b/nym-api/src/nym_contract_cache/cache/refresher.rs @@ -6,14 +6,21 @@ use crate::nym_contract_cache::cache::data::{CachedContractInfo, CachedContracts use crate::nyxd::Client; use crate::support::caching::CacheNotification; use anyhow::Result; -use nym_mixnet_contract_common::{MixId, MixNodeDetails, RewardedSetNodeStatus}; +use nym_api_requests::legacy::{ + LegacyGatewayBondWithId, LegacyMixNodeBondWithLayer, LegacyMixNodeDetailsWithLayer, +}; +use nym_mixnet_contract_common::{LegacyMixLayer, RewardedSet}; use nym_task::TaskClient; use nym_validator_client::nyxd::contract_traits::{ MixnetQueryClient, NymContractsProvider, VestingQueryClient, }; +use rand::prelude::SliceRandom; +use rand::rngs::OsRng; +use std::collections::HashSet; use std::{collections::HashMap, sync::atomic::Ordering, time::Duration}; use tokio::sync::watch; use tokio::time; +use tracing::{error, info, trace, warn}; pub struct NymContractCacheRefresher { nyxd_client: Client, @@ -109,15 +116,66 @@ impl NymContractCacheRefresher { let rewarding_params = self.nyxd_client.get_current_rewarding_parameters().await?; let current_interval = self.nyxd_client.get_current_interval().await?.interval; - let mixnodes = self.nyxd_client.get_mixnodes().await?; - let gateways = self.nyxd_client.get_gateways().await?; + let nym_nodes = self.nyxd_client.get_nymnodes().await?; + let mixnode_details = self.nyxd_client.get_mixnodes().await?; + let gateway_bonds = self.nyxd_client.get_gateways().await?; + let gateway_ids: HashMap<_, _> = self + .nyxd_client + .get_gateway_ids() + .await? + .into_iter() + .map(|id| (id.identity, id.node_id)) + .collect(); - let mix_to_family = self.nyxd_client.get_all_family_members().await?; + let mut gateways = Vec::with_capacity(gateway_bonds.len()); + for bond in gateway_bonds { + // we explicitly panic here because that value MUST exist. + // if it doesn't, we messed up the migration and we have big problems + let node_id = *gateway_ids.get(bond.identity()).unwrap_or_else(|| { + panic!( + "CONTRACT DATA INCONSISTENCY: MISSING GATEWAY ID FOR: {}", + bond.identity() + ) + }); + gateways.push(LegacyGatewayBondWithId { bond, node_id }) + } - let rewarded_set_map = self.get_rewarded_set_map().await; + let rewarded_set = self.get_rewarded_set().await; + let layer1 = rewarded_set.layer1.iter().collect::>(); + let layer2 = rewarded_set.layer2.iter().collect::>(); + let layer3 = rewarded_set.layer3.iter().collect::>(); - let (rewarded_set, active_set) = - Self::collect_rewarded_and_active_set_details(&mixnodes, &rewarded_set_map); + let layer_choices = [ + LegacyMixLayer::One, + LegacyMixLayer::Two, + LegacyMixLayer::Three, + ]; + let mut rng = OsRng; + let mut mixnodes = Vec::with_capacity(mixnode_details.len()); + for detail in mixnode_details { + // if node is not in the rewarded set, well. + // slap a random layer on it because legacy clients don't understand a concept of layerless mixnodes + let layer = if layer1.contains(&detail.mix_id()) { + LegacyMixLayer::One + } else if layer2.contains(&detail.mix_id()) { + LegacyMixLayer::Two + } else if layer3.contains(&detail.mix_id()) { + LegacyMixLayer::Three + } else { + // SAFETY: the slice is not empty so the unwrap is fine + #[allow(clippy::unwrap_used)] + layer_choices.choose(&mut rng).copied().unwrap() + }; + + mixnodes.push(LegacyMixNodeDetailsWithLayer { + bond_information: LegacyMixNodeBondWithLayer { + bond: detail.bond_information, + layer, + }, + rewarding_details: detail.rewarding_details, + pending_changes: detail.pending_changes, + }) + } let contract_info = self.get_nym_contracts_info().await?; @@ -131,11 +189,10 @@ impl NymContractCacheRefresher { .update( mixnodes, gateways, + nym_nodes, rewarded_set, - active_set, rewarding_params, current_interval, - mix_to_family, contract_info, ) .await; @@ -147,32 +204,31 @@ impl NymContractCacheRefresher { Ok(()) } - async fn get_rewarded_set_map(&self) -> HashMap { + async fn get_rewarded_set(&self) -> RewardedSet { self.nyxd_client - .get_rewarded_set_mixnodes() + .get_rewarded_set_nodes() .await - .map(|nodes| nodes.into_iter().collect()) .unwrap_or_default() } - fn collect_rewarded_and_active_set_details( - all_mixnodes: &[MixNodeDetails], - rewarded_set_nodes: &HashMap, - ) -> (Vec, Vec) { - let mut active_set = Vec::new(); - let mut rewarded_set = Vec::new(); - - for mix in all_mixnodes { - if let Some(status) = rewarded_set_nodes.get(&mix.mix_id()) { - rewarded_set.push(mix.clone()); - if status.is_active() { - active_set.push(mix.clone()) - } - } - } - - (rewarded_set, active_set) - } + // fn collect_rewarded_and_active_set_details( + // all_mixnodes: &[MixNodeDetails], + // rewarded_set_nodes: RewardedSet, + // ) -> (Vec, Vec) { + // let mut active_set = Vec::new(); + // let mut rewarded_set = Vec::new(); + // + // for mix in all_mixnodes { + // if let Some(status) = rewarded_set_nodes.get(&mix.mix_id()) { + // rewarded_set.push(mix.clone()); + // if status.is_active() { + // active_set.push(mix.clone()) + // } + // } + // } + // + // (rewarded_set, active_set) + // } pub(crate) async fn run(&self, mut shutdown: TaskClient) { let mut interval = time::interval(self.caching_interval); diff --git a/nym-api/src/nym_contract_cache/handlers.rs b/nym-api/src/nym_contract_cache/handlers.rs index 84f2c70fd2..d57f1317c4 100644 --- a/nym-api/src/nym_contract_cache/handlers.rs +++ b/nym-api/src/nym_contract_cache/handlers.rs @@ -1,20 +1,21 @@ // Copyright 2021-2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::{ - node_status_api::helpers_deprecated::{ - _get_active_set_detailed, _get_mixnodes_detailed, _get_rewarded_set_detailed, - }, - v2::AxumAppState, +use crate::node_status_api::helpers::{ + _get_active_set_legacy_mixnodes_detailed, _get_legacy_mixnodes_detailed, + _get_rewarded_set_legacy_mixnodes_detailed, }; -use axum::{extract, Router}; +use crate::support::http::state::AppState; +use axum::extract::State; +use axum::{Json, Router}; +use nym_api_requests::legacy::LegacyMixNodeDetailsWithLayer; use nym_api_requests::models::MixNodeBondAnnotated; -use nym_mixnet_contract_common::{ - mixnode::MixNodeDetails, reward_params::RewardingParams, GatewayBond, Interval, MixId, -}; +use nym_mixnet_contract_common::{reward_params::RewardingParams, GatewayBond, Interval, NodeId}; use std::collections::HashSet; -pub(crate) fn nym_contract_cache_routes() -> Router { +// we want to mark the routes as deprecated in swagger, but still expose them +#[allow(deprecated)] +pub(crate) fn nym_contract_cache_routes() -> Router { Router::new() .route("/mixnodes", axum::routing::get(get_mixnodes)) .route( @@ -52,13 +53,16 @@ pub(crate) fn nym_contract_cache_routes() -> Router { get, path = "/v1/mixnodes", responses( - (status = 200, body = Vec) + (status = 200, body = Vec) ) )] -async fn get_mixnodes( - extract::State(state): extract::State, -) -> axum::Json> { - state.nym_contract_cache().mixnodes_filtered().await.into() +#[deprecated] +async fn get_mixnodes(State(state): State) -> Json> { + state + .nym_contract_cache() + .legacy_mixnodes_filtered() + .await + .into() } // DEPRECATED: this endpoint now lives in `node_status_api`. Once all consumers are updated, @@ -76,10 +80,9 @@ async fn get_mixnodes( (status = 200, body = Vec) ) )] -async fn get_mixnodes_detailed( - extract::State(state): extract::State, -) -> axum::Json> { - _get_mixnodes_detailed(state.node_status_cache()) +#[deprecated] +async fn get_mixnodes_detailed(State(state): State) -> Json> { + _get_legacy_mixnodes_detailed(state.node_status_cache()) .await .into() } @@ -92,10 +95,17 @@ async fn get_mixnodes_detailed( (status = 200, body = Vec) ) )] -async fn get_gateways( - extract::State(state): extract::State, -) -> axum::Json> { - state.nym_contract_cache().gateways_filtered().await.into() +#[deprecated] +async fn get_gateways(State(state): State) -> Json> { + Json( + state + .nym_contract_cache() + .legacy_gateways_filtered() + .await + .into_iter() + .map(Into::into) + .collect(), + ) } #[utoipa::path( @@ -103,18 +113,20 @@ async fn get_gateways( get, path = "/v1/mixnodes/rewarded", responses( - (status = 200, body = Vec) + (status = 200, body = Vec) ) )] +#[deprecated] async fn get_rewarded_set( - extract::State(state): extract::State, -) -> axum::Json> { - state - .nym_contract_cache() - .rewarded_set() - .await - .to_owned() - .into() + State(state): State, +) -> Json> { + Json( + state + .nym_contract_cache() + .legacy_v1_rewarded_set_mixnodes() + .await + .clone(), + ) } // DEPRECATED: this endpoint now lives in `node_status_api`. Once all consumers are updated, @@ -132,12 +144,16 @@ async fn get_rewarded_set( (status = 200, body = Vec) ) )] +#[deprecated] async fn get_rewarded_set_detailed( - extract::State(state): extract::State, -) -> axum::Json> { - _get_rewarded_set_detailed(state.node_status_cache()) - .await - .into() + State(state): State, +) -> Json> { + _get_rewarded_set_legacy_mixnodes_detailed( + state.node_status_cache(), + state.nym_contract_cache(), + ) + .await + .into() } #[utoipa::path( @@ -145,17 +161,16 @@ async fn get_rewarded_set_detailed( get, path = "/v1/mixnodes/active", responses( - (status = 200, body = Vec) + (status = 200, body = Vec) ) )] -async fn get_active_set( - extract::State(state): extract::State, -) -> axum::Json> { +#[deprecated] +async fn get_active_set(State(state): State) -> Json> { state .nym_contract_cache() - .active_set() + .legacy_v1_active_set_mixnodes() .await - .to_owned() + .clone() .into() } @@ -175,10 +190,9 @@ async fn get_active_set( (status = 200, body = Vec) ) )] -async fn get_active_set_detailed( - extract::State(state): extract::State, -) -> axum::Json> { - _get_active_set_detailed(state.node_status_cache()) +#[deprecated] +async fn get_active_set_detailed(State(state): State) -> Json> { + _get_active_set_legacy_mixnodes_detailed(state.node_status_cache(), state.nym_contract_cache()) .await .into() } @@ -188,12 +202,11 @@ async fn get_active_set_detailed( get, path = "/v1/mixnodes/blacklisted", responses( - (status = 200, body = Option>) + (status = 200, body = Option>) ) )] -async fn get_blacklisted_mixnodes( - extract::State(state): extract::State, -) -> axum::Json>> { +#[deprecated] +async fn get_blacklisted_mixnodes(State(state): State) -> Json>> { let blacklist = state .nym_contract_cache() .mixnodes_blacklist() @@ -215,20 +228,22 @@ async fn get_blacklisted_mixnodes( (status = 200, body = Option>) ) )] -async fn get_blacklisted_gateways( - extract::State(state): extract::State, -) -> axum::Json>> { - let blacklist = state - .nym_contract_cache() - .gateways_blacklist() - .await - .to_owned(); +#[deprecated] +async fn get_blacklisted_gateways(State(state): State) -> Json>> { + let cache = state.nym_contract_cache(); + let blacklist = cache.gateways_blacklist().await.clone(); if blacklist.is_empty() { - None + Json(None) } else { - Some(blacklist) + let gateways = cache.legacy_gateways_all().await; + Json(Some( + gateways + .into_iter() + .filter(|g| blacklist.contains(&g.node_id)) + .map(|g| g.gateway.identity_key.clone()) + .collect(), + )) } - .into() } #[utoipa::path( @@ -240,8 +255,8 @@ async fn get_blacklisted_gateways( ) )] async fn get_interval_reward_params( - extract::State(state): extract::State, -) -> axum::Json> { + State(state): State, +) -> Json> { state .nym_contract_cache() .interval_reward_params() @@ -258,9 +273,7 @@ async fn get_interval_reward_params( (status = 200, body = Option) ) )] -async fn get_current_epoch( - extract::State(state): extract::State, -) -> axum::Json> { +async fn get_current_epoch(State(state): State) -> Json> { state .nym_contract_cache() .current_interval() diff --git a/nym-api/src/nym_contract_cache/legacy_helpers.rs b/nym-api/src/nym_contract_cache/legacy_helpers.rs new file mode 100644 index 0000000000..939f19b3a9 --- /dev/null +++ b/nym-api/src/nym_contract_cache/legacy_helpers.rs @@ -0,0 +1,2 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only diff --git a/nym-api/src/nym_contract_cache/mod.rs b/nym-api/src/nym_contract_cache/mod.rs index 9b1f48d9ad..49f4147a69 100644 --- a/nym-api/src/nym_contract_cache/mod.rs +++ b/nym-api/src/nym_contract_cache/mod.rs @@ -4,33 +4,30 @@ use crate::nym_contract_cache::cache::NymContractCache; use crate::support::{self, config, nyxd}; use nym_task::TaskManager; -use okapi::openapi3::OpenApi; -use rocket::Route; -use rocket_okapi::openapi_get_routes_spec; -use rocket_okapi::settings::OpenApiSettings; use self::cache::refresher::NymContractCacheRefresher; pub(crate) mod cache; -#[cfg(feature = "axum")] pub(crate) mod handlers; -pub(crate) mod routes; - -pub(crate) fn nym_contract_cache_routes(settings: &OpenApiSettings) -> (Vec, OpenApi) { - openapi_get_routes_spec![ - settings: routes::get_mixnodes, - routes::get_mixnodes_detailed, - routes::get_gateways, - routes::get_active_set, - routes::get_active_set_detailed, - routes::get_rewarded_set, - routes::get_rewarded_set_detailed, - routes::get_blacklisted_mixnodes, - routes::get_blacklisted_gateways, - routes::get_interval_reward_params, - routes::get_current_epoch, - ] -} +pub(crate) mod legacy_helpers; +// pub(crate) mod routes; +// +// pub(crate) fn nym_contract_cache_routes(settings: &OpenApiSettings) -> (Vec, OpenApi) { +// openapi_get_routes_spec![ +// settings: routes::get_mixnodes, +// routes::get_mixnodes_detailed, +// routes::get_gateways, +// routes::get_active_set, +// routes::get_active_set_detailed, +// routes::get_rewarded_set, +// routes::get_rewarded_set_detailed, +// routes::get_blacklisted_mixnodes, +// routes::get_blacklisted_gateways, +// routes::get_blacklisted_gateways_v2, +// routes::get_interval_reward_params, +// routes::get_current_epoch, +// ] +// } pub(crate) fn start_refresher( config: &config::NodeStatusAPI, diff --git a/nym-api/src/nym_contract_cache/routes.rs b/nym-api/src/nym_contract_cache/routes.rs index cef7f04c9c..52a9dd70e0 100644 --- a/nym-api/src/nym_contract_cache/routes.rs +++ b/nym-api/src/nym_contract_cache/routes.rs @@ -1,28 +1,21 @@ // Copyright 2021-2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::{ - node_status_api::{ - helpers_deprecated::{ - _get_active_set_detailed, _get_mixnodes_detailed, _get_rewarded_set_detailed, - }, - NodeStatusCache, - }, - nym_contract_cache::cache::NymContractCache, -}; +use crate::{node_status_api::NodeStatusCache, nym_contract_cache::cache::NymContractCache}; use nym_api_requests::models::MixNodeBondAnnotated; -use nym_mixnet_contract_common::{ - mixnode::MixNodeDetails, reward_params::RewardingParams, GatewayBond, Interval, MixId, -}; +use nym_mixnet_contract_common::{reward_params::RewardingParams, GatewayBond, Interval, NodeId}; +use nym_api_requests::legacy::LegacyMixNodeDetailsWithLayer; use rocket::{serde::json::Json, State}; use rocket_okapi::openapi; use std::collections::HashSet; -#[openapi(tag = "contract-cache")] +#[openapi(tag = "contract-cache", deprecated = true)] #[get("/mixnodes")] -pub async fn get_mixnodes(cache: &State) -> Json> { - Json(cache.mixnodes_filtered().await) +pub async fn get_mixnodes( + cache: &State, +) -> Json> { + Json(cache.legacy_mixnodes_filtered().await) } // DEPRECATED: this endpoint now lives in `node_status_api`. Once all consumers are updated, @@ -32,24 +25,34 @@ pub async fn get_mixnodes(cache: &State) -> Json, ) -> Json> { - Json(_get_mixnodes_detailed(cache).await) + todo!("rebasing") + // Json(_get_legacy_mixnodes_detailed(cache).await) } -#[openapi(tag = "contract-cache")] +#[openapi(tag = "contract-cache", deprecated = true)] #[get("/gateways")] pub async fn get_gateways(cache: &State) -> Json> { - Json(cache.gateways_filtered().await) + Json( + cache + .legacy_gateways_filtered() + .await + .into_iter() + .map(Into::into) + .collect(), + ) } -#[openapi(tag = "contract-cache")] +#[openapi(tag = "contract-cache", deprecated = true)] #[get("/mixnodes/rewarded")] -pub async fn get_rewarded_set(cache: &State) -> Json> { - Json(cache.rewarded_set().await.clone()) +pub async fn get_rewarded_set( + cache: &State, +) -> Json> { + Json(cache.legacy_v1_rewarded_set_mixnodes().await.clone()) } // DEPRECATED: this endpoint now lives in `node_status_api`. Once all consumers are updated, @@ -59,18 +62,22 @@ pub async fn get_rewarded_set(cache: &State) -> Json, + status_cache: &State, + contract_cache: &State, ) -> Json> { - Json(_get_rewarded_set_detailed(cache).await) + todo!("rebasing") + // Json(_get_rewarded_set_legacy_mixnodes_detailed(status_cache, contract_cache).await) } -#[openapi(tag = "contract-cache")] +#[openapi(tag = "contract-cache", deprecated = true)] #[get("/mixnodes/active")] -pub async fn get_active_set(cache: &State) -> Json> { - Json(cache.active_set().await.clone()) +pub async fn get_active_set( + cache: &State, +) -> Json> { + Json(cache.legacy_v1_active_set_mixnodes().await.clone()) } // DEPRECATED: this endpoint now lives in `node_status_api`. Once all consumers are updated, @@ -80,19 +87,21 @@ pub async fn get_active_set(cache: &State) -> Json, + status_cache: &State, + contract_cache: &State, ) -> Json> { - Json(_get_active_set_detailed(cache).await) + todo!("rebasing") + // Json(_get_active_set_legacy_mixnodes_detailed(status_cache, contract_cache).await) } -#[openapi(tag = "contract-cache")] +#[openapi(tag = "contract-cache", deprecated = true)] #[get("/mixnodes/blacklisted")] pub async fn get_blacklisted_mixnodes( cache: &State, -) -> Json>> { +) -> Json>> { let blacklist = cache.mixnodes_blacklist().await.clone(); if blacklist.is_empty() { Json(None) @@ -101,11 +110,31 @@ pub async fn get_blacklisted_mixnodes( } } -#[openapi(tag = "contract-cache")] +#[openapi(tag = "contract-cache", deprecated = true)] #[get("/gateways/blacklisted")] pub async fn get_blacklisted_gateways( cache: &State, ) -> Json>> { + let blacklist = cache.gateways_blacklist().await.clone(); + if blacklist.is_empty() { + Json(None) + } else { + let gateways = cache.legacy_gateways_all().await; + Json(Some( + gateways + .into_iter() + .filter(|g| blacklist.contains(&g.node_id)) + .map(|g| g.gateway.identity_key.clone()) + .collect(), + )) + } +} + +#[openapi(tag = "contract-cache", deprecated = true)] +#[get("/gateways/blacklisted_v2")] +pub async fn get_blacklisted_gateways_v2( + cache: &State, +) -> Json>> { let blacklist = cache.gateways_blacklist().await.clone(); if blacklist.is_empty() { Json(None) diff --git a/nym-api/src/nym_nodes/handlers.rs b/nym-api/src/nym_nodes/handlers.rs deleted file mode 100644 index 7f775d8de4..0000000000 --- a/nym-api/src/nym_nodes/handlers.rs +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::v2::AxumAppState; -use axum::{extract::State, Json, Router}; -use nym_api_requests::models::{DescribedGateway, DescribedMixNode}; -use nym_mixnet_contract_common::MixNodeBond; -use std::ops::Deref; - -// obviously this should get refactored later on because gateways will go away. -// unless maybe this will be filtering based on which nodes got assigned gateway role? TBD - -pub(crate) fn nym_node_routes() -> axum::Router { - Router::new() - .route( - "/gateways/described", - axum::routing::get(get_gateways_described), - ) - .route( - "/mixnodes/described", - axum::routing::get(get_mixnodes_described), - ) -} - -#[utoipa::path( - tag = "Nym Nodes", - get, - path = "/v1/gateways/described", - responses( - (status = 200, body = Vec) - ) -)] -async fn get_gateways_described(State(state): State) -> Json> { - let gateways = state.nym_contract_cache().gateways_filtered().await; - if gateways.is_empty() { - return Json(Vec::new()); - } - - // if the self describe cache is unavailable, well, don't attach describe data - let Ok(self_descriptions) = state.described_nodes_state().get().await else { - return Json(gateways.into_iter().map(Into::into).collect()); - }; - - // TODO: this is extremely inefficient, but given we don't have many gateways, - // it shouldn't be too much of a problem until we go ahead with directory v3 / the smoosh 2: electric smoosharoo, - // but at that point (I hope) the whole caching situation should get refactored - Json( - gateways - .into_iter() - .map(|bond| DescribedGateway { - self_described: self_descriptions.deref().get(bond.identity()).cloned(), - bond, - }) - .collect(), - ) -} - -#[utoipa::path( - tag = "Nym Nodes", - get, - path = "/v1/mixnodes/described", - responses( - (status = 200, body = Vec) - ) -)] -async fn get_mixnodes_described(State(state): State) -> Json> { - let mixnodes = state - .nym_contract_cache() - .mixnodes_filtered() - .await - .into_iter() - .map(|m| m.bond_information) - .collect::>(); - if mixnodes.is_empty() { - return Json(Vec::new()); - } - - // if the self describe cache is unavailable, well, don't attach describe data - let Ok(self_descriptions) = state.described_nodes_state().get().await else { - return Json(mixnodes.into_iter().map(Into::into).collect()); - }; - - // TODO: this is extremely inefficient, but given we don't have many gateways, - // it shouldn't be too much of a problem until we go ahead with directory v3 / the smoosh 2: electric smoosharoo, - // but at that point (I hope) the whole caching situation should get refactored - Json( - mixnodes - .into_iter() - .map(|bond| DescribedMixNode { - self_described: self_descriptions.deref().get(bond.identity()).cloned(), - bond, - }) - .collect(), - ) -} diff --git a/nym-api/src/nym_nodes/handlers/legacy.rs b/nym-api/src/nym_nodes/handlers/legacy.rs new file mode 100644 index 0000000000..1ebd9260c7 --- /dev/null +++ b/nym-api/src/nym_nodes/handlers/legacy.rs @@ -0,0 +1,97 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::support::http::state::AppState; +use axum::extract::State; +use axum::{Json, Router}; +use nym_api_requests::models::{LegacyDescribedGateway, LegacyDescribedMixNode}; + +// we want to mark the routes as deprecated in swagger, but still expose them +#[allow(deprecated)] +pub(crate) fn legacy_nym_node_routes() -> Router { + Router::new() + .route( + "/gateways/described", + axum::routing::get(get_gateways_described), + ) + .route( + "/mixnodes/described", + axum::routing::get(get_mixnodes_described), + ) +} + +#[utoipa::path( + tag = "Legacy gateways", + get, + path = "/v1/gateways/described", + responses( + (status = 200, body = Vec) + ) +)] +#[deprecated] +async fn get_gateways_described( + State(state): State, +) -> Json> { + let contract_cache = state.nym_contract_cache(); + let describe_cache = state.described_nodes_cache(); + let gateways = contract_cache.legacy_gateways_filtered().await; + if gateways.is_empty() { + return Json(Vec::new()); + } + + // if the self describe cache is unavailable, well, don't attach describe data and only return legacy gateways + let Ok(self_descriptions) = describe_cache.get().await else { + return Json(gateways.into_iter().map(Into::into).collect()); + }; + + Json( + gateways + .into_iter() + .map(|bond| LegacyDescribedGateway { + self_described: self_descriptions.get_description(&bond.node_id).cloned(), + bond, + }) + .collect(), + ) +} + +#[utoipa::path( + tag = "Legacy Mixnodes", + get, + path = "/v1/mixnodes/described", + responses( + (status = 200, body = Vec) + ) +)] +#[deprecated] +async fn get_mixnodes_described( + State(state): State, +) -> Json> { + let contract_cache = state.nym_contract_cache(); + let describe_cache = state.described_nodes_cache(); + + let mixnodes = contract_cache + .legacy_mixnodes_filtered() + .await + .into_iter() + .map(|m| m.bond_information) + .collect::>(); + if mixnodes.is_empty() { + return Json(Vec::new()); + } + + // if the self describe cache is unavailable, well, don't attach describe data + let Ok(self_descriptions) = describe_cache.get().await else { + return Json(mixnodes.into_iter().map(Into::into).collect()); + }; + + Json( + mixnodes + .into_iter() + .map(|bond| LegacyDescribedMixNode { + self_described: self_descriptions.get_description(&bond.mix_id).cloned(), + bond, + }) + .collect(), + ) +} diff --git a/nym-api/src/nym_nodes/handlers/mod.rs b/nym-api/src/nym_nodes/handlers/mod.rs new file mode 100644 index 0000000000..7655584625 --- /dev/null +++ b/nym-api/src/nym_nodes/handlers/mod.rs @@ -0,0 +1,281 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; +use crate::support::http::helpers::{NodeIdParam, PaginationRequest}; +use crate::support::http::state::AppState; +use axum::extract::{Path, Query, State}; +use axum::routing::get; +use axum::{Json, Router}; +use nym_api_requests::models::{ + AnnotationResponse, NodeDatePerformanceResponse, NodePerformanceResponse, NymNodeData, + PerformanceHistoryResponse, UptimeHistoryResponse, +}; +use nym_api_requests::pagination::{PaginatedResponse, Pagination}; +use nym_contracts_common::NaiveFloat; +use nym_mixnet_contract_common::reward_params::Performance; +use nym_mixnet_contract_common::NymNodeDetails; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use time::Date; +use utoipa::{IntoParams, ToSchema}; + +pub(crate) mod legacy; +pub(crate) mod unstable; + +pub(crate) fn nym_node_routes() -> Router { + Router::new() + .route("/bonded", get(get_bonded_nodes)) + .route("/described", get(get_described_nodes)) + .route("/annotation/:node_id", get(get_node_annotation)) + .route("/performance/:node_id", get(get_current_node_performance)) + .route( + "/historical-performance/:node_id", + get(get_historical_performance), + ) + .route( + "/performance-history/:node_id", + get(get_node_performance_history), + ) + // to make it compatible with all the explorers that were used to using 0-100 values + .route("/uptime-history/:node_id", get(get_node_uptime_history)) +} + +#[utoipa::path( + tag = "Nym Nodes", + get, + path = "/bonded", + context_path = "/v1/nym-nodes", + responses( + (status = 200, body = PaginatedResponse) + ), + params(PaginationRequest) +)] +async fn get_bonded_nodes( + State(state): State, + Query(pagination): Query, +) -> Json> { + // TODO: implement it + let _ = pagination; + + let details = state.nym_contract_cache().nym_nodes().await; + let total = details.len(); + + Json(PaginatedResponse { + pagination: Pagination { + total, + page: 0, + size: total, + }, + data: details, + }) +} + +#[utoipa::path( + tag = "Nym Nodes", + get, + path = "/described", + context_path = "/v1/nym-nodes", + responses( + (status = 200, body = PaginatedResponse) + ), + params(PaginationRequest) +)] +async fn get_described_nodes( + State(state): State, + Query(pagination): Query, +) -> AxumResult>> { + // TODO: implement it + let _ = pagination; + + let cache = state.described_nodes_cache.get().await?; + let descriptions = cache.all_nodes(); + + let data = descriptions + .map(|n| &n.description) + .cloned() + .collect::>(); + + Ok(Json(PaginatedResponse { + pagination: Pagination { + total: data.len(), + page: 0, + size: data.len(), + }, + data, + })) +} + +#[utoipa::path( + tag = "Nym Nodes", + get, + path = "/annotation/{node_id}", + context_path = "/v1/nym-nodes", + responses( + (status = 200, body = AnnotationResponse) + ), + params(NodeIdParam), +)] +async fn get_node_annotation( + Path(NodeIdParam { node_id }): Path, + State(state): State, +) -> AxumResult> { + let annotations = state + .node_status_cache + .node_annotations() + .await + .ok_or_else(AxumErrorResponse::internal)?; + + Ok(Json(AnnotationResponse { + node_id, + annotation: annotations.get(&node_id).cloned(), + })) +} + +#[utoipa::path( + tag = "Nym Nodes", + get, + path = "/performance/{node_id}", + context_path = "/v1/nym-nodes", + responses( + (status = 200, body = NodePerformanceResponse) + ), + params(NodeIdParam), +)] +async fn get_current_node_performance( + Path(NodeIdParam { node_id }): Path, + State(state): State, +) -> AxumResult> { + let annotations = state + .node_status_cache + .node_annotations() + .await + .ok_or_else(AxumErrorResponse::internal)?; + + Ok(Json(NodePerformanceResponse { + node_id, + performance: annotations + .get(&node_id) + .map(|n| n.last_24h_performance.naive_to_f64()), + })) +} + +// todo; probably extract it to requests crate +#[derive(Debug, Serialize, Deserialize, Copy, Clone, IntoParams, ToSchema, JsonSchema)] +#[into_params(parameter_in = Query)] +pub(crate) struct DateQuery { + #[schema(value_type = String, example = "1970-01-01")] + #[schemars(with = "String")] + pub(crate) date: Date, +} + +#[utoipa::path( + tag = "Nym Nodes", + get, + path = "/historical-performance/{node_id}", + context_path = "/v1/nym-nodes", + responses( + (status = 200, body = NodeDatePerformanceResponse) + ), + params(DateQuery, NodeIdParam) +)] +async fn get_historical_performance( + Path(NodeIdParam { node_id }): Path, + Query(DateQuery { date }): Query, + State(state): State, +) -> AxumResult> { + let uptime = state + .storage() + .get_historical_node_uptime_on(node_id, date) + .await?; + + Ok(Json(NodeDatePerformanceResponse { + node_id, + date, + performance: uptime.and_then(|u| { + Performance::from_percentage_value(u.uptime as u64) + .map(|p| p.naive_to_f64()) + .ok() + }), + })) +} + +#[utoipa::path( + tag = "Nym Nodes", + get, + path = "/performance-history/{node_id}", + context_path = "/v1/nym-nodes", + responses( + (status = 200, body = PerformanceHistoryResponse) + ), + params(PaginationRequest, NodeIdParam) +)] +async fn get_node_performance_history( + Path(NodeIdParam { node_id }): Path, + State(state): State, + Query(pagination): Query, +) -> AxumResult> { + // TODO: implement it + let _ = pagination; + + let history = state + .storage() + .get_node_uptime_history(node_id) + .await? + .into_iter() + .filter_map(|u| u.try_into().ok()) + .collect::>(); + let total = history.len(); + + Ok(Json(PerformanceHistoryResponse { + node_id, + history: PaginatedResponse { + pagination: Pagination { + total, + page: 0, + size: total, + }, + data: history, + }, + })) +} + +#[utoipa::path( + tag = "Nym Nodes", + get, + path = "/uptime-history/{node_id}", + context_path = "/v1/nym-nodes", + responses( + (status = 200, body = PerformanceHistoryResponse) + ), + params(PaginationRequest, NodeIdParam) +)] +async fn get_node_uptime_history( + Path(NodeIdParam { node_id }): Path, + State(state): State, + Query(pagination): Query, +) -> AxumResult> { + // TODO: implement it + let _ = pagination; + + let history = state + .storage() + .get_node_uptime_history(node_id) + .await? + .into_iter() + .filter_map(|u| u.try_into().ok()) + .collect::>(); + let total = history.len(); + + Ok(Json(UptimeHistoryResponse { + node_id, + history: PaginatedResponse { + pagination: Pagination { + total, + page: 0, + size: total, + }, + data: history, + }, + })) +} diff --git a/nym-api/src/nym_nodes/handlers_unstable.rs b/nym-api/src/nym_nodes/handlers/unstable.rs similarity index 52% rename from nym-api/src/nym_nodes/handlers_unstable.rs rename to nym-api/src/nym_nodes/handlers/unstable.rs index f0a7d8cc8b..2fab1eef94 100644 --- a/nym-api/src/nym_nodes/handlers_unstable.rs +++ b/nym-api/src/nym_nodes/handlers/unstable.rs @@ -21,19 +21,18 @@ //! - `/gateway/` => only returns (entry) gateway role data use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; -use crate::v2::AxumAppState; +use crate::support::http::state::AppState; use axum::extract::Query; use axum::extract::State; use axum::{Json, Router}; use nym_api_requests::nym_nodes::{ - CachedNodesResponse, FullFatNode, NodeRoleQueryParam, SemiSkimmedNode, SkimmedNode, + CachedNodesResponse, FullFatNode, NodeRole, NodeRoleQueryParam, SemiSkimmedNode, SkimmedNode, }; use nym_bin_common::version_checker; use serde::Deserialize; -use std::cmp::min; -use std::ops::Deref; +use std::collections::HashSet; -pub(crate) fn nym_node_routes_unstable() -> axum::Router { +pub(crate) fn nym_node_routes_unstable() -> axum::Router { Router::new() .route("/skimmed", axum::routing::get(nodes_basic)) .route("/semi-skimmed", axum::routing::get(nodes_expanded)) @@ -84,7 +83,7 @@ impl SemverCompatibilityQueryParam { ) )] async fn nodes_basic( - state: State, + state: State, Query(NodesParams { role, semver_compatibility, @@ -123,7 +122,7 @@ async fn nodes_basic( ) )] async fn nodes_expanded( - state: State, + state: State, Query(NodesParams { role, semver_compatibility, @@ -162,7 +161,7 @@ async fn nodes_expanded( ) )] async fn nodes_detailed( - state: State, + state: State, Query(NodesParams { role, semver_compatibility, @@ -201,61 +200,113 @@ async fn nodes_detailed( ) )] async fn gateways_basic( - state: State, + state: State, Query(SemverCompatibilityQueryParam { semver_compatibility, }): Query, ) -> AxumResult>> { let status_cache = state.node_status_cache(); - let describe_cache = state.described_nodes_state(); - let gateways_cache = - status_cache - .gateways_cache() - .await - .ok_or(AxumErrorResponse::internal_msg( - "could not obtain gateways cache", - ))?; + let contract_cache = state.nym_contract_cache(); + let describe_cache = state.described_nodes_cache(); - if gateways_cache.is_empty() { - return Ok(Json(CachedNodesResponse { - refreshed_at: gateways_cache.timestamp().into(), - nodes: vec![], - })); + // 1. get the rewarded set + let rewarded_set = contract_cache + .rewarded_set() + .await + .ok_or_else(AxumErrorResponse::internal)?; + + // determine which gateways are active, i.e. which gateways the clients should be using for connecting and routing the traffic + let active_gateways = rewarded_set.gateways().into_iter().collect::>(); + + // 2. grab all annotations so that we could attach scores to the [nym] nodes + let annotations = status_cache + .node_annotations() + .await + .ok_or_else(AxumErrorResponse::internal)?; + + // 3. grab all legacy gateways + // due to legacy endpoints we already have fully annotated data on them + let annotated_legacy_gateways = status_cache + .annotated_legacy_gateways() + .await + .ok_or_else(AxumErrorResponse::internal)?; + + // 4. grab all relevant described nym-nodes + let describe_cache = describe_cache.get().await?; + let gateway_capable_nym_nodes = describe_cache.gateway_capable_nym_nodes(); + + // 5. only return nodes that are present in the active set + let mut active_skimmed_gateways = Vec::new(); + + for (node_id, legacy) in annotated_legacy_gateways.iter() { + if !active_gateways.contains(node_id) { + continue; + } + + if let Some(semver_compat) = semver_compatibility.as_ref() { + let version = legacy.version(); + if !version_checker::is_minor_version_compatible(version, semver_compat) { + continue; + } + } + + // if we have self-described info, prefer it over contract data + if let Some(described) = describe_cache.get_node(node_id) { + active_skimmed_gateways.push( + described.to_skimmed_node(NodeRole::EntryGateway, legacy.node_performance.last_24h), + ) + } else { + active_skimmed_gateways.push(legacy.into()); + } } - // if the self describe cache is unavailable don't try to use self-describe data - let Ok(self_descriptions) = describe_cache.get().await else { - return Ok(Json(CachedNodesResponse { - refreshed_at: gateways_cache.timestamp().into(), - nodes: gateways_cache.values().map(Into::into).collect(), - })); - }; + for nym_node in gateway_capable_nym_nodes { + // if this node is not an active gateway, ignore it + if !active_gateways.contains(&nym_node.node_id) { + continue; + } - let refreshed_at = min(gateways_cache.timestamp(), self_descriptions.timestamp()); + // if we have wrong version, ignore + if let Some(semver_compat) = semver_compatibility.as_ref() { + let version = nym_node.version(); + if !version_checker::is_minor_version_compatible(version, semver_compat) { + continue; + } + } + + // NOTE: if we determined our node IS an active gateway, it MUST be EITHER entry or exit + let role = if rewarded_set.is_exit(&nym_node.node_id) { + NodeRole::ExitGateway + } else { + NodeRole::EntryGateway + }; + + // honestly, not sure under what exact circumstances this value could be missing, + // but in that case just use 0 performance + let annotation = annotations + .get(&nym_node.node_id) + .copied() + .unwrap_or_default(); + + active_skimmed_gateways + .push(nym_node.to_skimmed_node(role, annotation.last_24h_performance)); + } + + // min of all caches + let refreshed_at = [ + rewarded_set.timestamp(), + annotations.timestamp(), + describe_cache.timestamp(), + annotated_legacy_gateways.timestamp(), + ] + .into_iter() + .min() + .unwrap() + .into(); - // the same comment holds as with `get_gateways_described`. - // this is inefficient and will have to get refactored with directory v3 Ok(Json(CachedNodesResponse { - refreshed_at: refreshed_at.into(), - nodes: gateways_cache - .values() - .filter(|annotated_bond| { - if let Some(semver_compatibility) = semver_compatibility.as_ref() { - version_checker::is_minor_version_compatible( - &annotated_bond.gateway_bond.gateway.version, - semver_compatibility, - ) - } else { - true - } - }) - .map(|annotated_bond| { - SkimmedNode::from_described_gateway( - annotated_bond, - self_descriptions.deref().get(annotated_bond.identity()), - ) - }) - .collect(), + refreshed_at, + nodes: active_skimmed_gateways, })) } @@ -269,7 +320,7 @@ async fn gateways_basic( ) )] async fn gateways_expanded( - State(_state): State, + State(_state): State, Query(SemverCompatibilityQueryParam { semver_compatibility: _semver_compatibility, }): Query, @@ -287,7 +338,7 @@ async fn gateways_expanded( ) )] async fn gateways_detailed( - State(_state): State, + State(_state): State, Query(SemverCompatibilityQueryParam { semver_compatibility: _semver_compatibility, }): Query, @@ -305,38 +356,120 @@ async fn gateways_detailed( ) )] async fn mixnodes_basic( - state: State, + state: State, Query(SemverCompatibilityQueryParam { semver_compatibility, }): Query, ) -> AxumResult>> { - let mixnodes_cache = state - .node_status_cache() - .active_mixnodes_cache() + let status_cache = state.node_status_cache(); + let contract_cache = state.nym_contract_cache(); + let describe_cache = state.described_nodes_cache(); + + // 1. get the rewarded set + let rewarded_set = contract_cache + .rewarded_set() .await - .ok_or(AxumErrorResponse::internal_msg( - "could not obtain mixnodes cache", - ))?; + .ok_or_else(AxumErrorResponse::internal)?; + + // determine which mixnodes are active, i.e. which mixnodes the clients should be using for routing the traffic + let active_mixnodes = rewarded_set + .active_mixnodes() + .into_iter() + .collect::>(); + + // 2. grab all annotations so that we could attach scores to the [nym] nodes + let annotations = status_cache + .node_annotations() + .await + .ok_or_else(AxumErrorResponse::internal)?; + + // 3. grab all legacy mixnodes + // due to legacy endpoints we already have fully annotated data on them + let annotated_legacy_mixnodes = status_cache + .annotated_legacy_mixnodes() + .await + .ok_or_else(AxumErrorResponse::internal)?; + + // 4. grab all relevant described nym-nodes + let describe_cache = describe_cache.get().await?; + let mixing_nym_nodes = describe_cache.mixing_nym_nodes(); + + // TODO: in the future, only use the self-described cache and simply reject mixnodes that did not expose it + + // 5. only return nodes that are present in the active set + let mut active_skimmed_mixnodes = Vec::new(); + + for (mix_id, legacy) in annotated_legacy_mixnodes.iter() { + if !active_mixnodes.contains(mix_id) { + continue; + } + + if let Some(semver_compat) = semver_compatibility.as_ref() { + let version = legacy.version(); + if !version_checker::is_minor_version_compatible(version, semver_compat) { + continue; + } + } + + // if we have self-described info, prefer it over contract data + if let Some(described) = describe_cache.get_node(mix_id) { + active_skimmed_mixnodes.push(described.to_skimmed_node( + NodeRole::Mixnode { + layer: legacy.mixnode_details.bond_information.layer.into(), + }, + legacy.node_performance.last_24h, + )) + } else { + active_skimmed_mixnodes.push(legacy.into()); + } + } + + for nym_node in mixing_nym_nodes { + // if this node is not an active mixnode, ignore it + if !active_mixnodes.contains(&nym_node.node_id) { + continue; + } + + // if we have wrong version, ignore + if let Some(semver_compat) = semver_compatibility.as_ref() { + let version = nym_node.version(); + if !version_checker::is_minor_version_compatible(version, semver_compat) { + continue; + } + } + + // SAFETY: if we determined our node IS active, it MUST have a layer + // no other thread could have updated the rewarded set as we're still holding the [read] lock on the data + #[allow(clippy::unwrap_used)] + let layer = rewarded_set.try_get_mix_layer(&nym_node.node_id).unwrap(); + + // honestly, not sure under what exact circumstances this value could be missing, + // but in that case just use 0 performance + let annotation = annotations + .get(&nym_node.node_id) + .copied() + .unwrap_or_default(); + + active_skimmed_mixnodes.push( + nym_node.to_skimmed_node(NodeRole::Mixnode { layer }, annotation.last_24h_performance), + ); + } + + // min of all caches + let refreshed_at = [ + rewarded_set.timestamp(), + annotations.timestamp(), + describe_cache.timestamp(), + annotated_legacy_mixnodes.timestamp(), + ] + .into_iter() + .min() + .unwrap() + .into(); + Ok(Json(CachedNodesResponse { - refreshed_at: mixnodes_cache.timestamp().into(), - nodes: mixnodes_cache - .iter() - .filter(|annotated_bond| { - if let Some(semver_compatibility) = semver_compatibility.as_ref() { - version_checker::is_minor_version_compatible( - &annotated_bond - .mixnode_details - .bond_information - .mix_node - .version, - semver_compatibility, - ) - } else { - true - } - }) - .map(Into::into) - .collect(), + refreshed_at, + nodes: active_skimmed_mixnodes, })) } @@ -350,7 +483,7 @@ async fn mixnodes_basic( ) )] async fn mixnodes_expanded( - State(_state): State, + State(_state): State, Query(SemverCompatibilityQueryParam { semver_compatibility: _semver_compatibility, }): Query, @@ -368,7 +501,7 @@ async fn mixnodes_expanded( ) )] async fn mixnodes_detailed( - State(_state): State, + State(_state): State, Query(SemverCompatibilityQueryParam { semver_compatibility: _semver_compatibility, }): Query, diff --git a/nym-api/src/nym_nodes/mod.rs b/nym-api/src/nym_nodes/mod.rs index d7d5b98a7e..33a7ff2fd8 100644 --- a/nym-api/src/nym_nodes/mod.rs +++ b/nym-api/src/nym_nodes/mod.rs @@ -1,37 +1,35 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use okapi::openapi3::OpenApi; -use rocket::Route; -use rocket_okapi::openapi_get_routes_spec; -use rocket_okapi::settings::OpenApiSettings; - -#[cfg(feature = "axum")] pub(crate) mod handlers; -#[cfg(feature = "axum")] -pub(crate) mod handlers_unstable; -pub(crate) mod routes; -mod unstable_routes; +// pub(crate) mod routes; +// mod unstable_routes; -/// Merges the routes with http information and returns it to Rocket for serving -pub(crate) fn nym_node_routes_deprecated(settings: &OpenApiSettings) -> (Vec, OpenApi) { - openapi_get_routes_spec![ - settings: routes::get_gateways_described, routes::get_mixnodes_described - ] -} - -pub(crate) fn nym_node_routes_next(settings: &OpenApiSettings) -> (Vec, OpenApi) { - openapi_get_routes_spec![ - settings: - unstable_routes::nodes_basic, - unstable_routes::nodes_expanded, - unstable_routes::nodes_detailed, - unstable_routes::gateways_basic, - unstable_routes::gateways_expanded, - unstable_routes::gateways_detailed, - unstable_routes::mixnodes_basic, - unstable_routes::mixnodes_expanded, - unstable_routes::mixnodes_detailed, - ] -} +// /// Merges the routes with http information and returns it to Rocket for serving +// pub(crate) fn nym_node_routes_deprecated(settings: &OpenApiSettings) -> (Vec, OpenApi) { +// openapi_get_routes_spec![ +// settings: +// routes::get_gateways_described, +// routes::get_mixnodes_described, +// ] +// } +// +// pub(crate) fn nym_node_routes_next(settings: &OpenApiSettings) -> (Vec, OpenApi) { +// openapi_get_routes_spec![ +// settings: +// unstable_routes::nodes_basic, +// unstable_routes::nodes_expanded, +// unstable_routes::nodes_detailed, +// unstable_routes::gateways_basic, +// unstable_routes::gateways_expanded, +// unstable_routes::gateways_detailed, +// unstable_routes::mixnodes_basic, +// unstable_routes::mixnodes_expanded, +// unstable_routes::mixnodes_detailed, +// routes::all_described_nodes, +// routes::node_description, +// routes::node_annotation_by_identity, +// routes::node_annotation +// ] +// } diff --git a/nym-api/src/nym_nodes/routes.rs b/nym-api/src/nym_nodes/routes.rs index 061cc8cf9c..f5574418cc 100644 --- a/nym-api/src/nym_nodes/routes.rs +++ b/nym-api/src/nym_nodes/routes.rs @@ -2,60 +2,115 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::node_describe_cache::DescribedNodes; +use crate::node_status_api::NodeStatusCache; use crate::nym_contract_cache::cache::NymContractCache; use crate::support::caching::cache::SharedCache; -use nym_api_requests::models::{DescribedGateway, DescribedMixNode}; -use nym_mixnet_contract_common::MixNodeBond; +use nym_api_requests::models::{ + AnnotationResponse, LegacyDescribedGateway, LegacyDescribedMixNode, NymNodeDescription, +}; +use nym_mixnet_contract_common::NodeId; use rocket::serde::json::Json; use rocket::State; use rocket_okapi::openapi; use std::ops::Deref; -// obviously this should get refactored later on because gateways will go away. -// unless maybe this will be filtering based on which nodes got assigned gateway role? TBD +#[openapi(tag = "Nym Nodes")] +#[get("/all/described")] +pub async fn all_described_nodes( + describe_cache: &State>, +) -> Json> { + let Ok(self_descriptions) = describe_cache.get().await else { + return Json(Vec::new()); + }; + + Json(self_descriptions.all_nodes().cloned().collect()) +} #[openapi(tag = "Nym Nodes")] +#[get("/all//described")] +pub async fn node_description( + node_id: NodeId, + describe_cache: &State>, +) -> Json> { + let Ok(self_descriptions) = describe_cache.get().await else { + return Json(None); + }; + + Json(self_descriptions.get_node(&node_id).cloned()) +} + +#[openapi(tag = "Nym Nodes")] +#[get("/annotation-by-identity/")] +pub async fn node_annotation_by_identity( + identity: String, + status_cache: &State, +) -> Json { + let Some(node_id) = status_cache.map_identity_to_node_id(&identity).await else { + return Json(Default::default()); + }; + node_annotation(node_id, status_cache).await +} + +#[openapi(tag = "Nym Nodes")] +#[get("/annotation/")] +pub async fn node_annotation( + node_id: NodeId, + status_cache: &State, +) -> Json { + let Some(annotation) = status_cache.node_annotations().await else { + return Json(Default::default()); + }; + + Json(AnnotationResponse { + node_id: Some(node_id), + annotation: annotation.get(&node_id).cloned(), + }) +} + +/// This only returns descriptions of **legacy** gateways +#[openapi(tag = "Nym Nodes", deprecated = true)] #[get("/gateways/described")] pub async fn get_gateways_described( contract_cache: &State, describe_cache: &State>, -) -> Json> { - let gateways = contract_cache.gateways_filtered().await; +) -> Json> { + let gateways = contract_cache.legacy_gateways_filtered().await; if gateways.is_empty() { return Json(Vec::new()); } - // if the self describe cache is unavailable, well, don't attach describe data + // if the self describe cache is unavailable, well, don't attach describe data and only return legacy gateways let Ok(self_descriptions) = describe_cache.get().await else { return Json(gateways.into_iter().map(Into::into).collect()); }; - // TODO: this is extremely inefficient, but given we don't have many gateways, - // it shouldn't be too much of a problem until we go ahead with directory v3 / the smoosh 2: electric smoosharoo, - // but at that point (I hope) the whole caching situation should get refactored Json( gateways .into_iter() - .map(|bond| DescribedGateway { - self_described: self_descriptions.deref().get(bond.identity()).cloned(), + .map(|bond| LegacyDescribedGateway { + self_described: self_descriptions + .deref() + .get_description(&bond.node_id) + .cloned(), bond, }) .collect(), ) } -#[openapi(tag = "Nym Nodes")] +/// This only returns descriptions of **legacy** mixnodes +#[openapi(tag = "Nym Nodes", deprecated = true)] #[get("/mixnodes/described")] pub async fn get_mixnodes_described( contract_cache: &State, describe_cache: &State>, -) -> Json> { +) -> Json> { let mixnodes = contract_cache - .mixnodes_filtered() + .legacy_mixnodes_filtered() .await .into_iter() .map(|m| m.bond_information) - .collect::>(); + .collect::>(); if mixnodes.is_empty() { return Json(Vec::new()); } @@ -65,14 +120,14 @@ pub async fn get_mixnodes_described( return Json(mixnodes.into_iter().map(Into::into).collect()); }; - // TODO: this is extremely inefficient, but given we don't have many gateways, - // it shouldn't be too much of a problem until we go ahead with directory v3 / the smoosh 2: electric smoosharoo, - // but at that point (I hope) the whole caching situation should get refactored Json( mixnodes .into_iter() - .map(|bond| DescribedMixNode { - self_described: self_descriptions.deref().get(bond.identity()).cloned(), + .map(|bond| LegacyDescribedMixNode { + self_described: self_descriptions + .deref() + .get_description(&bond.mix_id) + .cloned(), bond, }) .collect(), diff --git a/nym-api/src/nym_nodes/unstable_routes.rs b/nym-api/src/nym_nodes/unstable_routes.rs index aa12e5b7f3..0e178da389 100644 --- a/nym-api/src/nym_nodes/unstable_routes.rs +++ b/nym-api/src/nym_nodes/unstable_routes.rs @@ -4,17 +4,17 @@ use crate::node_describe_cache::DescribedNodes; use crate::node_status_api::models::RocketErrorResponse; use crate::node_status_api::NodeStatusCache; +use crate::nym_contract_cache::cache::NymContractCache; use crate::support::caching::cache::SharedCache; use nym_api_requests::nym_nodes::{ - CachedNodesResponse, FullFatNode, NodeRoleQueryParam, SemiSkimmedNode, SkimmedNode, + CachedNodesResponse, FullFatNode, NodeRole, NodeRoleQueryParam, SemiSkimmedNode, SkimmedNode, }; use nym_bin_common::version_checker; use rocket::http::Status; use rocket::serde::json::Json; use rocket::State; use rocket_okapi::openapi; -use std::cmp::min; -use std::ops::Deref; +use std::collections::HashSet; /* routes: @@ -36,6 +36,7 @@ use std::ops::Deref; #[get("/skimmed?&")] pub async fn nodes_basic( status_cache: &State, + contract_cache: &State, describe_cache: &State>, role: Option, semver_compatibility: Option, @@ -43,10 +44,22 @@ pub async fn nodes_basic( if let Some(role) = role { match role { NodeRoleQueryParam::ActiveMixnode => { - return mixnodes_basic(status_cache, semver_compatibility).await + return mixnodes_basic( + status_cache, + contract_cache, + describe_cache, + semver_compatibility, + ) + .await } NodeRoleQueryParam::EntryGateway => { - return gateways_basic(status_cache, describe_cache, semver_compatibility).await + return gateways_basic( + status_cache, + contract_cache, + describe_cache, + semver_compatibility, + ) + .await } _ => {} } @@ -112,57 +125,108 @@ pub async fn nodes_detailed( #[get("/gateways/skimmed?")] pub async fn gateways_basic( status_cache: &State, + contract_cache: &State, describe_cache: &State>, semver_compatibility: Option, ) -> Result>, RocketErrorResponse> { - let gateways_cache = status_cache - .gateways_cache() + // 1. get the rewarded set + let rewarded_set = contract_cache + .rewarded_set() .await - .ok_or(RocketErrorResponse::new( - "could not obtain gateways cache", - Status::InternalServerError, - ))?; + .ok_or_else(RocketErrorResponse::internal_server_error)?; - if gateways_cache.is_empty() { - return Ok(Json(CachedNodesResponse { - refreshed_at: gateways_cache.timestamp().into(), - nodes: vec![], - })); + // determine which gateways are active, i.e. which gateways the clients should be using for connecting and routing the traffic + let active_gateways = rewarded_set.gateways().into_iter().collect::>(); + + // 2. grab all annotations so that we could attach scores to the [nym] nodes + let annotations = status_cache + .node_annotations() + .await + .ok_or_else(RocketErrorResponse::internal_server_error)?; + + // 3. grab all legacy gateways + // due to legacy endpoints we already have fully annotated data on them + let annotated_legacy_gateways = status_cache + .annotated_legacy_gateways() + .await + .ok_or_else(RocketErrorResponse::internal_server_error)?; + + // 4. grab all relevant described nym-nodes + let describe_cache = describe_cache.get().await?; + let gateway_capable_nym_nodes = describe_cache.gateway_capable_nym_nodes(); + + // 5. only return nodes that are present in the active set + let mut active_skimmed_gateways = Vec::new(); + + for (node_id, legacy) in annotated_legacy_gateways.iter() { + if !active_gateways.contains(node_id) { + continue; + } + + if let Some(semver_compat) = semver_compatibility.as_ref() { + let version = legacy.version(); + if !version_checker::is_minor_version_compatible(version, semver_compat) { + continue; + } + } + + // if we have self-described info, prefer it over contract data + if let Some(described) = describe_cache.get_node(node_id) { + active_skimmed_gateways.push( + described.to_skimmed_node(NodeRole::EntryGateway, legacy.node_performance.last_24h), + ) + } else { + active_skimmed_gateways.push(legacy.into()); + } } - // if the self describe cache is unavailable don't try to use self-describe data - let Ok(self_descriptions) = describe_cache.get().await else { - return Ok(Json(CachedNodesResponse { - refreshed_at: gateways_cache.timestamp().into(), - nodes: gateways_cache.values().map(Into::into).collect(), - })); - }; + for nym_node in gateway_capable_nym_nodes { + // if this node is not an active gateway, ignore it + if !active_gateways.contains(&nym_node.node_id) { + continue; + } - let refreshed_at = min(gateways_cache.timestamp(), self_descriptions.timestamp()); + // if we have wrong version, ignore + if let Some(semver_compat) = semver_compatibility.as_ref() { + let version = nym_node.version(); + if !version_checker::is_minor_version_compatible(version, semver_compat) { + continue; + } + } + + // NOTE: if we determined our node IS an active gateway, it MUST be EITHER entry or exit + let role = if rewarded_set.is_exit(&nym_node.node_id) { + NodeRole::ExitGateway + } else { + NodeRole::EntryGateway + }; + + // honestly, not sure under what exact circumstances this value could be missing, + // but in that case just use 0 performance + let annotation = annotations + .get(&nym_node.node_id) + .copied() + .unwrap_or_default(); + + active_skimmed_gateways + .push(nym_node.to_skimmed_node(role, annotation.last_24h_performance)); + } + + // min of all caches + let refreshed_at = [ + rewarded_set.timestamp(), + annotations.timestamp(), + describe_cache.timestamp(), + annotated_legacy_gateways.timestamp(), + ] + .into_iter() + .min() + .unwrap() + .into(); - // the same comment holds as with `get_gateways_described`. - // this is inefficient and will have to get refactored with directory v3 Ok(Json(CachedNodesResponse { - refreshed_at: refreshed_at.into(), - nodes: gateways_cache - .values() - .filter(|annotated_bond| { - if let Some(semver_compatibility) = semver_compatibility.as_ref() { - version_checker::is_minor_version_compatible( - &annotated_bond.gateway_bond.gateway.version, - semver_compatibility, - ) - } else { - true - } - }) - .map(|annotated_bond| { - SkimmedNode::from_described_gateway( - annotated_bond, - self_descriptions.deref().get(annotated_bond.identity()), - ) - }) - .collect(), + refreshed_at, + nodes: active_skimmed_gateways, })) } @@ -197,36 +261,116 @@ pub async fn gateways_detailed( #[openapi(tag = "Unstable Nym Nodes")] #[get("/mixnodes/skimmed?")] pub async fn mixnodes_basic( - cache: &State, + status_cache: &State, + contract_cache: &State, + describe_cache: &State>, semver_compatibility: Option, ) -> Result>, RocketErrorResponse> { - let mixnodes_cache = cache - .active_mixnodes_cache() + // 1. get the rewarded set + let rewarded_set = contract_cache + .rewarded_set() .await - .ok_or(RocketErrorResponse::new( - "could not obtain mixnodes cache", - Status::InternalServerError, - ))?; + .ok_or_else(RocketErrorResponse::internal_server_error)?; + + // determine which mixnodes are active, i.e. which mixnodes the clients should be using for routing the traffic + let active_mixnodes = rewarded_set + .active_mixnodes() + .into_iter() + .collect::>(); + + // 2. grab all annotations so that we could attach scores to the [nym] nodes + let annotations = status_cache + .node_annotations() + .await + .ok_or_else(RocketErrorResponse::internal_server_error)?; + + // 3. grab all legacy mixnodes + // due to legacy endpoints we already have fully annotated data on them + let annotated_legacy_mixnodes = status_cache + .annotated_legacy_mixnodes() + .await + .ok_or_else(RocketErrorResponse::internal_server_error)?; + + // 4. grab all relevant described nym-nodes + let describe_cache = describe_cache.get().await?; + let mixing_nym_nodes = describe_cache.mixing_nym_nodes(); + + // TODO: in the future, only use the self-described cache and simply reject mixnodes that did not expose it + + // 5. only return nodes that are present in the active set + let mut active_skimmed_mixnodes = Vec::new(); + + for (mix_id, legacy) in annotated_legacy_mixnodes.iter() { + if !active_mixnodes.contains(mix_id) { + continue; + } + + if let Some(semver_compat) = semver_compatibility.as_ref() { + let version = legacy.version(); + if !version_checker::is_minor_version_compatible(version, semver_compat) { + continue; + } + } + + // if we have self-described info, prefer it over contract data + if let Some(described) = describe_cache.get_node(mix_id) { + active_skimmed_mixnodes.push(described.to_skimmed_node( + NodeRole::Mixnode { + layer: legacy.mixnode_details.bond_information.layer.into(), + }, + legacy.node_performance.last_24h, + )) + } else { + active_skimmed_mixnodes.push(legacy.into()); + } + } + + for nym_node in mixing_nym_nodes { + // if this node is not an active mixnode, ignore it + if !active_mixnodes.contains(&nym_node.node_id) { + continue; + } + + // if we have wrong version, ignore + if let Some(semver_compat) = semver_compatibility.as_ref() { + let version = nym_node.version(); + if !version_checker::is_minor_version_compatible(version, semver_compat) { + continue; + } + } + + // SAFETY: if we determined our node IS active, it MUST have a layer + // no other thread could have updated the rewarded set as we're still holding the [read] lock on the data + #[allow(clippy::unwrap_used)] + let layer = rewarded_set.try_get_mix_layer(&nym_node.node_id).unwrap(); + + // honestly, not sure under what exact circumstances this value could be missing, + // but in that case just use 0 performance + let annotation = annotations + .get(&nym_node.node_id) + .copied() + .unwrap_or_default(); + + active_skimmed_mixnodes.push( + nym_node.to_skimmed_node(NodeRole::Mixnode { layer }, annotation.last_24h_performance), + ); + } + + // min of all caches + let refreshed_at = [ + rewarded_set.timestamp(), + annotations.timestamp(), + describe_cache.timestamp(), + annotated_legacy_mixnodes.timestamp(), + ] + .into_iter() + .min() + .unwrap() + .into(); + Ok(Json(CachedNodesResponse { - refreshed_at: mixnodes_cache.timestamp().into(), - nodes: mixnodes_cache - .iter() - .filter(|annotated_bond| { - if let Some(semver_compatibility) = semver_compatibility.as_ref() { - version_checker::is_minor_version_compatible( - &annotated_bond - .mixnode_details - .bond_information - .mix_node - .version, - semver_compatibility, - ) - } else { - true - } - }) - .map(Into::into) - .collect(), + refreshed_at, + nodes: active_skimmed_mixnodes, })) } diff --git a/nym-api/src/status/handlers.rs b/nym-api/src/status/handlers.rs index ea34727f9a..7dc316aac2 100644 --- a/nym-api/src/status/handlers.rs +++ b/nym-api/src/status/handlers.rs @@ -3,7 +3,7 @@ use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; use crate::status::ApiStatusState; -use crate::v2::AxumAppState; +use crate::support::http::state::AppState; use axum::Json; use axum::Router; use nym_api_requests::models::{ApiHealthResponse, SignerInformationResponse}; @@ -11,7 +11,7 @@ use nym_bin_common::build_information::BinaryBuildInformationOwned; use nym_compact_ecash::Base58; use std::sync::Arc; -pub(crate) fn api_status_routes() -> Router { +pub(crate) fn api_status_routes() -> Router { let api_status_state = Arc::new(ApiStatusState::new()); Router::new() @@ -84,7 +84,7 @@ async fn signer_information( identity: signer_state.identity.clone(), announce_address: signer_state.announce_address.clone(), verification_key: signer_state - .coconut_keypair + .ecash_keypair .verification_key() .await .map(|maybe_vk| maybe_vk.to_bs58()), diff --git a/nym-api/src/status/mod.rs b/nym-api/src/status/mod.rs index 1d09b341c8..5e74eeffb6 100644 --- a/nym-api/src/status/mod.rs +++ b/nym-api/src/status/mod.rs @@ -4,15 +4,11 @@ use crate::ecash; use nym_bin_common::bin_info; use nym_bin_common::build_information::BinaryBuildInformation; -use okapi::openapi3::OpenApi; -use rocket::Route; -use rocket_okapi::openapi_get_routes_spec; -use rocket_okapi::settings::OpenApiSettings; + use tokio::time::Instant; -#[cfg(feature = "axum")] pub(crate) mod handlers; -pub(crate) mod routes; +// pub(crate) mod routes; pub(crate) struct ApiStatusState { startup_time: Instant, @@ -28,7 +24,7 @@ pub(crate) struct SignerState { pub announce_address: String, - pub(crate) coconut_keypair: ecash::keys::KeyPair, + pub(crate) ecash_keypair: ecash::keys::KeyPair, } impl ApiStatusState { @@ -45,11 +41,11 @@ impl ApiStatusState { } } -pub(crate) fn api_status_routes(settings: &OpenApiSettings) -> (Vec, OpenApi) { - openapi_get_routes_spec![ - settings: - routes::health, - routes::build_information, - routes::signer_information - ] -} +// pub(crate) fn api_status_routes(settings: &OpenApiSettings) -> (Vec, OpenApi) { +// openapi_get_routes_spec![ +// settings: +// routes::health, +// routes::build_information, +// routes::signer_information +// ] +// } diff --git a/nym-api/src/status/routes.rs b/nym-api/src/status/routes.rs index 804744df18..911e9e2a9b 100644 --- a/nym-api/src/status/routes.rs +++ b/nym-api/src/status/routes.rs @@ -44,7 +44,7 @@ pub(crate) async fn signer_information( identity: signer_state.identity.clone(), announce_address: signer_state.announce_address.clone(), verification_key: signer_state - .coconut_keypair + .ecash_keypair .verification_key() .await .map(|maybe_vk| maybe_vk.to_bs58()), diff --git a/nym-api/src/support/caching/cache.rs b/nym-api/src/support/caching/cache.rs index 4df805728a..706d2b342b 100644 --- a/nym-api/src/support/caching/cache.rs +++ b/nym-api/src/support/caching/cache.rs @@ -31,12 +31,12 @@ impl SharedCache { SharedCache::default() } - pub(crate) async fn update(&self, value: T) { + pub(crate) async fn update(&self, value: impl Into) { let mut guard = self.0.write().await; if let Some(ref mut existing) = guard.inner { existing.unchecked_update(value) } else { - guard.inner = Some(Cache::new(value)) + guard.inner = Some(Cache::new(value.into())) } } @@ -52,6 +52,17 @@ impl SharedCache { ) -> Result, UninitialisedCache> { Ok(RwLockReadGuard::map(self.get().await?, |a| &a.value)) } + + pub(crate) async fn naive_wait_for_initial_values(&self) { + let initialisation_backoff = Duration::from_secs(5); + loop { + if self.get().await.is_ok() { + break; + } else { + tokio::time::sleep(initialisation_backoff).await; + } + } + } } impl From> for SharedCache { @@ -118,8 +129,8 @@ impl Cache { } // ugh. I hate to expose it, but it'd have broken pre-existing code - pub(crate) fn unchecked_update(&mut self, value: T) { - self.value = value; + pub(crate) fn unchecked_update(&mut self, value: impl Into) { + self.value = value.into(); self.as_at = OffsetDateTime::now_utc() } @@ -135,7 +146,6 @@ impl Cache { self.as_at } - #[allow(dead_code)] pub fn into_inner(self) -> T { self.value } diff --git a/nym-api/src/support/caching/refresher.rs b/nym-api/src/support/caching/refresher.rs index 6d32bb4f5a..b301220a0e 100644 --- a/nym-api/src/support/caching/refresher.rs +++ b/nym-api/src/support/caching/refresher.rs @@ -2,9 +2,11 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::support::caching::cache::SharedCache; +use async_trait::async_trait; use nym_task::TaskClient; use std::time::Duration; use tokio::time::interval; +use tracing::{error, info, trace}; pub struct CacheRefresher { name: String, @@ -88,6 +90,8 @@ where self.shared_cache.clone() } + // TODO: in the future offer 2 options of refreshing cache. either provide `T` directly + // or via `FnMut(&mut T)` closure async fn do_refresh_cache(&self) { match self.provider.try_refresh().await { Ok(updated_items) => { @@ -100,12 +104,12 @@ where } pub async fn refresh(&self, task_client: &mut TaskClient) { - log::info!("{}: refreshing cache state", self.name); + info!("{}: refreshing cache state", self.name); tokio::select! { biased; _ = task_client.recv() => { - log::trace!("{}: Received shutdown while refreshing cache", self.name) + trace!("{}: Received shutdown while refreshing cache", self.name) } _ = self.do_refresh_cache() => (), } @@ -119,7 +123,7 @@ where tokio::select! { biased; _ = task_client.recv() => { - log::trace!("{}: Received shutdown", self.name) + trace!("{}: Received shutdown", self.name) } _ = refresh_interval.tick() => self.refresh(&mut task_client).await, } diff --git a/nym-api/src/support/cli/run.rs b/nym-api/src/support/cli/run.rs index a19d2c1c29..08e8d49f94 100644 --- a/nym-api/src/support/cli/run.rs +++ b/nym-api/src/support/cli/run.rs @@ -1,9 +1,43 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::start_nym_api_tasks; +use crate::circulating_supply_api::cache::CirculatingSupplyCache; +use crate::ecash::api_routes::handlers::ecash_routes; +use crate::ecash::client::Client; +use crate::ecash::comm::QueryCommunicationChannel; +use crate::ecash::dkg::controller::keys::{ + can_validate_coconut_keys, load_bte_keypair, load_ecash_keypair_if_exists, +}; +use crate::ecash::dkg::controller::DkgController; +use crate::ecash::state::EcashState; +use crate::epoch_operations::EpochAdvancer; +use crate::network::models::NetworkDetails; +use crate::node_describe_cache::DescribedNodes; +use crate::node_status_api::handlers::unstable; +use crate::node_status_api::uptime_updater::HistoricalUptimeUpdater; +use crate::node_status_api::NodeStatusCache; +use crate::nym_contract_cache::cache::NymContractCache; +use crate::status::{ApiStatusState, SignerState}; +use crate::support::caching::cache::SharedCache; use crate::support::config::helpers::try_load_current_config; -use cfg_if::cfg_if; +use crate::support::config::Config; +use crate::support::http::state::{AppState, ShutdownHandles, TASK_MANAGER_TIMEOUT_S}; +use crate::support::http::RouterBuilder; +use crate::support::nyxd; +use crate::support::storage::NymApiStorage; +use crate::v3_migration::migrate_v3_database; +use crate::{ + circulating_supply_api, ecash, epoch_operations, network_monitor, node_describe_cache, + node_status_api, nym_contract_cache, +}; +use anyhow::{bail, Context}; +use nym_config::defaults::NymNetworkDetails; +use nym_sphinx::receiver::SphinxMessageReceiver; +use nym_task::TaskManager; +use nym_validator_client::nyxd::Coin; +use std::sync::Arc; +use tokio_util::sync::CancellationToken; +use tracing::{error, info}; #[derive(clap::Args, Debug)] pub(crate) struct Args { @@ -54,6 +88,196 @@ pub(crate) struct Args { pub(crate) monitor_credentials_mode: Option, } +async fn start_nym_api_tasks_axum(config: &Config) -> anyhow::Result { + let nyxd_client = nyxd::Client::new(config); + let connected_nyxd = config.get_nyxd_url(); + let nym_network_details = NymNetworkDetails::new_from_env(); + let network_details = NetworkDetails::new(connected_nyxd.to_string(), nym_network_details); + + let ecash_keypair_wrapper = ecash::keys::KeyPair::new(); + + // if the keypair doesnt exist (because say this API is running in the caching mode), nothing will happen + if let Some(loaded_keys) = load_ecash_keypair_if_exists(&config.ecash_signer)? { + let issued_for = loaded_keys.issued_for_epoch; + ecash_keypair_wrapper.set(loaded_keys).await; + + if can_validate_coconut_keys(&nyxd_client, issued_for).await? { + ecash_keypair_wrapper.validate() + } + } + + let storage = NymApiStorage::init(&config.node_status_api.storage_paths.database_path).await?; + + // try to perform any needed migrations of the storage + migrate_v3_database(&storage, &nyxd_client).await?; + + let identity_keypair = config.base.storage_paths.load_identity()?; + let identity_public_key = *identity_keypair.public_key(); + + let router = RouterBuilder::with_default_routes(config.network_monitor.enabled); + + let nym_contract_cache_state = NymContractCache::new(); + let node_status_cache_state = NodeStatusCache::new(); + let mix_denom = network_details.network.chain_details.mix_denom.base.clone(); + let circulating_supply_cache = CirculatingSupplyCache::new(mix_denom.to_owned()); + let described_nodes_cache = SharedCache::::new(); + let node_info_cache = unstable::NodeInfoCache::default(); + + let mut status_state = ApiStatusState::new(); + + // if ecash signer is enabled, add /ecash to server + let router = if config.ecash_signer.enabled { + let cosmos_address = nyxd_client.address().await; + + // make sure we have some tokens to cover multisig fees + let balance = nyxd_client.balance(&mix_denom).await?; + if balance.amount < ecash::MINIMUM_BALANCE { + let min = Coin::new(ecash::MINIMUM_BALANCE, mix_denom); + bail!("the account ({cosmos_address}) doesn't have enough funds to cover verification fees. it has {balance} while it needs at least {min}") + } + + let announce_address = config + .ecash_signer + .announce_address + .clone() + .map(|u| u.to_string()) + .unwrap_or_default(); + status_state.add_zk_nym_signer(SignerState { + cosmos_address: cosmos_address.to_string(), + identity: identity_keypair.public_key().to_base58_string(), + announce_address, + ecash_keypair: ecash_keypair_wrapper.clone(), + }); + + let ecash_contract = nyxd_client + .get_ecash_contract_address() + .await + .context("e-cash contract address is required to setup the zk-nym signer")?; + + let comm_channel = QueryCommunicationChannel::new(nyxd_client.clone()); + + let ecash_state = EcashState::new( + ecash_contract, + nyxd_client.clone(), + identity_keypair, + ecash_keypair_wrapper.clone(), + comm_channel, + storage.clone(), + ) + .await?; + + router.nest("/v1/ecash", ecash_routes(Arc::new(ecash_state))) + } else { + router + }; + + let router = router.with_state(AppState { + nym_contract_cache: nym_contract_cache_state.clone(), + node_status_cache: node_status_cache_state.clone(), + circulating_supply_cache: circulating_supply_cache.clone(), + storage: storage.clone(), + described_nodes_cache: described_nodes_cache.clone(), + network_details, + node_info_cache, + }); + + let task_manager = TaskManager::new(TASK_MANAGER_TIMEOUT_S); + + // start note describe cache refresher + // we should be doing the below, but can't due to our current startup structure + // let refresher = node_describe_cache::new_refresher(&config.topology_cacher); + // let cache = refresher.get_shared_cache(); + node_describe_cache::new_refresher_with_initial_value( + &config.topology_cacher, + nym_contract_cache_state.clone(), + described_nodes_cache.clone(), + ) + .named("node-self-described-data-refresher") + .start(task_manager.subscribe_named("node-self-described-data-refresher")); + + // start all the caches first + let nym_contract_cache_listener = nym_contract_cache::start_refresher( + &config.node_status_api, + &nym_contract_cache_state, + nyxd_client.clone(), + &task_manager, + ); + node_status_api::start_cache_refresh( + &config.node_status_api, + &nym_contract_cache_state, + &node_status_cache_state, + storage.clone(), + nym_contract_cache_listener, + &task_manager, + ); + circulating_supply_api::start_cache_refresh( + &config.circulating_supply_cacher, + nyxd_client.clone(), + &circulating_supply_cache, + &task_manager, + ); + + // start dkg task + if config.ecash_signer.enabled { + let dkg_bte_keypair = load_bte_keypair(&config.ecash_signer)?; + + DkgController::start( + &config.ecash_signer, + nyxd_client.clone(), + ecash_keypair_wrapper, + dkg_bte_keypair, + identity_public_key, + rand::rngs::OsRng, + &task_manager, + )?; + } + + // and then only start the uptime updater (and the monitor itself, duh) + // if the monitoring is enabled + if config.network_monitor.enabled { + network_monitor::start::( + &config.network_monitor, + &nym_contract_cache_state, + described_nodes_cache.clone(), + &storage, + nyxd_client.clone(), + &task_manager, + ) + .await; + + HistoricalUptimeUpdater::start(storage.to_owned(), &task_manager); + + // start 'rewarding' if its enabled + if config.rewarding.enabled { + epoch_operations::ensure_rewarding_permission(&nyxd_client).await?; + EpochAdvancer::start( + nyxd_client, + &nym_contract_cache_state, + described_nodes_cache.clone(), + &storage, + &task_manager, + ); + } + } + + let bind_address = config.base.bind_address.to_owned(); + let server = router.build_server(&bind_address).await?; + + let cancellation_token = CancellationToken::new(); + let shutdown_button = cancellation_token.clone(); + let axum_shutdown_receiver = cancellation_token.cancelled_owned(); + let server_handle = tokio::spawn(async move { + { + info!("Started Axum HTTP V2 server on {bind_address}"); + server.run(axum_shutdown_receiver).await + } + }); + + let shutdown = ShutdownHandles::new(task_manager, server_handle, shutdown_button); + + Ok(shutdown) +} + pub(crate) async fn execute(args: Args) -> anyhow::Result<()> { // args take precedence over env let config = try_load_current_config(&args.id)? @@ -62,41 +286,32 @@ pub(crate) async fn execute(args: Args) -> anyhow::Result<()> { config.validate()?; - #[cfg(feature = "axum")] - let mut axum_shutdown = crate::v2::start_nym_api_tasks_v2(&config).await?; - let mut rocket_shutdown = start_nym_api_tasks(config).await?; + let mut axum_shutdown = start_nym_api_tasks_axum(&config).await?; // it doesn't matter which server catches the interrupt: it needs only be caught once - if let Err(err) = rocket_shutdown.task_manager_handle.catch_interrupt().await { + if let Err(err) = axum_shutdown.task_manager_mut().catch_interrupt().await { error!("Error stopping Rocket tasks: {err}"); } - log::info!("Stopping nym API"); - rocket_shutdown.rocket_handle.notify(); + info!("Stopping nym API"); - // because Rocket caught the interrupt, it had already signalled its - // background tasks to retire. Now do that for axum - cfg_if! { - if #[cfg(feature = "axum")] { - axum_shutdown.task_manager_mut().signal_shutdown().ok(); - axum_shutdown.task_manager_mut().wait_for_shutdown().await; + axum_shutdown.task_manager_mut().signal_shutdown().ok(); + axum_shutdown.task_manager_mut().wait_for_shutdown().await; - let running_server = axum_shutdown.shutdown_axum(); + let running_server = axum_shutdown.shutdown_axum(); - match running_server.await { - Ok(Ok(_)) => { - info!("Axum HTTP server shut down without errors"); - }, - Ok(Err(err)) => { - error!("Axum HTTP server terminated with: {err}"); - anyhow::bail!(err) - }, - Err(err) => { - error!("Server task panicked: {err}"); - } - }; + match running_server.await { + Ok(Ok(_)) => { + info!("Axum HTTP server shut down without errors"); } - } + Ok(Err(err)) => { + error!("Axum HTTP server terminated with: {err}"); + anyhow::bail!(err) + } + Err(err) => { + error!("Server task panicked: {err}"); + } + }; Ok(()) } diff --git a/nym-api/src/support/config/helpers.rs b/nym-api/src/support/config/helpers.rs index 6521c8f7f4..8a6652e4f8 100644 --- a/nym-api/src/support/config/helpers.rs +++ b/nym-api/src/support/config/helpers.rs @@ -42,7 +42,7 @@ pub(crate) fn initialise_new(id: &str) -> Result { // create DKG BTE keys let mut rng = OsRng; - init_bte_keypair(&mut rng, &config.coconut_signer)?; + init_bte_keypair(&mut rng, &config.ecash_signer)?; Ok(config) } diff --git a/nym-api/src/support/config/mod.rs b/nym-api/src/support/config/mod.rs index a26384652f..235464dc0a 100644 --- a/nym-api/src/support/config/mod.rs +++ b/nym-api/src/support/config/mod.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::support::config::persistence::{ - CoconutSignerPaths, NetworkMonitorPaths, NodeStatusAPIPaths, NymApiPaths, + EcashSignerPaths, NetworkMonitorPaths, NodeStatusAPIPaths, NymApiPaths, }; use crate::support::config::r#override::OverrideConfig; use crate::support::config::template::CONFIG_TEMPLATE; @@ -19,6 +19,7 @@ use std::io; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::path::{Path, PathBuf}; use std::time::Duration; +use tracing::debug; use url::Url; use zeroize::{Zeroize, ZeroizeOnDrop}; @@ -106,7 +107,8 @@ pub struct Config { pub rewarding: Rewarding, - pub coconut_signer: CoconutSigner, + #[serde(alias = "coconut_signer")] + pub ecash_signer: EcashSigner, } impl NymConfigTemplate for Config { @@ -125,7 +127,7 @@ impl Config { topology_cacher: Default::default(), circulating_supply_cacher: Default::default(), rewarding: Default::default(), - coconut_signer: CoconutSigner::new_default(id.as_ref()), + ecash_signer: EcashSigner::new_default(id.as_ref()), } } @@ -136,7 +138,7 @@ impl Config { bail!("can't enable rewarding without providing a mnemonic") } - if !can_sign && self.coconut_signer.enabled { + if !can_sign && self.ecash_signer.enabled { bail!("can't enable coconut signer without providing a mnemonic") } @@ -159,10 +161,10 @@ impl Config { self.base.mnemonic = Some(mnemonic) } if let Some(enable_zk_nym) = args.enable_zk_nym { - self.coconut_signer.enabled = enable_zk_nym + self.ecash_signer.enabled = enable_zk_nym } if let Some(announce_address) = args.announce_address { - self.coconut_signer.announce_address = Some(announce_address) + self.ecash_signer.announce_address = Some(announce_address) } if let Some(monitor_credentials_mode) = args.monitor_credentials_mode { self.network_monitor.debug.disabled_credentials_mode = !monitor_credentials_mode @@ -226,13 +228,12 @@ impl Config { } } -// TODO rocket: when axum becomes the main server, change its bind addr default here fn default_http_socket_addr() -> SocketAddr { cfg_if::cfg_if! { if #[cfg(debug_assertions)] { - SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081) + SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8080) } else { - SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 8081) + SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 8080) } } } @@ -518,25 +519,25 @@ impl Default for RewardingDebug { } #[derive(Debug, Deserialize, PartialEq, Eq, Serialize)] -pub struct CoconutSigner { +pub struct EcashSigner { /// Specifies whether rewarding service is enabled in this process. pub enabled: bool, #[serde(deserialize_with = "de_maybe_stringified")] pub announce_address: Option, - pub storage_paths: CoconutSignerPaths, + pub storage_paths: EcashSignerPaths, #[serde(default)] - pub debug: CoconutSignerDebug, + pub debug: EcashSignerDebug, } -impl CoconutSigner { +impl EcashSigner { pub fn new_default>(id: P) -> Self { - CoconutSigner { + EcashSigner { enabled: false, announce_address: None, - storage_paths: CoconutSignerPaths::new_default(id), + storage_paths: EcashSignerPaths::new_default(id), debug: Default::default(), } } @@ -544,15 +545,15 @@ impl CoconutSigner { #[derive(Debug, Deserialize, PartialEq, Eq, Serialize)] #[serde(default)] -pub struct CoconutSignerDebug { +pub struct EcashSignerDebug { /// Duration of the interval for polling the dkg contract. #[serde(with = "humantime_serde")] pub dkg_contract_polling_rate: Duration, } -impl Default for CoconutSignerDebug { +impl Default for EcashSignerDebug { fn default() -> Self { - CoconutSignerDebug { + EcashSignerDebug { dkg_contract_polling_rate: DEFAULT_DKG_CONTRACT_POLLING_RATE, } } diff --git a/nym-api/src/support/config/persistence.rs b/nym-api/src/support/config/persistence.rs index d14996b743..f2c98c038f 100644 --- a/nym-api/src/support/config/persistence.rs +++ b/nym-api/src/support/config/persistence.rs @@ -14,7 +14,9 @@ pub const DEFAULT_NODE_STATUS_API_DATABASE_FILENAME: &str = "db.sqlite"; pub const DEFAULT_DKG_PERSISTENT_STATE_FILENAME: &str = "dkg_persistent_state.json"; pub const DEFAULT_DKG_DECRYPTION_KEY_FILENAME: &str = "dkg_decryption_key.pem"; pub const DEFAULT_DKG_PUBLIC_KEY_WITH_PROOF_FILENAME: &str = "dkg_public_key_with_proof.pem"; -pub const DEFAULT_COCONUT_KEY_FILENAME: &str = "coconut.pem"; + +// don't want to be changing the defaults in case something breaks..., but it should be called ecash.pem instead +pub const DEFAULT_ECASH_KEY_FILENAME: &str = "coconut.pem"; pub const DEFAULT_PRIVATE_IDENTITY_KEY_FILENAME: &str = "private_identity.pem"; pub const DEFAULT_PUBLIC_IDENTITY_KEY_FILENAME: &str = "public_identity.pem"; @@ -73,12 +75,13 @@ impl NodeStatusAPIPaths { } #[derive(Debug, Deserialize, PartialEq, Eq, Serialize)] -pub struct CoconutSignerPaths { +pub struct EcashSignerPaths { /// Path to a JSON file where state is persisted between different stages of DKG. pub dkg_persistent_state_path: PathBuf, /// Path to the coconut key. - pub coconut_key_path: PathBuf, + #[serde(alias = "coconut_key_path")] + pub ecash_key_path: PathBuf, /// Path to the dkg dealer decryption key. pub decryption_key_path: PathBuf, @@ -87,13 +90,13 @@ pub struct CoconutSignerPaths { pub public_key_with_proof_path: PathBuf, } -impl CoconutSignerPaths { +impl EcashSignerPaths { pub fn new_default>(id: P) -> Self { let data_dir = default_data_directory(id); - CoconutSignerPaths { + EcashSignerPaths { dkg_persistent_state_path: data_dir.join(DEFAULT_DKG_PERSISTENT_STATE_FILENAME), - coconut_key_path: data_dir.join(DEFAULT_COCONUT_KEY_FILENAME), + ecash_key_path: data_dir.join(DEFAULT_ECASH_KEY_FILENAME), decryption_key_path: data_dir.join(DEFAULT_DKG_DECRYPTION_KEY_FILENAME), public_key_with_proof_path: data_dir.join(DEFAULT_DKG_PUBLIC_KEY_WITH_PROOF_FILENAME), } diff --git a/nym-api/src/support/config/template.rs b/nym-api/src/support/config/template.rs index 108cc0f426..5ee155e016 100644 --- a/nym-api/src/support/config/template.rs +++ b/nym-api/src/support/config/template.rs @@ -16,7 +16,6 @@ id = '{{ base.id }}' local_validator = '{{ base.local_validator }}' # Socket address this api will use for binding its http API. -# Note: only used if `axum` feature is enabled. bind_address = '{{ base.bind_address }}' # Mnemonic used for rewarding and validator interaction @@ -109,26 +108,26 @@ enabled = {{ rewarding.enabled }} # Note, only values in range 0-100 are valid minimum_interval_monitor_threshold = {{ rewarding.debug.minimum_interval_monitor_threshold }} -[coconut_signer] +[ecash_signer] -# Specifies whether coconut signing protocol is enabled in this process. -enabled = {{ coconut_signer.enabled }} +# Specifies whether ecash signing protocol is enabled in this process. +enabled = {{ ecash_signer.enabled }} # address of this nym-api as announced to other instances for the purposes of performing the DKG. -announce_address = '{{ coconut_signer.announce_address }}' +announce_address = '{{ ecash_signer.announce_address }}' -[coconut_signer.storage_paths] +[ecash_signer.storage_paths] # Path to a JSON file where state is persisted between different stages of DKG. -dkg_persistent_state_path = '{{ coconut_signer.storage_paths.dkg_persistent_state_path }}' +dkg_persistent_state_path = '{{ ecash_signer.storage_paths.dkg_persistent_state_path }}' -# Path to the coconut key. -coconut_key_path = '{{ coconut_signer.storage_paths.coconut_key_path }}' +# Path to the ecash key. +ecash_key_path = '{{ ecash_signer.storage_paths.ecash_key_path }}' # Path to the dkg dealer decryption key -decryption_key_path = '{{ coconut_signer.storage_paths.decryption_key_path }}' +decryption_key_path = '{{ ecash_signer.storage_paths.decryption_key_path }}' # Path to the dkg dealer public key with proof -public_key_with_proof_path = '{{ coconut_signer.storage_paths.public_key_with_proof_path }}' +public_key_with_proof_path = '{{ ecash_signer.storage_paths.public_key_with_proof_path }}' "#; diff --git a/nym-api/src/support/http/helpers.rs b/nym-api/src/support/http/helpers.rs index 45efa8f586..4ea95d4cd2 100644 --- a/nym-api/src/support/http/helpers.rs +++ b/nym-api/src/support/http/helpers.rs @@ -1,11 +1,21 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use nym_mixnet_contract_common::NodeId; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use utoipa::{IntoParams, ToSchema}; -#[derive(Serialize, Deserialize, FromForm, Debug, JsonSchema)] +#[derive(Serialize, Deserialize, Debug, JsonSchema, ToSchema, IntoParams)] +#[into_params(parameter_in = Query)] pub struct PaginationRequest { pub page: Option, pub per_page: Option, } + +#[derive(Deserialize, IntoParams, ToSchema)] +#[into_params(parameter_in = Path)] +pub(crate) struct NodeIdParam { + #[schema(value_type = u32)] + pub(crate) node_id: NodeId, +} diff --git a/nym-api/src/support/http/mod.rs b/nym-api/src/support/http/mod.rs index 612dcb9333..9446891443 100644 --- a/nym-api/src/support/http/mod.rs +++ b/nym-api/src/support/http/mod.rs @@ -1,141 +1,120 @@ // Copyright 2022-2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::circulating_supply_api::cache::CirculatingSupplyCache; -use crate::ecash::client::Client; -use crate::ecash::state::EcashState; -use crate::ecash::{self, comm::QueryCommunicationChannel}; -use crate::network::models::NetworkDetails; -use crate::network::network_routes; -use crate::node_describe_cache::DescribedNodes; -use crate::node_status_api::routes_deprecated::unstable; -use crate::node_status_api::{self, NodeStatusCache}; -use crate::nym_contract_cache::cache::NymContractCache; -use crate::nym_nodes::{nym_node_routes_deprecated, nym_node_routes_next}; -use crate::status::{api_status_routes, ApiStatusState, SignerState}; -use crate::support::caching::cache::SharedCache; -use crate::support::config::Config; -use crate::support::{nyxd, storage}; -use crate::{circulating_supply_api, nym_contract_cache}; -use anyhow::{bail, Context, Result}; -use nym_crypto::asymmetric::identity; -use nym_validator_client::nyxd::Coin; -use rocket::{Ignite, Rocket}; -use rocket_cors::{AllowedHeaders, AllowedOrigins, Cors}; -use rocket_okapi::mount_endpoints_and_merged_docs; -use rocket_okapi::swagger_ui::make_swagger_ui; - pub(crate) mod helpers; pub(crate) mod openapi; +pub(crate) mod router; +pub(crate) mod state; +mod unstable_routes; -pub(crate) async fn setup_rocket( - config: &Config, - network_details: NetworkDetails, - nyxd_client: nyxd::Client, - identity_keypair: identity::KeyPair, - coconut_keypair: ecash::keys::KeyPair, -) -> anyhow::Result> { - let openapi_settings = rocket_okapi::settings::OpenApiSettings::default(); - let mut rocket = rocket::build(); +pub(crate) use router::RouterBuilder; - let mix_denom = network_details.network.chain_details.mix_denom.base.clone(); - - mount_endpoints_and_merged_docs! { - rocket, - "/v1".to_owned(), - openapi_settings, - "/" => (vec![], openapi::custom_openapi_spec()), - "" => circulating_supply_api::circulating_supply_routes(&openapi_settings), - "" => nym_contract_cache::nym_contract_cache_routes(&openapi_settings), - "/status" => node_status_api::node_status_routes(&openapi_settings, config.network_monitor.enabled), - "/network" => network_routes(&openapi_settings), - "/api-status" => api_status_routes(&openapi_settings), - "/ecash" => ecash::routes_open_api(&openapi_settings, config.coconut_signer.enabled), - "" => nym_node_routes_deprecated(&openapi_settings), - - // => when we move those routes, we'll need to add a redirection for backwards compatibility - "/unstable/nym-nodes" => nym_node_routes_next(&openapi_settings) - } - - let storage = - storage::NymApiStorage::init(&config.node_status_api.storage_paths.database_path).await?; - - let rocket = rocket - .manage(network_details) - .manage(SharedCache::::new()) - .mount("/swagger", make_swagger_ui(&openapi::get_docs())) - .attach(setup_rocket_cors()?) - .attach(NymContractCache::stage()) - .attach(NodeStatusCache::stage()) - .attach(CirculatingSupplyCache::stage(mix_denom.clone())) - .attach(storage::NymApiStorage::stage(storage.clone())) - .manage(unstable::NodeInfoCache::default()); - - let mut status_state = ApiStatusState::new(); - - let rocket = if config.coconut_signer.enabled { - // make sure we have some tokens to cover multisig fees - let balance = nyxd_client.balance(&mix_denom).await?; - if balance.amount < ecash::MINIMUM_BALANCE { - let address = nyxd_client.address().await; - let min = Coin::new(ecash::MINIMUM_BALANCE, mix_denom); - bail!("the account ({address}) doesn't have enough funds to cover verification fees. it has {balance} while it needs at least {min}") - } - - let cosmos_address = nyxd_client.address().await.to_string(); - let announce_address = config - .coconut_signer - .announce_address - .clone() - .map(|u| u.to_string()) - .unwrap_or_default(); - status_state.add_zk_nym_signer(SignerState { - cosmos_address, - identity: identity_keypair.public_key().to_base58_string(), - announce_address, - coconut_keypair: coconut_keypair.clone(), - }); - - let ecash_contract = nyxd_client - .get_ecash_contract_address() - .await - .context("e-cash contract address is required to setup the zk-nym signer")?; - - let comm_channel = QueryCommunicationChannel::new(nyxd_client.clone()); - - let ecash_state = EcashState::new( - ecash_contract, - nyxd_client.clone(), - identity_keypair, - coconut_keypair, - comm_channel, - storage.clone(), - ) - .await?; - - rocket.manage(ecash_state) - } else { - rocket - }; - - Ok(rocket.manage(status_state).ignite().await?) -} - -fn setup_rocket_cors() -> Result { - let allowed_origins = AllowedOrigins::all(); - - // You can also deserialize this - let cors = rocket_cors::CorsOptions { - allowed_origins, - allowed_methods: vec![rocket::http::Method::Post, rocket::http::Method::Get] - .into_iter() - .map(From::from) - .collect(), - allowed_headers: AllowedHeaders::all(), - allow_credentials: true, - ..Default::default() - } - .to_cors()?; - - Ok(cors) -} +// pub(crate) async fn setup_rocket( +// config: &Config, +// network_details: NetworkDetails, +// nyxd_client: nyxd::Client, +// identity_keypair: identity::KeyPair, +// coconut_keypair: ecash::keys::KeyPair, +// storage: NymApiStorage, +// ) -> anyhow::Result> { +// let openapi_settings = rocket_okapi::settings::OpenApiSettings::default(); +// let mut rocket = rocket::build(); +// +// let mix_denom = network_details.network.chain_details.mix_denom.base.clone(); +// +// mount_endpoints_and_merged_docs! { +// rocket, +// "/v1".to_owned(), +// openapi_settings, +// "/" => (vec![], openapi::custom_openapi_spec()), +// "" => circulating_supply_api::circulating_supply_routes(&openapi_settings), +// "" => nym_contract_cache::nym_contract_cache_routes(&openapi_settings), +// "/status" => node_status_api::node_status_routes(&openapi_settings, config.network_monitor.enabled), +// "/network" => network_routes(&openapi_settings), +// "/api-status" => api_status_routes(&openapi_settings), +// "/ecash" => ecash::routes_open_api(&openapi_settings, config.ecash_signer.enabled), +// "" => nym_node_routes_deprecated(&openapi_settings), +// +// // => when we move those routes, we'll need to add a redirection for backwards compatibility +// "/unstable/nym-nodes" => nym_node_routes_next(&openapi_settings) +// } +// +// let rocket = rocket +// .manage(network_details) +// .manage(SharedCache::::new()) +// .mount("/swagger", make_swagger_ui(&openapi::get_docs())) +// .attach(setup_rocket_cors()?) +// .attach(NymContractCache::stage()) +// .attach(NodeStatusCache::stage()) +// .attach(CirculatingSupplyCache::stage(mix_denom.clone())) +// .manage(unstable::NodeInfoCache::default()) +// .manage(storage.clone()); +// +// let mut status_state = ApiStatusState::new(); +// +// let rocket = if config.ecash_signer.enabled { +// // make sure we have some tokens to cover multisig fees +// let balance = nyxd_client.balance(&mix_denom).await?; +// if balance.amount < ecash::MINIMUM_BALANCE { +// let address = nyxd_client.address().await; +// let min = Coin::new(ecash::MINIMUM_BALANCE, mix_denom); +// bail!("the account ({address}) doesn't have enough funds to cover verification fees. it has {balance} while it needs at least {min}") +// } +// +// let cosmos_address = nyxd_client.address().await.to_string(); +// let announce_address = config +// .ecash_signer +// .announce_address +// .clone() +// .map(|u| u.to_string()) +// .unwrap_or_default(); +// status_state.add_zk_nym_signer(SignerState { +// cosmos_address, +// identity: identity_keypair.public_key().to_base58_string(), +// announce_address, +// ecash_keypair: coconut_keypair.clone(), +// }); +// +// let ecash_contract = nyxd_client +// .get_ecash_contract_address() +// .await +// .context("e-cash contract address is required to setup the zk-nym signer")?; +// +// let comm_channel = QueryCommunicationChannel::new(nyxd_client.clone()); +// +// let ecash_state = EcashState::new( +// ecash_contract, +// nyxd_client.clone(), +// identity_keypair, +// coconut_keypair, +// comm_channel, +// storage.clone(), +// ) +// .await?; +// +// rocket.manage(ecash_state) +// } else { +// rocket +// }; +// +// Ok(rocket.manage(status_state).ignite().await?) +// } +// +// fn setup_rocket_cors() -> Result { +// let allowed_origins = AllowedOrigins::all(); +// +// // You can also deserialize this +// let cors = rocket_cors::CorsOptions { +// allowed_origins, +// allowed_methods: vec![rocket::http::Method::Post, rocket::http::Method::Get] +// .into_iter() +// .map(From::from) +// .collect(), +// allowed_headers: AllowedHeaders::all(), +// allow_credentials: true, +// ..Default::default() +// } +// .to_cors()?; +// +// Ok(cors) +// } diff --git a/nym-api/src/support/http/openapi.rs b/nym-api/src/support/http/openapi.rs index 858d201dc7..4fc89422d9 100644 --- a/nym-api/src/support/http/openapi.rs +++ b/nym-api/src/support/http/openapi.rs @@ -1,41 +1,89 @@ -// Copyright 2023 - Nym Technologies SA +// Copyright 2023-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use okapi::openapi3::OpenApi; -use rocket_okapi::swagger_ui::SwaggerUIConfig; +use crate::network::handlers::ContractVersionSchemaResponse; +use nym_api_requests::models; +use utoipa::OpenApi; +use utoipauto::utoipauto; -pub fn custom_openapi_spec() -> OpenApi { - use rocket_okapi::okapi::openapi3::*; - OpenApi { - openapi: OpenApi::default_version(), - info: Info { - title: "Nym API".to_owned(), - description: None, - terms_of_service: None, - contact: None, - license: None, - version: env!("CARGO_PKG_VERSION").to_owned(), - ..Default::default() - }, - servers: get_servers(), - ..Default::default() - } -} +// TODO once https://github.com/ProbablyClem/utoipauto/pull/38 is released: +// include ",./nym-api/nym-api-requests/src from nym-api-requests" (and other packages mentioned below) +// for automatic model discovery based on ToSchema / IntoParams implementation. +// Then you can remove `components(schemas)` manual imports below -fn get_servers() -> Vec { - if std::env::var_os("CARGO").is_some() { - return vec![]; - } - vec![rocket_okapi::okapi::openapi3::Server { - url: std::env::var("OPEN_API_BASE").unwrap_or_else(|_| "/api/v1/".to_owned()), - description: Some("API".to_owned()), - ..Default::default() - }] -} - -pub(crate) fn get_docs() -> SwaggerUIConfig { - SwaggerUIConfig { - url: "../v1/openapi.json".to_owned(), - ..SwaggerUIConfig::default() - } -} +#[utoipauto(paths = "./nym-api/src")] +#[derive(OpenApi)] +#[openapi( + info(title = "Nym API"), + tags(), + components(schemas( + models::CirculatingSupplyResponse, + models::CoinSchema, + nym_mixnet_contract_common::Interval, + nym_api_requests::models::GatewayStatusReportResponse, + nym_api_requests::models::GatewayUptimeHistoryResponse, + nym_api_requests::models::GatewayCoreStatusResponse, + nym_api_requests::models::GatewayUptimeResponse, + nym_api_requests::models::RewardEstimationResponse, + nym_api_requests::models::UptimeResponse, + nym_api_requests::models::ComputeRewardEstParam, + nym_api_requests::models::MixNodeBondAnnotated, + nym_api_requests::models::GatewayBondAnnotated, + nym_api_requests::models::MixnodeTestResultResponse, + nym_api_requests::models::StakeSaturationResponse, + nym_api_requests::models::InclusionProbabilityResponse, + nym_api_requests::models::AllInclusionProbabilitiesResponse, + nym_api_requests::models::InclusionProbability, + nym_api_requests::models::SelectionChance, + crate::network::models::NetworkDetails, + nym_config::defaults::NymNetworkDetails, + nym_config::defaults::ChainDetails, + nym_config::defaults::DenomDetailsOwned, + nym_config::defaults::ValidatorDetails, + nym_config::defaults::NymContracts, + ContractVersionSchemaResponse, + crate::network::models::ContractInformation, + nym_api_requests::models::ApiHealthResponse, + nym_api_requests::models::ApiStatus, + nym_bin_common::build_information::BinaryBuildInformationOwned, + nym_api_requests::models::SignerInformationResponse, + nym_api_requests::models::LegacyDescribedGateway, + nym_mixnet_contract_common::Gateway, + nym_mixnet_contract_common::GatewayBond, + nym_api_requests::models::NymNodeDescription, + nym_api_requests::models::HostInformation, + nym_api_requests::models::HostKeys, + nym_node_requests::api::v1::node::models::AuxiliaryDetails, + nym_api_requests::models::NetworkRequesterDetails, + nym_api_requests::models::IpPacketRouterDetails, + nym_api_requests::models::AuthenticatorDetails, + nym_api_requests::models::WebSockets, + nym_api_requests::nym_nodes::NodeRole, + nym_api_requests::models::LegacyDescribedMixNode, + nym_api_requests::ecash::VerificationKeyResponse, + nym_api_requests::ecash::models::AggregatedExpirationDateSignatureResponse, + nym_api_requests::ecash::models::AggregatedCoinIndicesSignatureResponse, + nym_api_requests::ecash::models::EpochCredentialsResponse, + nym_api_requests::ecash::models::IssuedCredentialResponse, + nym_api_requests::ecash::models::IssuedTicketbookBody, + nym_api_requests::ecash::models::BlindedSignatureResponse, + nym_api_requests::ecash::models::PartialExpirationDateSignatureResponse, + nym_api_requests::ecash::models::PartialCoinIndicesSignatureResponse, + nym_api_requests::ecash::models::EcashTicketVerificationResponse, + nym_api_requests::ecash::models::EcashTicketVerificationRejection, + nym_api_requests::ecash::models::EcashBatchTicketRedemptionResponse, + nym_api_requests::ecash::models::SpentCredentialsResponse, + nym_api_requests::ecash::models::IssuedCredentialsResponse, + nym_api_requests::nym_nodes::SkimmedNode, + nym_api_requests::nym_nodes::SemiSkimmedNode, + nym_api_requests::nym_nodes::FullFatNode, + nym_api_requests::nym_nodes::BasicEntryInformation, + nym_api_requests::nym_nodes::NodeRoleQueryParam, + nym_api_requests::models::AnnotationResponse, + nym_api_requests::models::NodePerformanceResponse, + nym_api_requests::models::NodeDatePerformanceResponse, + nym_api_requests::models::PerformanceHistoryResponse, + nym_api_requests::models::UptimeHistoryResponse, + )) +)] +pub(crate) struct ApiDoc; diff --git a/nym-api/src/v2/router.rs b/nym-api/src/support/http/router.rs similarity index 81% rename from nym-api/src/v2/router.rs rename to nym-api/src/support/http/router.rs index 6ece0b7323..38eec8d76e 100644 --- a/nym-api/src/v2/router.rs +++ b/nym-api/src/support/http/router.rs @@ -5,11 +5,15 @@ use crate::circulating_supply_api::handlers::circulating_supply_routes; use crate::network::handlers::nym_network_routes; use crate::node_status_api::handlers::node_status_routes; use crate::nym_contract_cache::handlers::nym_contract_cache_routes; +use crate::nym_nodes::handlers::legacy::legacy_nym_node_routes; use crate::nym_nodes::handlers::nym_node_routes; -use crate::nym_nodes::handlers_unstable::nym_node_routes_unstable; use crate::status; -use crate::v2::AxumAppState; +use crate::support::http::openapi::ApiDoc; +use crate::support::http::state::AppState; +use crate::support::http::unstable_routes::unstable_routes; use anyhow::anyhow; +use axum::response::Redirect; +use axum::routing::get; use axum::Router; use core::net::SocketAddr; use nym_http_api_common::logging::logger; @@ -27,7 +31,7 @@ use utoipa_swagger_ui::SwaggerUi; /// /// [order]: https://docs.rs/axum/latest/axum/middleware/index.html#ordering pub(crate) struct RouterBuilder { - unfinished_router: Router, + unfinished_router: Router, } impl RouterBuilder { @@ -43,23 +47,22 @@ impl RouterBuilder { // .on_request(DefaultOnRequest::new()) // .on_response(DefaultOnResponse::new().latency_unit(tower_http::LatencyUnit::Micros)), // ) - // .route("/", axum::routing::get(|| async {axum::response::Redirect::permanent("/swagger")})) // .route("/swagger", axum::routing::get(hello)) let default_routes = Router::new() - .merge( - SwaggerUi::new("/swagger") - .url("/api-docs/openapi.json", super::api_docs::ApiDoc::openapi()), - ) + .merge(SwaggerUi::new("/swagger").url("/api-docs/openapi.json", ApiDoc::openapi())) + .route("/", get(|| async { Redirect::to("/swagger") })) .nest( "/v1", Router::new() - .nest("/circulating-supply", circulating_supply_routes()) + // unfortunately some routes didn't use correct prefix and were attached to the root .merge(nym_contract_cache_routes()) + .merge(legacy_nym_node_routes()) + .nest("/circulating-supply", circulating_supply_routes()) .nest("/status", node_status_routes(network_monitor)) .nest("/network", nym_network_routes()) .nest("/api-status", status::handlers::api_status_routes()) - .merge(nym_node_routes()) - .nest("/unstable/nym-nodes", nym_node_routes_unstable()), // CORS layer needs to be "outside" of routes + .nest("/nym-nodes", nym_node_routes()) + .nest("/unstable", unstable_routes()), // CORS layer needs to be "outside" of routes ); Self { @@ -67,7 +70,7 @@ impl RouterBuilder { } } - pub(crate) fn nest(self, path: &str, router: Router) -> Self { + pub(crate) fn nest(self, path: &str, router: Router) -> Self { Self { unfinished_router: self.unfinished_router.nest(path, router), } @@ -75,14 +78,14 @@ impl RouterBuilder { /// Invoke this as late as possible before constructing HTTP server /// (after all routes were added). - pub(crate) fn with_state(self, state: AxumAppState) -> RouterWithState { + pub(crate) fn with_state(self, state: AppState) -> RouterWithState { RouterWithState { router: self.finalize_routes().with_state(state), } } /// Middleware added here intercepts the request before it gets to other routes. - fn finalize_routes(self) -> Router { + fn finalize_routes(self) -> Router { self.unfinished_router .layer(setup_cors()) .layer(axum::middleware::from_fn(logger)) diff --git a/nym-api/src/support/http/state.rs b/nym-api/src/support/http/state.rs new file mode 100644 index 0000000000..a0a640333b --- /dev/null +++ b/nym-api/src/support/http/state.rs @@ -0,0 +1,104 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::circulating_supply_api::cache::CirculatingSupplyCache; +use crate::network::models::NetworkDetails; +use crate::node_describe_cache::DescribedNodes; +use crate::node_status_api::handlers::unstable; +use crate::node_status_api::NodeStatusCache; +use crate::nym_contract_cache::cache::NymContractCache; +use crate::support::caching::cache::SharedCache; +use crate::support::storage; +use nym_task::TaskManager; +use tokio::task::JoinHandle; +use tokio_util::sync::CancellationToken; + +pub(crate) const TASK_MANAGER_TIMEOUT_S: u64 = 10; + +/// Shutdown goes 2 directions: +/// 1. signal background tasks to gracefully finish +/// 2. signal server itself +/// +/// These are done through separate shutdown handles. Of course, shut down server +/// AFTER you have shut down BG tasks (or past their grace period). +pub(crate) struct ShutdownHandles { + task_manager: TaskManager, + axum_shutdown_button: ShutdownAxum, + /// Tokio JoinHandle for axum server's task + axum_join_handle: AxumJoinHandle, +} + +impl ShutdownHandles { + /// Cancellation token is given to Axum server constructor. When the token + /// receives a shutdown signal, Axum server will shut down gracefully. + pub(crate) fn new( + task_manager: TaskManager, + axum_server_handle: AxumJoinHandle, + shutdown_button: CancellationToken, + ) -> Self { + Self { + task_manager, + axum_shutdown_button: ShutdownAxum(shutdown_button.clone()), + axum_join_handle: axum_server_handle, + } + } + + pub(crate) fn task_manager_mut(&mut self) -> &mut TaskManager { + &mut self.task_manager + } + + /// Signal server to shut down, then return join handle to its + /// `tokio` task + /// + /// https://tikv.github.io/doc/tokio/task/struct.JoinHandle.html + #[must_use] + pub(crate) fn shutdown_axum(self) -> AxumJoinHandle { + self.axum_shutdown_button.0.cancel(); + self.axum_join_handle + } +} + +struct ShutdownAxum(CancellationToken); + +type AxumJoinHandle = JoinHandle>; + +#[derive(Clone)] +pub(crate) struct AppState { + pub(crate) nym_contract_cache: NymContractCache, + pub(crate) node_status_cache: NodeStatusCache, + pub(crate) circulating_supply_cache: CirculatingSupplyCache, + pub(crate) storage: storage::NymApiStorage, + pub(crate) described_nodes_cache: SharedCache, + pub(crate) network_details: NetworkDetails, + pub(crate) node_info_cache: unstable::NodeInfoCache, +} + +impl AppState { + pub(crate) fn nym_contract_cache(&self) -> &NymContractCache { + &self.nym_contract_cache + } + + pub(crate) fn node_status_cache(&self) -> &NodeStatusCache { + &self.node_status_cache + } + + pub(crate) fn circulating_supply_cache(&self) -> &CirculatingSupplyCache { + &self.circulating_supply_cache + } + + pub(crate) fn network_details(&self) -> &NetworkDetails { + &self.network_details + } + + pub(crate) fn described_nodes_cache(&self) -> &SharedCache { + &self.described_nodes_cache + } + + pub(crate) fn storage(&self) -> &storage::NymApiStorage { + &self.storage + } + + pub(crate) fn node_info_cache(&self) -> &unstable::NodeInfoCache { + &self.node_info_cache + } +} diff --git a/nym-api/src/support/http/unstable_routes.rs b/nym-api/src/support/http/unstable_routes.rs new file mode 100644 index 0000000000..7bc1a9644c --- /dev/null +++ b/nym-api/src/support/http/unstable_routes.rs @@ -0,0 +1,11 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::nym_nodes::handlers::unstable::nym_node_routes_unstable; +use crate::support::http::state::AppState; +use axum::Router; + +// as those get stabilised, they should get deprecated and use a redirection instead +pub(crate) fn unstable_routes() -> Router { + Router::new().nest("/nym-nodes", nym_node_routes_unstable()) +} diff --git a/nym-api/src/support/nyxd/mod.rs b/nym-api/src/support/nyxd/mod.rs index 69f9a49360..46ae0c67dc 100644 --- a/nym-api/src/support/nyxd/mod.rs +++ b/nym-api/src/support/nyxd/mod.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::ecash::error::EcashError; -use crate::epoch_operations::MixnodeWithPerformance; +use crate::epoch_operations::RewardedNodeWithParams; use crate::support::config::Config; use anyhow::Result; use async_trait::async_trait; @@ -24,14 +24,16 @@ use nym_config::defaults::{ChainDetails, NymNetworkDetails}; use nym_dkg::Threshold; use nym_ecash_contract_common::blacklist::BlacklistedAccountResponse; use nym_ecash_contract_common::deposit::{DepositId, DepositResponse}; -use nym_mixnet_contract_common::families::FamilyHead; +use nym_mixnet_contract_common::gateway::PreassignedId; use nym_mixnet_contract_common::mixnode::MixNodeDetails; +use nym_mixnet_contract_common::nym_node::Role; use nym_mixnet_contract_common::reward_params::RewardingParams; use nym_mixnet_contract_common::{ - CurrentIntervalResponse, EpochStatus, ExecuteMsg, GatewayBond, IdentityKey, LayerAssignment, - MixId, RewardedSetNodeStatus, + CurrentIntervalResponse, EpochStatus, ExecuteMsg, GatewayBond, IdentityKey, NymNodeDetails, + RewardedSet, RoleAssignment, }; use nym_validator_client::coconut::EcashApiError; +use nym_validator_client::nyxd::contract_traits::mixnet_query_client::MixnetQueryClientExt; use nym_validator_client::nyxd::contract_traits::PagedDkgQueryClient; use nym_validator_client::nyxd::error::NyxdError; use nym_validator_client::nyxd::{ @@ -213,6 +215,10 @@ impl Client { Ok(hash) } + pub(crate) async fn get_nymnodes(&self) -> Result, NyxdError> { + nyxd_query!(self, get_all_nymnodes_detailed().await) + } + pub(crate) async fn get_mixnodes(&self) -> Result, NyxdError> { nyxd_query!(self, get_all_mixnodes_detailed().await) } @@ -221,6 +227,10 @@ impl Client { nyxd_query!(self, get_all_gateways().await) } + pub(crate) async fn get_gateway_ids(&self) -> Result, NyxdError> { + nyxd_query!(self, get_all_preassigned_gateway_ids().await) + } + pub(crate) async fn get_current_interval(&self) -> Result { nyxd_query!(self, get_current_interval_details().await) } @@ -235,10 +245,8 @@ impl Client { nyxd_query!(self, get_rewarding_parameters().await) } - pub(crate) async fn get_rewarded_set_mixnodes( - &self, - ) -> Result, NyxdError> { - nyxd_query!(self, get_all_rewarded_set_mixnodes().await) + pub(crate) async fn get_rewarded_set_nodes(&self) -> Result { + nyxd_query!(self, get_rewarded_set().await) } pub(crate) async fn get_current_vesting_account_storage_key(&self) -> Result { @@ -266,13 +274,6 @@ impl Client { ) -> Result, NyxdError> { nyxd_query!(self, get_all_accounts_vesting_coins().await) } - - pub(crate) async fn get_all_family_members( - &self, - ) -> Result, NyxdError> { - nyxd_query!(self, get_all_family_members().await) - } - pub(crate) async fn get_pending_events_count(&self) -> Result { let pending = nyxd_query!(self, get_number_of_pending_events().await?); Ok(pending.epoch_events + pending.interval_events) @@ -283,29 +284,21 @@ impl Client { Ok(()) } + fn generate_reward_messages( + &self, + rewarded_set: &[RewardedNodeWithParams], + ) -> Vec<(ExecuteMsg, Vec)> { + rewarded_set + .iter() + .map(|node| (*node).into()) + .zip(std::iter::repeat(Vec::new())) + .collect() + } + pub(crate) async fn send_rewarding_messages( &self, - nodes: &[MixnodeWithPerformance], + rewarded_set: &[RewardedNodeWithParams], ) -> Result<(), NyxdError> { - // for some reason, compiler complains if this is explicitly inline in code ¯\_(ツ)_/¯ - #[inline] - #[allow(unused_variables)] - fn generate_reward_messages( - eligible_mixnodes: &[MixnodeWithPerformance], - ) -> Vec<(ExecuteMsg, Vec)> { - cfg_if::cfg_if! { - if #[cfg(feature = "no-reward")] { - vec![] - } else { - eligible_mixnodes - .iter() - .map(|node| (*node).into()) - .zip(std::iter::repeat(Vec::new())) - .collect() - } - } - } - // the expect is fine as we always construct the client with the mixnet contract explicitly set let mixnet_contract = nyxd_query!( self, @@ -314,7 +307,7 @@ impl Client { .clone() ); - let msgs = generate_reward_messages(nodes); + let msgs = self.generate_reward_messages(rewarded_set); // "technically" we don't need a write access to the client, // but we REALLY don't want to accidentally send any transactions while we're sending rewarding messages @@ -325,21 +318,65 @@ impl Client { &mixnet_contract, msgs, Default::default(), - format!("rewarding {} mixnodes", nodes.len()), + format!("rewarding {} nodes", rewarded_set.len()), ) .await? ); Ok(()) } - pub(crate) async fn advance_current_epoch( + fn generate_role_assignment_messages( &self, - new_rewarded_set: Vec, - expected_active_set_size: u32, + rewarded_set: RewardedSet, + ) -> Vec<(ExecuteMsg, Vec)> { + // currently we just assign all of them together, + // but the contract is ready to handle them separately should we need it + // if the tx is too big + let mut msgs = Vec::new(); + for (role, nodes) in [ + (Role::ExitGateway, rewarded_set.exit_gateways), + (Role::EntryGateway, rewarded_set.entry_gateways), + (Role::Layer1, rewarded_set.layer1), + (Role::Layer2, rewarded_set.layer2), + (Role::Layer3, rewarded_set.layer3), + (Role::Standby, rewarded_set.standby), + ] { + msgs.push(( + ExecuteMsg::AssignRoles { + assignment: RoleAssignment { role, nodes }, + }, + Vec::new(), + )); + } + msgs + } + + pub(crate) async fn send_role_assignment_messages( + &self, + rewarded_set: RewardedSet, ) -> Result<(), NyxdError> { + // the expect is fine as we always construct the client with the mixnet contract explicitly set + let mixnet_contract = nyxd_query!( + self, + mixnet_contract_address() + .expect("mixnet contract address is not available") + .clone() + ); + + let msgs = self.generate_role_assignment_messages(rewarded_set); + + // "technically" we don't need a write access to the client, + // but we REALLY don't want to accidentally send any transactions while we're sending rewarding messages + // as that would have messed up sequence numbers nyxd_signing!( self, - advance_current_epoch(new_rewarded_set, expected_active_set_size, None).await? + execute_multiple( + &mixnet_contract, + msgs, + Default::default(), + "assigning all the rewarded set roles", + ) + .await? ); Ok(()) } diff --git a/nym-api/src/support/storage/manager.rs b/nym-api/src/support/storage/manager.rs index a7950e3252..392efa6af4 100644 --- a/nym-api/src/support/storage/manager.rs +++ b/nym-api/src/support/storage/manager.rs @@ -1,14 +1,17 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::node_status_api::models::{HistoricalUptime, Uptime}; + +use crate::node_status_api::models::{HistoricalUptime as ApiHistoricalUptime, Uptime}; use crate::node_status_api::utils::{ActiveGatewayStatuses, ActiveMixnodeStatuses}; use crate::support::storage::models::{ - ActiveGateway, ActiveMixnode, GatewayDetails, MixnodeDetails, NodeStatus, RewardingReport, - TestedGatewayStatus, TestedMixnodeStatus, TestingRoute, + ActiveGateway, ActiveMixnode, GatewayDetails, HistoricalUptime, MixnodeDetails, NodeStatus, + RewardingReport, TestedGatewayStatus, TestedMixnodeStatus, TestingRoute, }; -use nym_mixnet_contract_common::{EpochId, IdentityKey, MixId}; -use nym_types::monitoring::{GatewayResult, MixnodeResult, NodeResult}; -use time::OffsetDateTime; +use nym_mixnet_contract_common::{EpochId, IdentityKey, NodeId}; +use nym_types::monitoring::NodeResult; +use sqlx::FromRow; +use time::Date; +use tracing::info; #[derive(Clone)] pub(crate) struct StorageManager { @@ -16,12 +19,12 @@ pub(crate) struct StorageManager { } pub struct AvgMixnodeReliability { - mix_id: MixId, + mix_id: NodeId, value: Option, } impl AvgMixnodeReliability { - pub fn mix_id(&self) -> MixId { + pub fn mix_id(&self) -> NodeId { self.mix_id } @@ -30,14 +33,15 @@ impl AvgMixnodeReliability { } } +#[derive(FromRow)] pub struct AvgGatewayReliability { - identity: String, + node_id: NodeId, value: Option, } impl AvgGatewayReliability { - pub fn identity(&self) -> &str { - &self.identity + pub fn node_id(&self) -> NodeId { + self.node_id } pub fn value(&self) -> f32 { @@ -50,9 +54,9 @@ impl StorageManager { pub(crate) async fn get_mixnode_mix_ids_by_identity( &self, identity: &str, - ) -> Result, sqlx::Error> { + ) -> Result, sqlx::Error> { let ids = sqlx::query!( - r#"SELECT mix_id as "mix_id: MixId" FROM mixnode_details WHERE identity_key = ?"#, + r#"SELECT mix_id as "mix_id: NodeId" FROM mixnode_details WHERE identity_key = ?"#, identity ) .fetch_all(&self.connection_pool) @@ -91,7 +95,7 @@ impl StorageManager { AvgMixnodeReliability, r#" SELECT - d.mix_id as "mix_id: MixId", + d.mix_id as "mix_id: NodeId", AVG(s.reliability) as "value: f32" FROM mixnode_details d @@ -115,11 +119,12 @@ impl StorageManager { start_ts_secs: i64, end_ts_secs: i64, ) -> Result, sqlx::Error> { - let result = sqlx::query_as!( - AvgGatewayReliability, + // we can't use `query_as!` macro because we don't apply all required table changes during sqlx migrations. + // some (like v3 directory) happens at runtime + let result = sqlx::query_as( r#" SELECT - d.identity as "identity: String", + d.node_id as "node_id: NodeId", CASE WHEN count(*) > 3 THEN AVG(reliability) ELSE 100 END as "value: f32" FROM gateway_details d @@ -130,9 +135,9 @@ impl StorageManager { timestamp <= ? GROUP BY 1 "#, - start_ts_secs, - end_ts_secs ) + .bind(start_ts_secs) + .bind(end_ts_secs) .fetch_all(&self.connection_pool) .await?; Ok(result) @@ -145,7 +150,7 @@ impl StorageManager { /// * `mix_id`: mix-id (as assigned by the smart contract) of the mixnode. pub(crate) async fn get_mixnode_database_id( &self, - mix_id: MixId, + mix_id: NodeId, ) -> Result, sqlx::Error> { let id = sqlx::query!("SELECT id FROM mixnode_details WHERE mix_id = ?", mix_id) .fetch_optional(&self.connection_pool) @@ -155,12 +160,23 @@ impl StorageManager { Ok(id) } + pub(crate) async fn get_gateway_database_id( + &self, + node_id: NodeId, + ) -> Result, sqlx::Error> { + let id = sqlx::query!("SELECT id FROM gateway_details WHERE node_id = ?", node_id) + .fetch_optional(&self.connection_pool) + .await? + .map(|row| row.id); + + Ok(id) + } + /// Tries to obtain row id of given gateway given its identity - /// - /// # Arguments - /// - /// * `identity`: identity (base58-encoded public key) of the gateway. - pub(crate) async fn get_gateway_id(&self, identity: &str) -> Result, sqlx::Error> { + pub(crate) async fn get_gateway_database_id_by_identity( + &self, + identity: &str, + ) -> Result, sqlx::Error> { let id = sqlx::query!( "SELECT id FROM gateway_details WHERE identity = ?", identity @@ -172,21 +188,34 @@ impl StorageManager { Ok(id) } - /// Tries to obtain owner value of given mixnode given its mix_id - /// - /// # Arguments - /// - /// * `mix_id`: mix-id (as assigned by the smart contract) of the mixnode. - pub(crate) async fn get_mixnode_owner( + pub(crate) async fn get_gateway_node_id_from_identity_key( &self, - mix_id: MixId, - ) -> Result, sqlx::Error> { - let owner = sqlx::query!("SELECT owner FROM mixnode_details WHERE mix_id = ?", mix_id) - .fetch_optional(&self.connection_pool) - .await? - .map(|row| row.owner); + identity: &str, + ) -> Result, sqlx::Error> { + let node_id = sqlx::query!( + r#"SELECT node_id as "node_id: NodeId" FROM gateway_details WHERE identity = ?"#, + identity + ) + .fetch_optional(&self.connection_pool) + .await? + .map(|row| row.node_id); - Ok(owner) + Ok(node_id) + } + + pub(crate) async fn get_gateway_identity_key( + &self, + node_id: NodeId, + ) -> Result, sqlx::Error> { + let identity_key = sqlx::query!( + "SELECT identity FROM gateway_details WHERE node_id = ?", + node_id + ) + .fetch_optional(&self.connection_pool) + .await? + .map(|row| row.identity); + + Ok(identity_key) } /// Tries to obtain identity value of given mixnode given its mix_id @@ -196,7 +225,7 @@ impl StorageManager { /// * `mix_id`: mix-id (as assigned by the smart contract) of the mixnode. pub(crate) async fn get_mixnode_identity_key( &self, - mix_id: MixId, + mix_id: NodeId, ) -> Result, sqlx::Error> { let identity_key = sqlx::query!( "SELECT identity_key FROM mixnode_details WHERE mix_id = ?", @@ -209,26 +238,6 @@ impl StorageManager { Ok(identity_key) } - /// Tries to obtain owner value of given gateway given its identity - /// - /// # Arguments - /// - /// * `identity`: identity (base58-encoded public key) of the gateway. - pub(crate) async fn get_gateway_owner( - &self, - identity: &str, - ) -> Result, sqlx::Error> { - let owner = sqlx::query!( - "SELECT owner FROM gateway_details WHERE identity = ?", - identity - ) - .fetch_optional(&self.connection_pool) - .await? - .map(|row| row.owner); - - Ok(owner) - } - /// Gets all reliability statuses for mixnode with particular identity that were inserted /// into the database after the specified unix timestamp. /// @@ -238,7 +247,7 @@ impl StorageManager { /// * `timestamp`: unix timestamp of the lower bound of the selection. pub(crate) async fn get_mixnode_statuses_since( &self, - mix_id: MixId, + mix_id: NodeId, timestamp: i64, ) -> Result, sqlx::Error> { sqlx::query_as!( @@ -266,7 +275,7 @@ impl StorageManager { /// * `timestamp`: unix timestamp of the lower bound of the selection. pub(crate) async fn get_gateway_statuses_since( &self, - identity: &str, + node_id: NodeId, timestamp: i64, ) -> Result, sqlx::Error> { sqlx::query_as!( @@ -276,9 +285,9 @@ impl StorageManager { FROM gateway_status JOIN gateway_details ON gateway_status.gateway_details_id = gateway_details.id - WHERE gateway_details.identity=? AND gateway_status.timestamp > ?; + WHERE gateway_details.node_id=? AND gateway_status.timestamp > ?; "#, - identity, + node_id, timestamp, ) .fetch_all(&self.connection_pool) @@ -292,16 +301,16 @@ impl StorageManager { /// * `mix_id`: mix-id (as assigned by the smart contract) of the mixnode. pub(crate) async fn get_mixnode_historical_uptimes( &self, - mix_id: MixId, - ) -> Result, sqlx::Error> { + mix_id: NodeId, + ) -> Result, sqlx::Error> { let uptimes = sqlx::query!( r#" SELECT date, uptime - FROM mixnode_historical_uptime - JOIN mixnode_details - ON mixnode_historical_uptime.mixnode_details_id = mixnode_details.id - WHERE mixnode_details.mix_id = ? - ORDER BY date ASC + FROM mixnode_historical_uptime + JOIN mixnode_details + ON mixnode_historical_uptime.mixnode_details_id = mixnode_details.id + WHERE mixnode_details.mix_id = ? + ORDER BY date ASC "#, mix_id ) @@ -312,7 +321,7 @@ impl StorageManager { // better safe than sorry and not use an unwrap) .filter_map(|row| { Uptime::try_from(row.uptime.unwrap_or_default()) - .map(|uptime| HistoricalUptime { + .map(|uptime| ApiHistoricalUptime { date: row.date.unwrap_or_default(), uptime, }) @@ -330,18 +339,18 @@ impl StorageManager { /// * `identity`: identity (base58-encoded public key) of the gateway. pub(crate) async fn get_gateway_historical_uptimes( &self, - identity: &str, - ) -> Result, sqlx::Error> { + node_id: NodeId, + ) -> Result, sqlx::Error> { let uptimes = sqlx::query!( r#" SELECT date, uptime - FROM gateway_historical_uptime - JOIN gateway_details - ON gateway_historical_uptime.gateway_details_id = gateway_details.id - WHERE gateway_details.identity = ? - ORDER BY date ASC + FROM gateway_historical_uptime + JOIN gateway_details + ON gateway_historical_uptime.gateway_details_id = gateway_details.id + WHERE gateway_details.node_id = ? + ORDER BY date ASC "#, - identity + node_id ) .fetch_all(&self.connection_pool) .await? @@ -350,7 +359,7 @@ impl StorageManager { // better safe than sorry and not use an unwrap) .filter_map(|row| { Uptime::try_from(row.uptime.unwrap_or_default()) - .map(|uptime| HistoricalUptime { + .map(|uptime| ApiHistoricalUptime { date: row.date.unwrap_or_default(), uptime, }) @@ -361,6 +370,54 @@ impl StorageManager { Ok(uptimes) } + pub(crate) async fn get_historical_mix_uptime_on( + &self, + contract_node_id: i64, + date: Date, + ) -> Result, sqlx::Error> { + sqlx::query_as!( + HistoricalUptime, + r#" + SELECT date as "date!: Date", uptime as "uptime!" + FROM mixnode_historical_uptime + JOIN mixnode_details + ON mixnode_historical_uptime.mixnode_details_id = mixnode_details.id + WHERE + mixnode_details.mix_id = ? + AND + mixnode_historical_uptime.date = ? + "#, + contract_node_id, + date + ) + .fetch_optional(&self.connection_pool) + .await + } + + pub(crate) async fn get_historical_gateway_uptime_on( + &self, + contract_node_id: i64, + date: Date, + ) -> Result, sqlx::Error> { + sqlx::query_as!( + HistoricalUptime, + r#" + SELECT date as "date!: Date", uptime as "uptime!" + FROM gateway_historical_uptime + JOIN gateway_details + ON gateway_historical_uptime.gateway_details_id = gateway_details.id + WHERE + gateway_details.node_id = ? + AND + gateway_historical_uptime.date = ? + "#, + contract_node_id, + date + ) + .fetch_optional(&self.connection_pool) + .await + } + /// Gets all reliability statuses for mixnode with particular id that were inserted /// into the database within the specified time interval. /// @@ -451,7 +508,7 @@ impl StorageManager { /// /// * `since`: unix timestamp indicating the lower bound interval of the selection. /// * `until`: unix timestamp indicating the upper bound interval of the selection. - pub(crate) async fn get_gateway_statuses_by_id( + pub(crate) async fn get_gateway_statuses_by_database_id( &self, id: i64, since: i64, @@ -481,7 +538,7 @@ impl StorageManager { pub(crate) async fn submit_mixnode_statuses( &self, timestamp: i64, - mixnode_results: Vec, + mixnode_results: Vec, ) -> Result<(), sqlx::Error> { // insert it all in a transaction to make sure all nodes are updated at the same time // (plus it's a nice guard against new nodes) @@ -489,13 +546,12 @@ impl StorageManager { for mixnode_result in mixnode_results { let mixnode_id = sqlx::query!( r#" - INSERT OR IGNORE INTO mixnode_details(mix_id, identity_key, owner) VALUES (?, ?, ?); + INSERT OR IGNORE INTO mixnode_details(mix_id, identity_key) VALUES (?, ?); SELECT id FROM mixnode_details WHERE mix_id = ?; "#, - mixnode_result.mix_id, + mixnode_result.node_id, mixnode_result.identity, - mixnode_result.owner, - mixnode_result.mix_id, + mixnode_result.node_id, ) .fetch_one(&mut tx) .await? @@ -524,39 +580,41 @@ impl StorageManager { ) -> Result<(), sqlx::Error> { info!("Inserting {} mixnode statuses", mixnode_results.len()); - let timestamp = OffsetDateTime::now_utc().unix_timestamp(); - // insert it all in a transaction to make sure all nodes are updated at the same time - // (plus it's a nice guard against new nodes) - let mut tx = self.connection_pool.begin().await?; - for mixnode_result in mixnode_results { - let mixnode_id = sqlx::query!( - r#" - INSERT OR IGNORE INTO mixnode_details_v2(node_id, identity_key) VALUES (?, ?); - SELECT id FROM mixnode_details_v2 WHERE node_id = ?; - "#, - mixnode_result.node_id, - mixnode_result.identity, - mixnode_result.node_id, - ) - .fetch_one(&mut tx) - .await? - .id; - - // insert the actual status - sqlx::query!( - r#" - INSERT INTO mixnode_status_v2 (mixnode_details_id, reliability, timestamp) VALUES (?, ?, ?); - "#, - mixnode_id, - mixnode_result.reliability, - timestamp - ) - .execute(&mut tx) - .await?; - } - - // finally commit the transaction - tx.commit().await + todo!("look at what broke during rebasing"); + // + // let timestamp = OffsetDateTime::now_utc().unix_timestamp(); + // // insert it all in a transaction to make sure all nodes are updated at the same time + // // (plus it's a nice guard against new nodes) + // let mut tx = self.connection_pool.begin().await?; + // for mixnode_result in mixnode_results { + // let mixnode_id = sqlx::query!( + // r#" + // INSERT OR IGNORE INTO mixnode_details_v2(node_id, identity_key) VALUES (?, ?); + // SELECT id FROM mixnode_details_v2 WHERE node_id = ?; + // "#, + // mixnode_result.node_id, + // mixnode_result.identity, + // mixnode_result.node_id, + // ) + // .fetch_one(&mut tx) + // .await? + // .id; + // + // // insert the actual status + // sqlx::query!( + // r#" + // INSERT INTO mixnode_status_v2 (mixnode_details_id, reliability, timestamp) VALUES (?, ?, ?); + // "#, + // mixnode_id, + // mixnode_result.reliability, + // timestamp + // ) + // .execute(&mut tx) + // .await?; + // } + // + // // finally commit the transaction + // tx.commit().await } /// Tries to submit gateway [`NodeResult`] from the network monitor to the database. @@ -568,7 +626,7 @@ impl StorageManager { pub(crate) async fn submit_gateway_statuses( &self, timestamp: i64, - gateway_results: Vec, + gateway_results: Vec, ) -> Result<(), sqlx::Error> { // insert it all in a transaction to make sure all nodes are updated at the same time // (plus it's a nice guard against new nodes) @@ -580,11 +638,11 @@ impl StorageManager { // same ID "problem" as described for mixnode insertion let gateway_id = sqlx::query!( r#" - INSERT OR IGNORE INTO gateway_details(identity, owner) VALUES (?, ?); + INSERT OR IGNORE INTO gateway_details(node_id, identity) VALUES (?, ?); SELECT id FROM gateway_details WHERE identity = ?; "#, + gateway_result.node_id, gateway_result.identity, - gateway_result.owner, gateway_result.identity, ) .fetch_one(&mut tx) @@ -614,43 +672,45 @@ impl StorageManager { ) -> Result<(), sqlx::Error> { info!("Inserting {} gateway statuses", gateway_results.len()); - let timestamp = OffsetDateTime::now_utc().unix_timestamp(); - // insert it all in a transaction to make sure all nodes are updated at the same time - // (plus it's a nice guard against new nodes) - let mut tx = self.connection_pool.begin().await?; + todo!("look at what broke during rebasing"); - for gateway_result in gateway_results { - // if gateway info doesn't exist, insert it and get its id - - // same ID "problem" as described for mixnode insertion - let gateway_id = sqlx::query!( - r#" - INSERT OR IGNORE INTO gateway_details_v2(identity, node_id) VALUES (?, ?); - SELECT id FROM gateway_details_v2 WHERE identity = ?; - "#, - gateway_result.identity, - gateway_result.node_id, - gateway_result.identity, - ) - .fetch_one(&mut tx) - .await? - .id; - - // insert the actual status - sqlx::query!( - r#" - INSERT INTO gateway_status_v2 (gateway_details_id, reliability, timestamp) VALUES (?, ?, ?); - "#, - gateway_id, - gateway_result.reliability, - timestamp - ) - .execute(&mut tx) - .await?; - } - - // finally commit the transaction - tx.commit().await + // let timestamp = OffsetDateTime::now_utc().unix_timestamp(); + // // insert it all in a transaction to make sure all nodes are updated at the same time + // // (plus it's a nice guard against new nodes) + // let mut tx = self.connection_pool.begin().await?; + // + // for gateway_result in gateway_results { + // // if gateway info doesn't exist, insert it and get its id + // + // // same ID "problem" as described for mixnode insertion + // let gateway_id = sqlx::query!( + // r#" + // INSERT OR IGNORE INTO gateway_details_v2(identity, node_id) VALUES (?, ?); + // SELECT id FROM gateway_details_v2 WHERE identity = ?; + // "#, + // gateway_result.identity, + // gateway_result.node_id, + // gateway_result.identity, + // ) + // .fetch_one(&mut tx) + // .await? + // .id; + // + // // insert the actual status + // sqlx::query!( + // r#" + // INSERT INTO gateway_status_v2 (gateway_details_id, reliability, timestamp) VALUES (?, ?, ?); + // "#, + // gateway_id, + // gateway_result.reliability, + // timestamp + // ) + // .execute(&mut tx) + // .await?; + // } + // + // // finally commit the transaction + // tx.commit().await } /// Saves the information about which nodes were used as core nodes during this particular @@ -802,13 +862,13 @@ impl StorageManager { /// * `uptime`: the actual uptime of the node during the specified day. pub(crate) async fn insert_gateway_historical_uptime( &self, - mix_id: i64, + db_id: i64, date: &str, uptime: u8, ) -> Result<(), sqlx::Error> { sqlx::query!( "INSERT INTO gateway_historical_uptime(gateway_details_id, date, uptime) VALUES (?, ?, ?)", - mix_id, + db_id, date, uptime, ).execute(&self.connection_pool).await?; @@ -901,7 +961,7 @@ impl StorageManager { sqlx::query_as!( ActiveMixnode, r#" - SELECT DISTINCT identity_key, mix_id as "mix_id: MixId", owner, id + SELECT DISTINCT identity_key, mix_id as "mix_id: NodeId", id FROM mixnode_details JOIN mixnode_status ON mixnode_details.id = mixnode_status.mixnode_details_id @@ -928,10 +988,9 @@ impl StorageManager { since: i64, until: i64, ) -> Result, sqlx::Error> { - sqlx::query_as!( - ActiveGateway, + sqlx::query_as( r#" - SELECT DISTINCT identity, owner, id + SELECT DISTINCT identity, node_id as "node_id: NodeId", id FROM gateway_details JOIN gateway_status ON gateway_details.id = gateway_status.gateway_details_id @@ -939,9 +998,9 @@ impl StorageManager { SELECT 1 FROM gateway_status WHERE timestamp > ? AND timestamp < ? ) "#, - since, - until, ) + .bind(since) + .bind(until) .fetch_all(&self.connection_pool) .await } @@ -1032,7 +1091,6 @@ impl StorageManager { let statuses = ActiveMixnodeStatuses { mix_id: active_node.mix_id, identity: active_node.identity_key, - owner: active_node.owner, statuses, }; @@ -1060,12 +1118,12 @@ impl StorageManager { let mut active_day_statuses = Vec::with_capacity(active_nodes.len()); for active_node in active_nodes.into_iter() { let statuses = self - .get_gateway_statuses_by_id(active_node.id, since, until) + .get_gateway_statuses_by_database_id(active_node.id, since, until) .await?; let statuses = ActiveGatewayStatuses { + node_id: active_node.node_id, identity: active_node.identity, - owner: active_node.owner, statuses, }; @@ -1092,13 +1150,12 @@ impl StorageManager { &self, id: i64, ) -> Result, sqlx::Error> { - sqlx::query_as!( - GatewayDetails, - "SELECT * FROM gateway_details WHERE id = ?", - id - ) - .fetch_optional(&self.connection_pool) - .await + // we can't use `query_as!` macro because we don't apply all required table changes during sqlx migrations. + // some (like v3 directory) happens at runtime + sqlx::query_as("SELECT * FROM gateway_details WHERE id = ?") + .bind(id) + .fetch_optional(&self.connection_pool) + .await } pub(crate) async fn get_mixnode_statuses_count(&self, db_id: i64) -> Result { @@ -1119,7 +1176,7 @@ impl StorageManager { pub(crate) async fn get_mixnode_statuses( &self, - mix_id: MixId, + mix_id: NodeId, limit: u32, offset: u32, ) -> Result, sqlx::Error> { @@ -1204,3 +1261,86 @@ impl StorageManager { .await } } + +pub(crate) mod v3_migration { + use crate::support::storage::manager::StorageManager; + use crate::support::storage::models::GatewayDetailsBeforeMigration; + use nym_mixnet_contract_common::NodeId; + + impl StorageManager { + pub(crate) async fn check_v3_migration(&self) -> Result { + sqlx::query!("SELECT EXISTS (SELECT 1 FROM v3_migration_info) AS 'exists'",) + .fetch_one(&self.connection_pool) + .await + .map(|result| result.exists == 1) + } + + pub(crate) async fn set_v3_migration_completion(&self) -> Result<(), sqlx::Error> { + sqlx::query!("INSERT INTO v3_migration_info(id) VALUES (0)") + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn get_all_known_gateways( + &self, + ) -> Result, sqlx::Error> { + sqlx::query_as("SELECT * FROM gateway_details") + .fetch_all(&self.connection_pool) + .await + } + + pub(crate) async fn set_gateway_node_id( + &self, + identity: &str, + node_id: NodeId, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "UPDATE gateway_details SET node_id = ? WHERE identity = ?", + node_id, + identity + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn purge_gateway(&self, db_id: i64) -> Result<(), sqlx::Error> { + sqlx::query!( + r#" + DELETE FROM gateway_historical_uptime WHERE gateway_details_id = ?; + DELETE FROM gateway_status WHERE gateway_details_id = ?; + DELETE FROM testing_route WHERE gateway_id = ?; + DELETE FROM gateway_details WHERE id = ?; + "#, + db_id, + db_id, + db_id, + db_id, + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn make_node_id_not_null(&self) -> Result<(), sqlx::Error> { + sqlx::query( + r#" + CREATE TABLE gateway_details_temp + ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + node_id INTEGER NOT NULL UNIQUE, + identity VARCHAR NOT NULL UNIQUE + ); + + INSERT INTO gateway_details_temp SELECT * FROM gateway_details; + DROP TABLE gateway_details; + ALTER TABLE gateway_details_temp RENAME TO gateway_details; + "#, + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + } +} diff --git a/nym-api/src/support/storage/mod.rs b/nym-api/src/support/storage/mod.rs index d4cdc64c02..e647adf7d0 100644 --- a/nym-api/src/support/storage/mod.rs +++ b/nym-api/src/support/storage/mod.rs @@ -3,21 +3,21 @@ use crate::network_monitor::test_route::TestRoute; use crate::node_status_api::models::{ - GatewayStatusReport, GatewayUptimeHistory, MixnodeStatusReport, MixnodeUptimeHistory, - NymApiStorageError, Uptime, + GatewayStatusReport, GatewayUptimeHistory, HistoricalUptime as ApiHistoricalUptime, + MixnodeStatusReport, MixnodeUptimeHistory, NymApiStorageError, Uptime, }; use crate::node_status_api::{ONE_DAY, ONE_HOUR}; use crate::storage::manager::StorageManager; use crate::storage::models::{NodeStatus, TestingRoute}; use crate::support::storage::models::{ - GatewayDetails, MixnodeDetails, TestedGatewayStatus, TestedMixnodeStatus, + GatewayDetails, HistoricalUptime, MixnodeDetails, TestedGatewayStatus, TestedMixnodeStatus, }; -use nym_mixnet_contract_common::MixId; -use nym_types::monitoring::{GatewayResult, MixnodeResult}; -use rocket::fairing::AdHoc; +use nym_mixnet_contract_common::NodeId; +use nym_types::monitoring::NodeResult; use sqlx::ConnectOptions; use std::path::Path; -use time::OffsetDateTime; +use time::{Date, OffsetDateTime}; +use tracing::{error, info, warn}; use self::manager::{AvgGatewayReliability, AvgMixnodeReliability}; @@ -64,18 +64,11 @@ impl NymApiStorage { Ok(storage) } - #[deprecated(note = "TODO rocket: obsolete because it's used for Rocket")] - pub(crate) fn stage(storage: NymApiStorage) -> AdHoc { - AdHoc::try_on_ignite("SQLx Database", |rocket| async { - Ok(rocket.manage(storage)) - }) - } - #[allow(unused)] pub(crate) async fn mix_identity_to_mix_ids( &self, identity: &str, - ) -> Result, NymApiStorageError> { + ) -> Result, NymApiStorageError> { Ok(self .manager .get_mixnode_mix_ids_by_identity(identity) @@ -86,7 +79,7 @@ impl NymApiStorage { pub(crate) async fn mix_identity_to_latest_mix_id( &self, identity: &str, - ) -> Result, NymApiStorageError> { + ) -> Result, NymApiStorageError> { Ok(self .mix_identity_to_mix_ids(identity) .await? @@ -127,7 +120,7 @@ impl NymApiStorage { /// * `since`: unix timestamp indicating the lower bound interval of the selection. async fn get_mixnode_statuses( &self, - mix_id: MixId, + mix_id: NodeId, since: i64, ) -> Result, NymApiStorageError> { let statuses = self @@ -143,16 +136,15 @@ impl NymApiStorage { /// /// # Arguments /// - /// * `identity`: identity key of the gateway to query. /// * `since`: unix timestamp indicating the lower bound interval of the selection. async fn get_gateway_statuses( &self, - identity: &str, + node_id: NodeId, since: i64, ) -> Result, NymApiStorageError> { let statuses = self .manager - .get_gateway_statuses_since(identity, since) + .get_gateway_statuses_since(node_id, since) .await?; Ok(statuses) @@ -165,7 +157,7 @@ impl NymApiStorage { /// * `mix_id`: mix-id (as assigned by the smart contract) of the mixnode. pub(crate) async fn construct_mixnode_report( &self, - mix_id: MixId, + mix_id: NodeId, ) -> Result { let now = OffsetDateTime::now_utc(); let day_ago = (now - ONE_DAY).unix_timestamp(); @@ -186,21 +178,14 @@ impl NymApiStorage { .get_monitor_runs_count(day_ago, now.unix_timestamp()) .await?; - let mixnode_owner = - self.manager.get_mixnode_owner(mix_id).await?.expect( - "The node doesn't have an owner even though we have status information on it!", - ); - - let mixnode_identity = - self.manager.get_mixnode_identity_key(mix_id).await?.expect( - "The node doesn't have an owner even though we have status information on it!", - ); + let mixnode_identity = self.manager.get_mixnode_identity_key(mix_id).await?.expect( + "The node doesn't have an identity even though we have status information on it!", + ); Ok(MixnodeStatusReport::construct_from_last_day_reports( now, mix_id, mixnode_identity, - mixnode_owner, statuses, last_hour_runs_count, last_day_runs_count, @@ -209,19 +194,17 @@ impl NymApiStorage { pub(crate) async fn construct_gateway_report( &self, - identity: &str, + node_id: NodeId, ) -> Result { let now = OffsetDateTime::now_utc(); let day_ago = (now - ONE_DAY).unix_timestamp(); let hour_ago = (now - ONE_HOUR).unix_timestamp(); - let statuses = self.get_gateway_statuses(identity, day_ago).await?; + let statuses = self.get_gateway_statuses(node_id, day_ago).await?; // if we have no statuses, the node doesn't exist (or monitor is down), but either way, we can't make a report if statuses.is_empty() { - return Err(NymApiStorageError::GatewayReportNotFound { - identity: identity.to_owned(), - }); + return Err(NymApiStorageError::GatewayReportNotFound { node_id }); } // determine the number of runs the gateway should have been online for @@ -232,14 +215,18 @@ impl NymApiStorage { .get_monitor_runs_count(day_ago, now.unix_timestamp()) .await?; - let gateway_owner = self.manager.get_gateway_owner(identity).await?.expect( - "The gateway doesn't have an owner even though we have status information on it!", - ); + let gateway_identity = self + .manager + .get_gateway_identity_key(node_id) + .await? + .expect( + "The node doesn't have an identity even though we have status information on it!", + ); Ok(GatewayStatusReport::construct_from_last_day_reports( now, - identity.to_owned(), - gateway_owner, + node_id, + gateway_identity, statuses, last_hour_runs_count, last_day_runs_count, @@ -248,7 +235,7 @@ impl NymApiStorage { pub(crate) async fn get_mixnode_uptime_history( &self, - mix_id: MixId, + mix_id: NodeId, ) -> Result { let history = self.manager.get_mixnode_historical_uptimes(mix_id).await?; @@ -256,69 +243,123 @@ impl NymApiStorage { return Err(NymApiStorageError::MixnodeUptimeHistoryNotFound { mix_id }); } - let mixnode_owner = - self.manager.get_mixnode_owner(mix_id).await?.expect( - "The node doesn't have an owner even though we have uptime history for it!", - ); - let mixnode_identity = self.manager.get_mixnode_identity_key(mix_id).await?.expect( "The node doesn't have an identity even though we have uptime history for it!", ); - Ok(MixnodeUptimeHistory::new( - mix_id, - mixnode_identity, - mixnode_owner, + Ok(MixnodeUptimeHistory::new(mix_id, mixnode_identity, history)) + } + + pub(crate) async fn get_gateway_uptime_history_by_identity( + &self, + gateway_identity: &str, + ) -> Result { + let Some(node_id) = self + .manager + .get_gateway_node_id_from_identity_key(gateway_identity) + .await? + else { + return Err(NymApiStorageError::GatewayNotFound { + identity: gateway_identity.to_string(), + }); + }; + + let history = self.manager.get_gateway_historical_uptimes(node_id).await?; + + if history.is_empty() { + return Err(NymApiStorageError::GatewayUptimeHistoryNotFound { node_id }); + } + + Ok(GatewayUptimeHistory::new( + node_id, + gateway_identity, history, )) } - pub(crate) async fn get_gateway_uptime_history( + pub(crate) async fn get_node_uptime_history( &self, - identity: &str, - ) -> Result { - let history = self - .manager - .get_gateway_historical_uptimes(identity) - .await?; + node_id: NodeId, + ) -> Result, NymApiStorageError> { + let history = self.manager.get_mixnode_historical_uptimes(node_id).await?; - if history.is_empty() { - return Err(NymApiStorageError::GatewayUptimeHistoryNotFound { - identity: identity.to_owned(), - }); + if !history.is_empty() { + return Ok(history); } - let gateway_owner = - self.manager.get_gateway_owner(identity).await?.expect( - "The gateway doesn't have an owner even though we have uptime history for it!", - ); - - Ok(GatewayUptimeHistory::new( - identity.to_owned(), - gateway_owner, - history, - )) + Ok(self.manager.get_gateway_historical_uptimes(node_id).await?) } pub(crate) async fn get_average_mixnode_uptime_in_the_last_24hrs( &self, - mix_id: MixId, + node_id: NodeId, end_ts_secs: i64, ) -> Result { let start = end_ts_secs - 86400; - self.get_average_mixnode_uptime_in_time_interval(mix_id, start, end_ts_secs) - .await + let reliability = self + .get_average_mixnode_reliability_in_time_interval(node_id, start, end_ts_secs) + .await?; + Ok(Uptime::new(reliability)) } pub(crate) async fn get_average_gateway_uptime_in_the_last_24hrs( &self, - identity: &str, + node_id: NodeId, end_ts_secs: i64, ) -> Result { let start = end_ts_secs - 86400; - self.get_average_gateway_uptime_in_time_interval(identity, start, end_ts_secs) + let reliability = self + .get_average_gateway_reliability_in_time_interval(node_id, start, end_ts_secs) + .await?; + Ok(Uptime::new(reliability)) + } + + pub(crate) async fn get_average_node_uptime_in_the_last_24hrs( + &self, + node_id: NodeId, + end_ts_secs: i64, + ) -> Result { + let start = end_ts_secs - 86400; + self.get_average_node_reliability_in_time_interval(node_id, start, end_ts_secs) .await + .map(Uptime::new) + } + + pub(crate) async fn get_historical_mix_uptime_on( + &self, + node_id: NodeId, + date: Date, + ) -> Result, NymApiStorageError> { + Ok(self + .manager + .get_historical_mix_uptime_on(node_id as i64, date) + .await?) + } + + pub(crate) async fn get_historical_gateway_uptime_on( + &self, + node_id: NodeId, + date: Date, + ) -> Result, NymApiStorageError> { + Ok(self + .manager + .get_historical_gateway_uptime_on(node_id as i64, date) + .await?) + } + + pub(crate) async fn get_historical_node_uptime_on( + &self, + node_id: NodeId, + date: Date, + ) -> Result, NymApiStorageError> { + if let Ok(result_as_mix) = self.get_historical_mix_uptime_on(node_id, date).await { + if result_as_mix.is_some() { + return Ok(result_as_mix); + } + } + + self.get_historical_gateway_uptime_on(node_id, date).await } /// Based on the data available in the validator API, determines the average uptime of particular @@ -329,15 +370,16 @@ impl NymApiStorage { /// * `mix_id`: mix-id (as assigned by the smart contract) of the mixnode. /// * `since`: unix timestamp indicating the lower bound interval of the selection. /// * `end`: unix timestamp indicating the upper bound interval of the selection. - pub(crate) async fn get_average_mixnode_uptime_in_time_interval( + pub(crate) async fn get_average_mixnode_reliability_in_time_interval( &self, - mix_id: MixId, + mix_id: NodeId, start: i64, end: i64, - ) -> Result { + ) -> Result { + // those two should have been a single sql query /shrug let mixnode_database_id = match self.manager.get_mixnode_database_id(mix_id).await? { Some(id) => id, - None => return Ok(Uptime::zero()), + None => return Ok(0.), }; let reliability = self @@ -345,11 +387,7 @@ impl NymApiStorage { .get_mixnode_average_reliability_in_interval(mixnode_database_id, start, end) .await?; - if let Some(reliability) = reliability { - Ok(Uptime::new(reliability)) - } else { - Ok(Uptime::zero()) - } + Ok(reliability.unwrap_or_default()) } /// Based on the data available in the validator API, determines the average uptime of particular @@ -360,15 +398,16 @@ impl NymApiStorage { /// * `identity`: base58-encoded identity of the gateway. /// * `since`: unix timestamp indicating the lower bound interval of the selection. /// * `end`: unix timestamp indicating the upper bound interval of the selection. - pub(crate) async fn get_average_gateway_uptime_in_time_interval( + pub(crate) async fn get_average_gateway_reliability_in_time_interval( &self, - identity: &str, + node_id: NodeId, start: i64, end: i64, - ) -> Result { - let gateway_database_id = match self.manager.get_gateway_id(identity).await? { + ) -> Result { + // those two should have been a single sql query /shrug + let gateway_database_id = match self.manager.get_gateway_database_id(node_id).await? { Some(id) => id, - None => return Ok(Uptime::zero()), + None => return Ok(0.), }; let reliability = self @@ -376,11 +415,26 @@ impl NymApiStorage { .get_gateway_average_reliability_in_interval(gateway_database_id, start, end) .await?; - if let Some(reliability) = reliability { - Ok(Uptime::new(reliability)) - } else { - Ok(Uptime::zero()) + Ok(reliability.unwrap_or_default()) + } + + pub(crate) async fn get_average_node_reliability_in_time_interval( + &self, + node_id: NodeId, + start: i64, + end: i64, + ) -> Result { + if let Ok(result_as_mix) = self + .get_average_mixnode_reliability_in_time_interval(node_id, start, end) + .await + { + if result_as_mix != 0. { + return Ok(result_as_mix); + } } + + self.get_average_gateway_reliability_in_time_interval(node_id, start, end) + .await } /// Obtain status reports of mixnodes that were active in the specified time interval. @@ -416,7 +470,6 @@ impl NymApiStorage { OffsetDateTime::from_unix_timestamp(end).unwrap(), statuses.mix_id, statuses.identity, - statuses.owner, statuses.statuses, last_hour_runs_count, last_day_runs_count, @@ -458,8 +511,8 @@ impl NymApiStorage { .map(|statuses| { GatewayStatusReport::construct_from_last_day_reports( OffsetDateTime::from_unix_timestamp(end).unwrap(), + statuses.node_id, statuses.identity, - statuses.owner, statuses.statuses, last_hour_runs_count, last_day_runs_count, @@ -509,7 +562,7 @@ impl NymApiStorage { let gateway_db_id = self .manager - .get_gateway_id(&test_route.gateway().identity_key.to_base58_string()) + .get_gateway_database_id(test_route.gateway().node_id) .await? .ok_or_else(|| NymApiStorageError::DatabaseInconsistency { reason: format!( @@ -539,7 +592,7 @@ impl NymApiStorage { /// * `since`: optional unix timestamp indicating the lower bound interval of the selection. pub(crate) async fn get_core_mixnode_status_count( &self, - mix_id: MixId, + mix_id: NodeId, since: Option, ) -> Result { let db_id = self.manager.get_mixnode_database_id(mix_id).await?; @@ -565,12 +618,15 @@ impl NymApiStorage { /// /// * `identity`: identity (base58-encoded public key) of the gateway. /// * `since`: optional unix timestamp indicating the lower bound interval of the selection. - pub(crate) async fn get_core_gateway_status_count( + pub(crate) async fn get_core_gateway_status_count_by_identity( &self, identity: &str, since: Option, ) -> Result { - let node_id = self.manager.get_gateway_id(identity).await?; + let node_id = self + .manager + .get_gateway_database_id_by_identity(identity) + .await?; if let Some(node_id) = node_id { let since = since @@ -595,8 +651,8 @@ impl NymApiStorage { /// * `route_results`: pub(crate) async fn insert_monitor_run_results( &self, - mixnode_results: Vec, - gateway_results: Vec, + mixnode_results: Vec, + gateway_results: Vec, test_routes: Vec, ) -> Result<(), NymApiStorageError> { info!("Submitting new node results to the database. There are {} mixnode results and {} gateway results", mixnode_results.len(), gateway_results.len()); @@ -678,8 +734,8 @@ impl NymApiStorage { for report in gateway_reports { // if this ever fails, we have a super weird error because we just constructed report for that node // and we never delete node data! - let node_id = match self.manager.get_gateway_id(&report.identity).await? { - Some(node_id) => node_id, + let db_id = match self.manager.get_gateway_database_id(report.node_id).await? { + Some(db_id) => db_id, None => { error!( "Somehow we failed to grab id of gateway {} from the database!", @@ -690,7 +746,7 @@ impl NymApiStorage { }; self.manager - .insert_gateway_historical_uptime(node_id, today_iso_8601, report.last_day.u8()) + .insert_gateway_historical_uptime(db_id, today_iso_8601, report.last_day.u8()) .await?; } @@ -749,7 +805,7 @@ impl NymApiStorage { pub(crate) async fn get_mixnode_detailed_statuses( &self, - mix_id: MixId, + mix_id: NodeId, limit: u32, offset: u32, ) -> Result, NymApiStorageError> { @@ -783,3 +839,42 @@ impl NymApiStorage { .await?) } } + +pub(crate) mod v3_migration { + use crate::node_status_api::models::NymApiStorageError; + use crate::support::storage::models::GatewayDetailsBeforeMigration; + use crate::support::storage::NymApiStorage; + use nym_mixnet_contract_common::NodeId; + + impl NymApiStorage { + pub(crate) async fn check_v3_migration(&self) -> Result { + Ok(self.manager.check_v3_migration().await?) + } + + pub(crate) async fn set_v3_migration_completion(&self) -> Result<(), NymApiStorageError> { + Ok(self.manager.set_v3_migration_completion().await?) + } + + pub(crate) async fn get_all_known_gateways( + &self, + ) -> Result, NymApiStorageError> { + Ok(self.manager.get_all_known_gateways().await?) + } + + pub(crate) async fn set_gateway_node_id( + &self, + identity: &str, + node_id: NodeId, + ) -> Result<(), NymApiStorageError> { + Ok(self.manager.set_gateway_node_id(identity, node_id).await?) + } + + pub(crate) async fn purge_gateway(&self, db_id: i64) -> Result<(), NymApiStorageError> { + Ok(self.manager.purge_gateway(db_id).await?) + } + + pub(crate) async fn make_node_id_not_null(&self) -> Result<(), NymApiStorageError> { + Ok(self.manager.make_node_id_not_null().await?) + } + } +} diff --git a/nym-api/src/support/storage/models.rs b/nym-api/src/support/storage/models.rs index f48f2187f1..6a7b120e66 100644 --- a/nym-api/src/support/storage/models.rs +++ b/nym-api/src/support/storage/models.rs @@ -2,7 +2,9 @@ // SPDX-License-Identifier: GPL-3.0-only use nym_api_requests::models::TestNode; -use nym_mixnet_contract_common::MixId; +use nym_mixnet_contract_common::NodeId; +use sqlx::FromRow; +use time::Date; // Internally used struct to catch results from the database to calculate uptimes for given mixnode/gateway pub(crate) struct NodeStatus { @@ -23,15 +25,15 @@ impl NodeStatus { // Internally used structs to catch results from the database to find active mixnodes pub(crate) struct ActiveMixnode { pub(crate) id: i64, - pub(crate) mix_id: MixId, + pub(crate) mix_id: NodeId, pub(crate) identity_key: String, - pub(crate) owner: String, } +#[derive(FromRow)] pub(crate) struct ActiveGateway { pub(crate) id: i64, + pub(crate) node_id: NodeId, pub(crate) identity: String, - pub(crate) owner: String, } pub(crate) struct TestingRoute { @@ -54,7 +56,6 @@ pub(crate) struct RewardingReport { pub struct MixnodeDetails { pub id: i64, pub mix_id: i64, - pub owner: String, pub identity_key: String, } @@ -67,9 +68,19 @@ impl From for TestNode { } } +#[derive(FromRow)] +pub struct GatewayDetailsBeforeMigration { + pub id: i64, + #[sqlx(default)] + #[allow(dead_code)] + pub node_id: Option, + pub identity: String, +} + +#[derive(FromRow)] pub struct GatewayDetails { pub id: i64, - pub owner: String, + pub node_id: NodeId, pub identity: String, } @@ -111,3 +122,10 @@ pub struct TestedGatewayStatus { pub layer3_mix_id: i64, pub monitor_run_id: i64, } + +#[derive(FromRow)] +pub struct HistoricalUptime { + #[allow(dead_code)] + pub date: Date, + pub uptime: i64, +} diff --git a/nym-api/src/v2/api_docs.rs b/nym-api/src/v2/api_docs.rs deleted file mode 100644 index 9b248bb803..0000000000 --- a/nym-api/src/v2/api_docs.rs +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::network::handlers::ContractVersionSchemaResponse; -use nym_api_requests::models; -use utoipa::OpenApi; -use utoipauto::utoipauto; - -// TODO once https://github.com/ProbablyClem/utoipauto/pull/38 is released: -// include ",./nym-api/nym-api-requests/src from nym-api-requests" (and other packages mentioned below) -// for automatic model discovery based on ToSchema / IntoParams implementation. -// Then you can remove `components(schemas)` manual imports below - -#[utoipauto(paths = "./nym-api/src")] -#[derive(OpenApi)] -#[openapi( - info(title = "Nym API"), - tags(), - components(schemas( - models::CirculatingSupplyResponse, - models::CoinSchema, - nym_mixnet_contract_common::Interval, - nym_api_requests::models::GatewayStatusReportResponse, - nym_api_requests::models::GatewayUptimeHistoryResponse, - nym_api_requests::models::HistoricalUptimeResponse, - nym_api_requests::models::GatewayCoreStatusResponse, - nym_api_requests::models::GatewayUptimeResponse, - nym_api_requests::models::RewardEstimationResponse, - nym_api_requests::models::UptimeResponse, - nym_api_requests::models::ComputeRewardEstParam, - nym_api_requests::models::MixNodeBondAnnotated, - nym_api_requests::models::GatewayBondAnnotated, - nym_api_requests::models::MixnodeTestResultResponse, - nym_api_requests::models::StakeSaturationResponse, - nym_api_requests::models::InclusionProbabilityResponse, - nym_api_requests::models::AllInclusionProbabilitiesResponse, - nym_api_requests::models::InclusionProbability, - nym_api_requests::models::SelectionChance, - crate::network::models::NetworkDetails, - nym_config::defaults::NymNetworkDetails, - nym_config::defaults::ChainDetails, - nym_config::defaults::DenomDetailsOwned, - nym_config::defaults::ValidatorDetails, - nym_config::defaults::NymContracts, - ContractVersionSchemaResponse, - crate::network::models::ContractInformation, - nym_api_requests::models::ApiHealthResponse, - nym_api_requests::models::ApiStatus, - nym_bin_common::build_information::BinaryBuildInformationOwned, - nym_api_requests::models::SignerInformationResponse, - nym_api_requests::models::DescribedGateway, - nym_api_requests::models::MixNodeDetailsSchema, - nym_mixnet_contract_common::Gateway, - nym_mixnet_contract_common::GatewayBond, - nym_api_requests::models::NymNodeDescription, - nym_api_requests::models::HostInformation, - nym_api_requests::models::HostKeys, - nym_node_requests::api::v1::node::models::AuxiliaryDetails, - nym_api_requests::models::NetworkRequesterDetails, - nym_api_requests::models::IpPacketRouterDetails, - nym_api_requests::models::AuthenticatorDetails, - nym_api_requests::models::WebSockets, - nym_api_requests::nym_nodes::NodeRole, - nym_api_requests::models::DescribedMixNode, - nym_api_requests::ecash::VerificationKeyResponse, - nym_api_requests::ecash::models::AggregatedExpirationDateSignatureResponse, - nym_api_requests::ecash::models::AggregatedCoinIndicesSignatureResponse, - nym_api_requests::ecash::models::EpochCredentialsResponse, - nym_api_requests::ecash::models::IssuedCredentialResponse, - nym_api_requests::ecash::models::IssuedTicketbookBody, - nym_api_requests::ecash::models::BlindedSignatureResponse, - nym_api_requests::ecash::models::PartialExpirationDateSignatureResponse, - nym_api_requests::ecash::models::PartialCoinIndicesSignatureResponse, - nym_api_requests::ecash::models::EcashTicketVerificationResponse, - nym_api_requests::ecash::models::EcashTicketVerificationRejection, - nym_api_requests::ecash::models::EcashBatchTicketRedemptionResponse, - nym_api_requests::ecash::models::SpentCredentialsResponse, - nym_api_requests::ecash::models::IssuedCredentialsResponse, - nym_api_requests::nym_nodes::SkimmedNode, - nym_api_requests::nym_nodes::BasicEntryInformation, - nym_api_requests::nym_nodes::SemiSkimmedNode, - nym_api_requests::nym_nodes::NodeRoleQueryParam, - )) -)] -pub(super) struct ApiDoc; diff --git a/nym-api/src/v2/mod.rs b/nym-api/src/v2/mod.rs deleted file mode 100644 index 59109d16b7..0000000000 --- a/nym-api/src/v2/mod.rs +++ /dev/null @@ -1,313 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use super::support::nyxd; -use crate::circulating_supply_api::cache::CirculatingSupplyCache; -use crate::ecash::api_routes::handlers::ecash_routes; -use crate::ecash::client::Client; -use crate::ecash::comm::QueryCommunicationChannel; -use crate::ecash::dkg::controller::keys::{ - can_validate_coconut_keys, load_bte_keypair, load_ecash_keypair_if_exists, -}; -use crate::ecash::dkg::controller::DkgController; -use crate::ecash::state::EcashState; -use crate::epoch_operations::{self, RewardedSetUpdater}; -use crate::network::models::NetworkDetails; -use crate::node_describe_cache::{self, DescribedNodes}; -use crate::node_status_api::handlers::unstable; -use crate::node_status_api::uptime_updater::HistoricalUptimeUpdater; -use crate::node_status_api::{self, NodeStatusCache}; -use crate::nym_contract_cache::cache::NymContractCache; -use crate::status::{ApiStatusState, SignerState}; -use crate::support::caching::cache::SharedCache; -use crate::support::config::Config; -use crate::support::storage; -use crate::{circulating_supply_api, ecash, network_monitor, nym_contract_cache}; -use anyhow::{bail, Context}; -use nym_config::defaults::NymNetworkDetails; -use nym_sphinx::receiver::SphinxMessageReceiver; -use nym_task::TaskManager; -use nym_validator_client::nyxd::Coin; -use router::RouterBuilder; -use std::sync::Arc; -use tokio::task::JoinHandle; -use tokio_util::sync::CancellationToken; - -pub(crate) mod api_docs; -pub(crate) mod router; - -const TASK_MANAGER_TIMEOUT_S: u64 = 10; - -/// Shutdown goes 2 directions: -/// 1. signal background tasks to gracefully finish -/// 2. signal server itself -/// -/// These are done through separate shutdown handles. Ofcourse, shut down server -/// AFTER you have shut down BG tasks (or past their grace period). -pub(crate) struct ShutdownHandles { - task_manager: TaskManager, - axum_shutdown_button: ShutdownAxum, - /// Tokio JoinHandle for axum server's task - axum_join_handle: AxumJoinHandle, -} - -impl ShutdownHandles { - /// Cancellation token is given to Axum server constructor. When the token - /// receives a shutdown signal, Axum server will shut down gracefully. - pub(crate) fn new( - task_manager: TaskManager, - axum_server_handle: AxumJoinHandle, - shutdown_button: CancellationToken, - ) -> Self { - Self { - task_manager, - axum_shutdown_button: ShutdownAxum(shutdown_button.clone()), - axum_join_handle: axum_server_handle, - } - } - - pub(crate) fn task_manager_mut(&mut self) -> &mut TaskManager { - &mut self.task_manager - } - - /// Signal server to shut down, then return join handle to its - /// `tokio` task - /// - /// https://tikv.github.io/doc/tokio/task/struct.JoinHandle.html - #[must_use] - pub(crate) fn shutdown_axum(self) -> AxumJoinHandle { - self.axum_shutdown_button.0.cancel(); - self.axum_join_handle - } -} - -struct ShutdownAxum(CancellationToken); - -type AxumJoinHandle = JoinHandle>; - -#[derive(Clone)] -// TODO rocket remove smurf name after eliminating rocket -pub(crate) struct AxumAppState { - nym_contract_cache: NymContractCache, - node_status_cache: NodeStatusCache, - circulating_supply_cache: CirculatingSupplyCache, - storage: storage::NymApiStorage, - described_nodes_state: SharedCache, - network_details: NetworkDetails, - node_info_cache: unstable::NodeInfoCache, -} - -impl AxumAppState { - pub(crate) fn nym_contract_cache(&self) -> &NymContractCache { - &self.nym_contract_cache - } - - pub(crate) fn node_status_cache(&self) -> &NodeStatusCache { - &self.node_status_cache - } - - pub(crate) fn circulating_supply_cache(&self) -> &CirculatingSupplyCache { - &self.circulating_supply_cache - } - - pub(crate) fn network_details(&self) -> &NetworkDetails { - &self.network_details - } - - pub(crate) fn described_nodes_state(&self) -> &SharedCache { - &self.described_nodes_state - } - - pub(crate) fn storage(&self) -> &storage::NymApiStorage { - &self.storage - } - - pub(crate) fn node_info_cache(&self) -> &unstable::NodeInfoCache { - &self.node_info_cache - } -} - -pub(crate) async fn start_nym_api_tasks_v2(config: &Config) -> anyhow::Result { - let nyxd_client = nyxd::Client::new(config); - let connected_nyxd = config.get_nyxd_url(); - let nym_network_details = NymNetworkDetails::new_from_env(); - let network_details = NetworkDetails::new(connected_nyxd.to_string(), nym_network_details); - - let coconut_keypair = ecash::keys::KeyPair::new(); - - // if the keypair doesnt exist (because say this API is running in the caching mode), nothing will happen - if let Some(loaded_keys) = load_ecash_keypair_if_exists(&config.coconut_signer)? { - let issued_for = loaded_keys.issued_for_epoch; - coconut_keypair.set(loaded_keys).await; - - if can_validate_coconut_keys(&nyxd_client, issued_for).await? { - coconut_keypair.validate() - } - } - - let identity_keypair = config.base.storage_paths.load_identity()?; - let identity_public_key = *identity_keypair.public_key(); - - let router = RouterBuilder::with_default_routes(config.network_monitor.enabled); - - let nym_contract_cache_state = NymContractCache::new(); - let node_status_cache_state = NodeStatusCache::new(); - let mix_denom = network_details.network.chain_details.mix_denom.base.clone(); - let circulating_supply_cache = CirculatingSupplyCache::new(mix_denom.to_owned()); - let described_nodes_state = SharedCache::::new(); - let storage = - storage::NymApiStorage::init(&config.node_status_api.storage_paths.database_path).await?; - let node_info_cache = unstable::NodeInfoCache::default(); - - let mut status_state = ApiStatusState::new(); - - // if coconut signer is enabled, add /coconut to server - let router = if config.coconut_signer.enabled { - // make sure we have some tokens to cover multisig fees - let balance = nyxd_client.balance(&mix_denom).await?; - if balance.amount < ecash::MINIMUM_BALANCE { - let address = nyxd_client.address().await; - let min = Coin::new(ecash::MINIMUM_BALANCE, mix_denom); - bail!("the account ({address}) doesn't have enough funds to cover verification fees. it has {balance} while it needs at least {min}") - } - - let cosmos_address = nyxd_client.address().await.to_string(); - let announce_address = config - .coconut_signer - .announce_address - .clone() - .map(|u| u.to_string()) - .unwrap_or_default(); - status_state.add_zk_nym_signer(SignerState { - cosmos_address, - identity: identity_keypair.public_key().to_base58_string(), - announce_address, - coconut_keypair: coconut_keypair.clone(), - }); - - let ecash_contract = nyxd_client - .get_ecash_contract_address() - .await - .context("e-cash contract address is required to setup the zk-nym signer")?; - - let comm_channel = QueryCommunicationChannel::new(nyxd_client.clone()); - - let ecash_state = EcashState::new( - ecash_contract, - nyxd_client.clone(), - identity_keypair, - coconut_keypair.clone(), - comm_channel, - storage.clone(), - ) - .await?; - - router.nest("/v1/ecash", ecash_routes(Arc::new(ecash_state))) - } else { - router - }; - - let router = router.with_state(AxumAppState { - nym_contract_cache: nym_contract_cache_state.clone(), - node_status_cache: node_status_cache_state.clone(), - circulating_supply_cache: circulating_supply_cache.clone(), - storage: storage.clone(), - described_nodes_state: described_nodes_state.clone(), - network_details, - node_info_cache, - }); - - let task_manager = TaskManager::new(TASK_MANAGER_TIMEOUT_S); - - // start note describe cache refresher - // we should be doing the below, but can't due to our current startup structure - // let refresher = node_describe_cache::new_refresher(&config.topology_cacher); - // let cache = refresher.get_shared_cache(); - node_describe_cache::new_refresher_with_initial_value( - &config.topology_cacher, - nym_contract_cache_state.clone(), - described_nodes_state, - ) - .named("node-self-described-data-refresher") - .start(task_manager.subscribe_named("node-self-described-data-refresher")); - - // start all the caches first - let nym_contract_cache_listener = nym_contract_cache::start_refresher( - &config.node_status_api, - &nym_contract_cache_state, - nyxd_client.clone(), - &task_manager, - ); - node_status_api::start_cache_refresh( - &config.node_status_api, - &nym_contract_cache_state, - &node_status_cache_state, - storage.clone(), - nym_contract_cache_listener, - &task_manager, - ); - circulating_supply_api::start_cache_refresh( - &config.circulating_supply_cacher, - nyxd_client.clone(), - &circulating_supply_cache, - &task_manager, - ); - - // start dkg task - if config.coconut_signer.enabled { - let dkg_bte_keypair = load_bte_keypair(&config.coconut_signer)?; - - DkgController::start( - &config.coconut_signer, - nyxd_client.clone(), - coconut_keypair, - dkg_bte_keypair, - identity_public_key, - rand::rngs::OsRng, - &task_manager, - )?; - } - - // and then only start the uptime updater (and the monitor itself, duh) - // if the monitoring is enabled - if config.network_monitor.enabled { - network_monitor::start::( - &config.network_monitor, - &nym_contract_cache_state, - &storage, - nyxd_client.clone(), - &task_manager, - ) - .await; - - HistoricalUptimeUpdater::start(storage.to_owned(), &task_manager); - - // start 'rewarding' if its enabled - if config.rewarding.enabled { - epoch_operations::ensure_rewarding_permission(&nyxd_client).await?; - RewardedSetUpdater::start( - nyxd_client, - &nym_contract_cache_state, - storage, - &task_manager, - ); - } - } - - let bind_address = config.base.bind_address.to_owned(); - let server = router.build_server(&bind_address).await?; - - let cancellation_token = CancellationToken::new(); - let shutdown_button = cancellation_token.clone(); - let axum_shutdown_receiver = cancellation_token.cancelled_owned(); - let server_handle = tokio::spawn(async move { - { - info!("Started Axum HTTP V2 server on {bind_address}"); - server.run(axum_shutdown_receiver).await - } - }); - - let shutdown = ShutdownHandles::new(task_manager, server_handle, shutdown_button); - - Ok(shutdown) -} diff --git a/nym-api/src/v3_migration.rs b/nym-api/src/v3_migration.rs new file mode 100644 index 0000000000..c4e920247b --- /dev/null +++ b/nym-api/src/v3_migration.rs @@ -0,0 +1,82 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::support::nyxd::Client; +use crate::support::storage::NymApiStorage; +use anyhow::bail; +use std::collections::HashMap; +use tracing::{debug, info, warn}; + +pub async fn migrate_v3_database( + storage: &NymApiStorage, + nyxd_client: &Client, +) -> anyhow::Result<()> { + if storage.check_v3_migration().await? { + // we have already run the migration + return Ok(()); + } + + info!( + "migrating the database to be compatible with the v3 directory. this might take a while..." + ); + + // get the ids of all the gateways + let preassigned_ids = nyxd_client + .get_gateway_ids() + .await? + .into_iter() + .map(|id| (id.identity, id.node_id)) + .collect::>(); + let contract_gateways = nyxd_client.get_gateways().await?; + let nym_nodes = nyxd_client.get_nymnodes().await?; + + if preassigned_ids.len() != contract_gateways.len() { + bail!("CONTRACT DATA CORRUPTION: THE NUMBER OF PREASSIGNED GATEWAY IDS IS DIFFERENT THAN THE NUMBER OF GATEWAYS") + } + + // assign node_id to every gateway + let all_known = storage.get_all_known_gateways().await?; + for gateway in all_known { + let identity = &gateway.identity; + debug!("migrating gateway {identity}"); + if let Some(assigned) = preassigned_ids.get(identity) { + storage + .set_gateway_node_id(&gateway.identity, *assigned) + .await?; + continue; + }; + + // no pre-assigned id, perhaps the operator has already migrated into a nym-node? + if let Some(nym_node) = nym_nodes + .iter() + .find(|n| &n.bond_information.node.identity_key == identity) + { + storage + .set_gateway_node_id(identity, nym_node.node_id()) + .await?; + continue; + } + + // check if that gateway is even still bonded + let bonded = contract_gateways + .iter() + .any(|g| &g.gateway.identity_key == identity); + + if !bonded { + warn!("could not migrate gateway {identity}, as it does not appear to be bonded. all of its data is going to get purged."); + storage.purge_gateway(gateway.id).await?; + } else { + // this is critical issue because it should have never happened + warn!("could not migrate gateway {identity} even though it's still bonded. something bad has happened!"); + bail!("could not migrate gateway {identity}") + } + } + + debug!("making the column not nullable"); + storage.make_node_id_not_null().await?; + + debug!("marking v3 migration as complete"); + storage.set_v3_migration_completion().await?; + + Ok(()) +} diff --git a/nym-node/nym-node-requests/src/api/client.rs b/nym-node/nym-node-requests/src/api/client.rs index 87257e2835..6255a5644a 100644 --- a/nym-node/nym-node-requests/src/api/client.rs +++ b/nym-node/nym-node-requests/src/api/client.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::api::v1::gateway::models::WebSockets; -use crate::api::v1::node::models::{AuxiliaryDetails, SignedHostInformation}; +use crate::api::v1::node::models::{AuxiliaryDetails, NodeRoles, SignedHostInformation}; use crate::api::ErrorResponse; use crate::routes; use async_trait::async_trait; @@ -39,6 +39,10 @@ pub trait NymNodeApiClientExt: ApiClient { .await } + async fn get_roles(&self) -> Result { + self.get_json_from(routes::api::v1::roles_absolute()).await + } + async fn get_auxiliary_details(&self) -> Result { self.get_json_from(routes::api::v1::auxiliary_absolute()) .await diff --git a/nym-node/nym-node-requests/src/api/v1/node/models.rs b/nym-node/nym-node-requests/src/api/v1/node/models.rs index b3c4a03931..eb2a4f4ce0 100644 --- a/nym-node/nym-node-requests/src/api/v1/node/models.rs +++ b/nym-node/nym-node-requests/src/api/v1/node/models.rs @@ -18,6 +18,27 @@ pub struct NodeRoles { pub ip_packet_router_enabled: bool, } +impl NodeRoles { + pub fn can_operate_mixnode(&self) -> bool { + self.mixnode_enabled + } + + pub fn can_operate_entry_gateway(&self) -> bool { + self.gateway_enabled + } + + pub fn can_operate_exit_gateway(&self) -> bool { + self.gateway_enabled && self.network_requester_enabled && self.ip_packet_router_enabled + } +} + +#[derive(Clone, Copy, Default, Debug, Serialize, Deserialize, JsonSchema)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +pub struct AnnouncePorts { + pub verloc_port: Option, + pub mix_port: Option, +} + #[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema)] #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] pub struct HostInformation { @@ -175,6 +196,9 @@ pub struct AuxiliaryDetails { #[schemars(length(equal = 2))] pub location: Option, + #[serde(default)] + pub announce_ports: AnnouncePorts, + /// Specifies whether this node operator has agreed to the terms and conditions /// as defined at // make sure to include the default deserialisation as this field hasn't existed when the struct was first created diff --git a/nym-node/src/cli/commands/migrate.rs b/nym-node/src/cli/commands/migrate.rs index 08d9d0b2e0..ec8cdaeeea 100644 --- a/nym-node/src/cli/commands/migrate.rs +++ b/nym-node/src/cli/commands/migrate.rs @@ -235,6 +235,7 @@ async fn migrate_mixnode(mut args: Args) -> Result<(), NymNodeError> { .with_mixnode(args.mixnode.override_config_section(config::MixnodeConfig { verloc: config::mixnode::Verloc { bind_address: SocketAddr::new(ip, cfg.mixnode.verloc_port), + announce_port: None, debug: config::mixnode::VerlocDebug { packets_per_node: cfg.verloc.packets_per_node, connection_timeout: cfg.verloc.connection_timeout, @@ -386,6 +387,7 @@ async fn migrate_gateway(mut args: Args) -> Result<(), NymNodeError> { })) .with_mixnet(args.mixnet.override_config_section(config::Mixnet { bind_address: SocketAddr::new(ip, cfg.gateway.mix_port), + announce_port: None, nym_api_urls: cfg.gateway.nym_api_urls.clone(), nyxd_urls: cfg.gateway.nyxd_urls.clone(), debug: config::MixnetDebug { diff --git a/nym-node/src/config/mixnode.rs b/nym-node/src/config/mixnode.rs index 684ea7129b..f5a56e128a 100644 --- a/nym-node/src/config/mixnode.rs +++ b/nym-node/src/config/mixnode.rs @@ -7,6 +7,7 @@ use crate::error::MixnodeError; use clap::crate_version; use nym_config::defaults::DEFAULT_VERLOC_LISTENING_PORT; use nym_config::helpers::inaddr_any; +use nym_config::serde_helpers::de_maybe_port; use serde::{Deserialize, Serialize}; use std::net::SocketAddr; use std::time::Duration; @@ -41,6 +42,12 @@ pub struct Verloc { /// default: `0.0.0.0:1790` pub bind_address: SocketAddr, + /// If applicable, custom port announced in the self-described API that other clients and nodes + /// will use. + /// Useful when the node is behind a proxy. + #[serde(deserialize_with = "de_maybe_port")] + pub announce_port: Option, + #[serde(default)] pub debug: VerlocDebug, } @@ -49,6 +56,7 @@ impl Default for Verloc { fn default() -> Self { Verloc { bind_address: SocketAddr::new(inaddr_any(), DEFAULT_VERLOC_PORT), + announce_port: None, debug: Default::default(), } } diff --git a/nym-node/src/config/mod.rs b/nym-node/src/config/mod.rs index 5da49419cf..60ed79ab20 100644 --- a/nym-node/src/config/mod.rs +++ b/nym-node/src/config/mod.rs @@ -12,6 +12,7 @@ use nym_config::defaults::{ mainnet, var_names, DEFAULT_MIX_LISTENING_PORT, DEFAULT_NYM_NODE_HTTP_PORT, WG_PORT, }; use nym_config::helpers::inaddr_any; +use nym_config::serde_helpers::de_maybe_port; use nym_config::serde_helpers::de_maybe_stringified; use nym_config::{ must_get_home, parse_urls, read_config_from_toml_file, save_formatted_config_to_file, @@ -416,6 +417,12 @@ pub struct Mixnet { /// default: `0.0.0.0:1789` pub bind_address: SocketAddr, + /// If applicable, custom port announced in the self-described API that other clients and nodes + /// will use. + /// Useful when the node is behind a proxy. + #[serde(deserialize_with = "de_maybe_port")] + pub announce_port: Option, + /// Addresses to nym APIs from which the node gets the view of the network. pub nym_api_urls: Vec, @@ -492,6 +499,7 @@ impl Default for Mixnet { Mixnet { bind_address: SocketAddr::new(inaddr_any(), DEFAULT_MIXNET_PORT), + announce_port: None, nym_api_urls, nyxd_urls, debug: Default::default(), diff --git a/nym-node/src/config/old_configs/old_config_v3.rs b/nym-node/src/config/old_configs/old_config_v3.rs index 809955e5ee..f13cfdf4a0 100644 --- a/nym-node/src/config/old_configs/old_config_v3.rs +++ b/nym-node/src/config/old_configs/old_config_v3.rs @@ -992,6 +992,7 @@ pub async fn try_upgrade_config_v3>( }, mixnet: Mixnet { bind_address: old_cfg.mixnet.bind_address, + announce_port: None, nym_api_urls: old_cfg.mixnet.nym_api_urls, nyxd_urls: old_cfg.mixnet.nyxd_urls, debug: MixnetDebug { @@ -1066,6 +1067,7 @@ pub async fn try_upgrade_config_v3>( storage_paths: MixnodePaths {}, verloc: Verloc { bind_address: old_cfg.mixnode.verloc.bind_address, + announce_port: None, debug: VerlocDebug { packets_per_node: old_cfg.mixnode.verloc.debug.packets_per_node, connection_timeout: old_cfg.mixnode.verloc.debug.connection_timeout, diff --git a/nym-node/src/config/template.rs b/nym-node/src/config/template.rs index f1576f2fe2..83588a0639 100644 --- a/nym-node/src/config/template.rs +++ b/nym-node/src/config/template.rs @@ -41,6 +41,12 @@ location = '{{ host.location }}' # default: `0.0.0.0:1789` bind_address = '{{ mixnet.bind_address }}' +# If applicable, custom port announced in the self-described API that other clients and nodes +# will use. +# Useful when the node is behind a proxy. +# (default: 0 - disabled) +announce_port ={{#if mixnet.announce_port }} {{ mixnet.announce_port }} {{else}} 0 {{/if}} + # Addresses to nym APIs from which the node gets the view of the network. nym_api_urls = [ {{#each mixnet.nym_api_urls }}'{{this}}',{{/each}} @@ -144,6 +150,12 @@ public_diffie_hellman_key_file = '{{ wireguard.storage_paths.public_diffie_hellm # default: `0.0.0.0:1790` bind_address = '{{ mixnode.verloc.bind_address }}' +# If applicable, custom port announced in the self-described API that other clients and nodes +# will use. +# Useful when the node is behind a proxy. +# (default: 0 - disabled) +announce_port ={{#if mixnode.verloc.announce_port }} {{ mixnode.verloc.announce_port }} {{else}} 0 {{/if}} + [mixnode.storage_paths] # currently empty diff --git a/nym-node/src/node/mod.rs b/nym-node/src/node/mod.rs index 3bb5fe83ca..d3e756c6e0 100644 --- a/nym-node/src/node/mod.rs +++ b/nym-node/src/node/mod.rs @@ -25,7 +25,7 @@ use nym_node::config::{ }; use nym_node::error::{EntryGatewayError, ExitGatewayError, MixnodeError, NymNodeError}; use nym_node_http_api::api::api_requests; -use nym_node_http_api::api::api_requests::v1::node::models::NodeDescription; +use nym_node_http_api::api::api_requests::v1::node::models::{AnnouncePorts, NodeDescription}; use nym_node_http_api::state::metrics::{SharedMixingStats, SharedVerlocStats}; use nym_node_http_api::state::AppState; use nym_node_http_api::{NymNodeHTTPServer, NymNodeRouter}; @@ -632,6 +632,10 @@ impl NymNode { let auxiliary_details = api_requests::v1::node::models::AuxiliaryDetails { location: self.config.host.location, + announce_ports: AnnouncePorts { + verloc_port: self.config.mixnode.verloc.announce_port, + mix_port: self.config.mixnet.announce_port, + }, accepted_operator_terms_and_conditions: self.accepted_operator_terms_and_conditions, }; diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index 951f240165..eeef135c89 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -173,9 +173,9 @@ checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" [[package]] name = "async-trait" -version = "0.1.83" +version = "0.1.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" +checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1" dependencies = [ "proc-macro2", "quote", @@ -1637,9 +1637,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.34" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" +checksum = "324a1be68054ef05ad64b861cc9eaf1d623d2d8cb25b4bf2cb9cdd902b4bf253" dependencies = [ "crc32fast", "miniz_oxide 0.8.0", @@ -3096,10 +3096,12 @@ dependencies = [ "nym-crypto", "nym-ecash-time", "nym-mixnet-contract-common", + "nym-network-defaults", "nym-node-requests", "nym-serde-helpers", "schemars", "serde", + "serde_json", "sha2 0.10.8", "tendermint 0.37.0", "thiserror", @@ -3289,6 +3291,7 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-controllers", + "cw-storage-plus", "humantime-serde", "log", "nym-contracts-common", @@ -3361,7 +3364,6 @@ version = "0.1.0" dependencies = [ "base64 0.22.1", "bs58", - "hex", "serde", "time", ] @@ -3450,7 +3452,6 @@ dependencies = [ "nym-mixnet-contract-common", "nym-multisig-contract-common", "nym-network-defaults", - "nym-serde-helpers", "nym-vesting-contract-common", "prost", "reqwest 0.12.4", @@ -4878,9 +4879,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.8" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" dependencies = [ "serde", ] @@ -5762,18 +5763,18 @@ checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c" [[package]] name = "thiserror" -version = "1.0.64" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.64" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", @@ -5977,7 +5978,7 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "winnow 0.6.20", + "winnow 0.6.19", ] [[package]] @@ -6853,9 +6854,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.6.20" +version = "0.6.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +checksum = "c52ac009d615e79296318c1bcce2d422aaca15ad08515e344feeda07df67a587" dependencies = [ "memchr", ] diff --git a/nym-wallet/nym-wallet-types/src/admin.rs b/nym-wallet/nym-wallet-types/src/admin.rs index 0aac089ee2..0e76cba619 100644 --- a/nym-wallet/nym-wallet-types/src/admin.rs +++ b/nym-wallet/nym-wallet-types/src/admin.rs @@ -17,9 +17,8 @@ use serde::{Deserialize, Serialize}; )] #[derive(Serialize, Deserialize, Debug)] pub struct TauriContractStateParams { - minimum_mixnode_pledge: DecCoin, - minimum_gateway_pledge: DecCoin, - minimum_mixnode_delegation: Option, + minimum_pledge: DecCoin, + minimum_delegation: Option, operating_cost: TauriOperatingCostRange, profit_margin: TauriProfitMarginRange, @@ -52,7 +51,7 @@ impl TauriContractStateParams { state_params: ContractStateParams, reg: &RegisteredCoins, ) -> Result { - let rewarding_denom = &state_params.minimum_mixnode_pledge.denom; + let rewarding_denom = &state_params.minimum_pledge.denom; let min_operating_cost_c = Coin { denom: rewarding_denom.into(), amount: state_params.interval_operating_cost.minimum, @@ -63,12 +62,10 @@ impl TauriContractStateParams { }; Ok(TauriContractStateParams { - minimum_mixnode_pledge: reg - .attempt_convert_to_display_dec_coin(state_params.minimum_mixnode_pledge.into())?, - minimum_gateway_pledge: reg - .attempt_convert_to_display_dec_coin(state_params.minimum_gateway_pledge.into())?, - minimum_mixnode_delegation: state_params - .minimum_mixnode_delegation + minimum_pledge: reg + .attempt_convert_to_display_dec_coin(state_params.minimum_pledge.into())?, + minimum_delegation: state_params + .minimum_delegation .map(|min_del| reg.attempt_convert_to_display_dec_coin(min_del.into())) .transpose()?, @@ -96,16 +93,13 @@ impl TauriContractStateParams { let max_operating_cost_c = reg.attempt_convert_to_base_coin(self.operating_cost.maximum)?; Ok(ContractStateParams { - minimum_mixnode_delegation: self - .minimum_mixnode_delegation + minimum_delegation: self + .minimum_delegation .map(|min_del| reg.attempt_convert_to_base_coin(min_del)) .transpose()? .map(Into::into), - minimum_mixnode_pledge: reg - .attempt_convert_to_base_coin(self.minimum_mixnode_pledge)? - .into(), - minimum_gateway_pledge: reg - .attempt_convert_to_base_coin(self.minimum_gateway_pledge)? + minimum_pledge: reg + .attempt_convert_to_base_coin(self.minimum_pledge)? .into(), profit_margin: ContractProfitMarginRange { diff --git a/nym-wallet/src-tauri/src/error.rs b/nym-wallet/src-tauri/src/error.rs index 9d879c706e..cefac100e7 100644 --- a/nym-wallet/src-tauri/src/error.rs +++ b/nym-wallet/src-tauri/src/error.rs @@ -158,6 +158,9 @@ pub enum BackendError { #[error("there aren't any vesting delegations to migrate")] NoVestingDelegations, + + #[error("this operation is no longer allowed to be performed with vesting tokens. please move them to your liquid balance and try again")] + DisabledVestingOperation, } impl Serialize for BackendError { diff --git a/nym-wallet/src-tauri/src/operations/helpers.rs b/nym-wallet/src-tauri/src/operations/helpers.rs index da0093e293..fc8212d48e 100644 --- a/nym-wallet/src-tauri/src/operations/helpers.rs +++ b/nym-wallet/src-tauri/src/operations/helpers.rs @@ -10,7 +10,7 @@ use nym_contracts_common::signing::{ use nym_crypto::asymmetric::identity; use nym_mixnet_contract_common::{ construct_legacy_mixnode_bonding_sign_payload, Gateway, GatewayBondingPayload, MixNode, - MixNodeCostParams, SignableGatewayBondingMsg, SignableLegacyMixNodeBondingMsg, + NodeCostParams, SignableGatewayBondingMsg, SignableLegacyMixNodeBondingMsg, }; use nym_validator_client::nyxd::contract_traits::MixnetQueryClient; use nym_validator_client::nyxd::error::NyxdError; @@ -39,7 +39,7 @@ impl AddressAndNonceProvider for DirectSigningHttpRpcValidatorClient { pub(crate) async fn create_mixnode_bonding_sign_payload( client: &P, mix_node: MixNode, - cost_params: MixNodeCostParams, + cost_params: NodeCostParams, pledge: Coin, vesting: bool, ) -> Result { @@ -61,7 +61,7 @@ pub(crate) async fn create_mixnode_bonding_sign_payload( client: &P, mix_node: &MixNode, - cost_params: &MixNodeCostParams, + cost_params: &NodeCostParams, pledge: &Coin, vesting: bool, msg_signature: &MessageSignature, @@ -148,6 +148,7 @@ mod tests { use super::*; use cosmwasm_std::coin; use nym_contracts_common::Percent; + use nym_mixnet_contract_common::NodeCostParams; use rand_chacha::rand_core::SeedableRng; use rand_chacha::ChaCha20Rng; @@ -186,7 +187,7 @@ mod tests { identity_key: identity_keypair.public_key().to_base58_string(), version: "v1.2.3".to_string(), }; - let dummy_cost_params = MixNodeCostParams { + let dummy_cost_params = NodeCostParams { profit_margin_percent: Percent::from_percentage_value(42).unwrap(), interval_operating_cost: coin(1111111, "unym"), }; diff --git a/nym-wallet/src-tauri/src/operations/mixnet/bond.rs b/nym-wallet/src-tauri/src/operations/mixnet/bond.rs index 1cf3a7bd69..a942321f9b 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/bond.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/bond.rs @@ -9,10 +9,11 @@ use crate::state::WalletState; use crate::{nyxd_client, Gateway, MixNode}; use nym_contracts_common::signing::MessageSignature; use nym_mixnet_contract_common::gateway::GatewayConfigUpdate; -use nym_mixnet_contract_common::{MixId, MixNodeConfigUpdate}; +use nym_mixnet_contract_common::{MixNodeConfigUpdate, NodeId}; use nym_types::currency::DecCoin; use nym_types::gateway::GatewayBond; -use nym_types::mixnode::{MixNodeCostParams, MixNodeDetails}; +use nym_types::mixnode::{MixNodeDetails, NodeCostParams}; +use nym_types::nym_node::NymNodeDetails; use nym_types::transaction::TransactionExecuteResult; use nym_validator_client::client::NymApiClientExt; use nym_validator_client::nyxd::contract_traits::{MixnetQueryClient, MixnetSigningClient}; @@ -89,7 +90,7 @@ pub async fn unbond_gateway( #[tauri::command] pub async fn bond_mixnode( mixnode: MixNode, - cost_params: MixNodeCostParams, + cost_params: NodeCostParams, msg_signature: MessageSignature, pledge: DecCoin, fee: Option, @@ -256,7 +257,7 @@ pub async fn unbond_mixnode( #[tauri::command] pub async fn update_mixnode_cost_params( - new_costs: MixNodeCostParams, + new_costs: NodeCostParams, fee: Option, state: tauri::State<'_, WalletState>, ) -> Result { @@ -272,7 +273,7 @@ pub async fn update_mixnode_cost_params( let res = guard .current_client()? .nyxd - .update_mixnode_cost_params(cost_params, fee) + .update_cost_params(cost_params, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); log::trace!("<<< {:?}", res); @@ -420,6 +421,37 @@ pub async fn gateway_bond_details( Ok(res) } +#[tauri::command] +pub async fn nym_node_bond_details( + state: tauri::State<'_, WalletState>, +) -> Result, BackendError> { + log::info!(">>> Get nym-node bond details"); + let guard = state.read().await; + let client = guard.current_client()?; + let res = client + .nyxd + .get_owned_nymnode(&client.nyxd.address()) + .await?; + let details = res + .details + .map(|details| { + guard + .registered_coins() + .map(|reg| NymNodeDetails::from_mixnet_contract_nym_node_details(details, reg)) + }) + .transpose()? + .transpose()?; + log::info!( + "<<< node_id/identity_key = {:?}", + details.as_ref().map(|r| ( + r.bond_information.node_id, + &r.bond_information.node.identity_key + )) + ); + log::trace!("<<< {:?}", details); + Ok(details) +} + #[tauri::command] pub async fn get_pending_operator_rewards( address: String, @@ -459,7 +491,7 @@ pub async fn get_pending_operator_rewards( #[tauri::command] pub async fn get_number_of_mixnode_delegators( - mix_id: MixId, + mix_id: NodeId, state: tauri::State<'_, WalletState>, ) -> Result { Ok(nyxd_client!(state) @@ -487,7 +519,7 @@ pub async fn get_mix_node_description( #[tauri::command] pub async fn get_mixnode_uptime( - mix_id: MixId, + mix_id: NodeId, state: tauri::State<'_, WalletState>, ) -> Result { log::info!(">>> Get mixnode uptime"); diff --git a/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs b/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs index 7a705f6d03..31af28d199 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs @@ -4,14 +4,14 @@ use crate::error::BackendError; use crate::state::WalletState; use crate::vesting::delegate::vesting_undelegate_from_mixnode; -use nym_mixnet_contract_common::mixnode::StakeSaturationResponse; -use nym_mixnet_contract_common::MixId; +use nym_mixnet_contract_common::mixnode::MixStakeSaturationResponse; +use nym_mixnet_contract_common::NodeId; use nym_types::currency::DecCoin; use nym_types::delegation::{Delegation, DelegationWithEverything, DelegationsSummaryResponse}; use nym_types::deprecated::{ convert_to_delegation_events, DelegationEvent, WrappedDelegationEvent, }; -use nym_types::mixnode::MixNodeCostParams; +use nym_types::mixnode::NodeCostParams; use nym_types::pending_events::PendingEpochEvent; use nym_types::transaction::TransactionExecuteResult; use nym_validator_client::client::NymApiClientExt; @@ -69,7 +69,7 @@ pub async fn get_pending_delegation_events( #[tauri::command] pub async fn delegate_to_mixnode( - mix_id: MixId, + mix_id: NodeId, amount: DecCoin, fee: Option, state: tauri::State<'_, WalletState>, @@ -86,10 +86,7 @@ pub async fn delegate_to_mixnode( delegation_base, fee, ); - let res = client - .nyxd - .delegate_to_mixnode(mix_id, delegation_base, fee) - .await?; + let res = client.nyxd.delegate(mix_id, delegation_base, fee).await?; log::info!("<<< tx hash = {}", res.transaction_hash); log::trace!("<<< {:?}", res); Ok(TransactionExecuteResult::from_execute_result( @@ -99,7 +96,7 @@ pub async fn delegate_to_mixnode( #[tauri::command] pub async fn undelegate_from_mixnode( - mix_id: MixId, + mix_id: NodeId, fee: Option, state: tauri::State<'_, WalletState>, ) -> Result { @@ -111,11 +108,7 @@ pub async fn undelegate_from_mixnode( mix_id, fee ); - let res = guard - .current_client()? - .nyxd - .undelegate_from_mixnode(mix_id, fee) - .await?; + let res = guard.current_client()?.nyxd.undelegate(mix_id, fee).await?; log::info!("<<< tx hash = {}", res.transaction_hash); log::trace!("<<< {:?}", res); Ok(TransactionExecuteResult::from_execute_result( @@ -125,7 +118,7 @@ pub async fn undelegate_from_mixnode( #[tauri::command] pub async fn undelegate_all_from_mixnode( - mix_id: MixId, + mix_id: NodeId, uses_vesting_contract_tokens: bool, fee_liquid: Option, fee_vesting: Option, @@ -195,8 +188,8 @@ pub async fn get_all_mix_delegations( let d = Delegation::from_mixnet_contract(delegation.clone(), reg).tap_err(|err| { log::error!( - " <<< Failed to get delegation for mix id {} from contract. Error: {}", - delegation.mix_id, + " <<< Failed to get delegation for node id {} from contract. Error: {}", + delegation.node_id, err ); })?; @@ -262,7 +255,7 @@ pub async fn get_all_mix_delegations( let cost_params = mixnode .as_ref() .map(|m| { - MixNodeCostParams::from_mixnet_contract_mixnode_cost_params( + NodeCostParams::from_mixnet_contract_mixnode_cost_params( m.rewarding_details.cost_params.clone(), reg, ) @@ -329,7 +322,7 @@ pub async fn get_all_mix_delegations( log::error!(" <<< {}", str_err); error_strings.push(str_err); }) - .unwrap_or(StakeSaturationResponse { + .unwrap_or(MixStakeSaturationResponse { mix_id: d.mix_id, uncapped_saturation: None, current_saturation: None, @@ -420,7 +413,7 @@ pub async fn get_all_mix_delegations( } fn filter_pending_events( - mix_id: MixId, + mix_id: NodeId, pending_events: &[WrappedDelegationEvent], ) -> Vec { pending_events @@ -434,7 +427,7 @@ fn filter_pending_events( #[tauri::command] pub async fn get_pending_delegator_rewards( address: String, - mix_id: MixId, + mix_id: NodeId, proxy: Option, state: tauri::State<'_, WalletState>, ) -> Result { diff --git a/nym-wallet/src-tauri/src/operations/mixnet/rewards.rs b/nym-wallet/src-tauri/src/operations/mixnet/rewards.rs index 266295c6e1..4563005b88 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/rewards.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/rewards.rs @@ -1,7 +1,7 @@ use crate::error::BackendError; use crate::state::WalletState; use crate::vesting::rewards::vesting_claim_delegator_reward; -use nym_mixnet_contract_common::{MixId, RewardingParams}; +use nym_mixnet_contract_common::{NodeId, RewardingParams}; use nym_types::transaction::TransactionExecuteResult; use nym_validator_client::nyxd::contract_traits::{ MixnetQueryClient, MixnetSigningClient, NymContractsProvider, PagedMixnetQueryClient, @@ -31,17 +31,17 @@ pub async fn claim_operator_reward( #[tauri::command] pub async fn claim_delegator_reward( - mix_id: MixId, + node_id: NodeId, fee: Option, state: tauri::State<'_, WalletState>, ) -> Result { - log::info!(">>> Withdraw delegator reward: mix_id = {}", mix_id); + log::info!(">>> Withdraw delegator reward: node_id = {node_id}"); let guard = state.read().await; let fee_amount = guard.convert_tx_fee(fee.as_ref()); let res = guard .current_client()? .nyxd - .withdraw_delegator_reward(mix_id, fee) + .withdraw_delegator_reward(node_id, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); log::trace!("<<< {:?}", res); @@ -52,19 +52,16 @@ pub async fn claim_delegator_reward( #[tauri::command] pub async fn claim_locked_and_unlocked_delegator_reward( - mix_id: MixId, + node_id: NodeId, fee: Option, state: tauri::State<'_, WalletState>, ) -> Result, BackendError> { - log::info!( - ">>> Claim delegator reward (locked and unlocked): mix_id = {}", - mix_id - ); + log::info!(">>> Claim delegator reward (locked and unlocked): node_id = {node_id}",); let guard = state.read().await; let client = guard.current_client()?; - log::trace!(">>> Get delegations: mix_id = {}", mix_id); + log::trace!(">>> Get delegations: node_id = {node_id}"); let address = client.nyxd.address(); let delegations = client.nyxd.get_all_delegator_delegations(&address).await?; log::trace!("<<< {} delegations", delegations.len()); @@ -76,11 +73,11 @@ pub async fn claim_locked_and_unlocked_delegator_reward( .to_string(); let liquid_delegation = client .nyxd - .get_delegation_details(mix_id, &address, None) + .get_delegation_details(node_id, &address, None) .await?; let vesting_delegation = client .nyxd - .get_delegation_details(mix_id, &address, Some(vesting_contract)) + .get_delegation_details(node_id, &address, Some(vesting_contract)) .await?; drop(guard); @@ -97,10 +94,10 @@ pub async fn claim_locked_and_unlocked_delegator_reward( let mut res: Vec = vec![]; if did_delegate_with_mixnet_contract { - res.push(claim_delegator_reward(mix_id, fee.clone(), state.clone()).await?); + res.push(claim_delegator_reward(node_id, fee.clone(), state.clone()).await?); } if did_delegate_with_vesting_contract { - res.push(vesting_claim_delegator_reward(mix_id, fee, state).await?); + res.push(vesting_claim_delegator_reward(node_id, fee, state).await?); } log::trace!("<<< {:?}", res); Ok(res) diff --git a/nym-wallet/src-tauri/src/operations/nym_api/status.rs b/nym-wallet/src-tauri/src/operations/nym_api/status.rs index 096615b3b5..916dac0cea 100644 --- a/nym-wallet/src-tauri/src/operations/nym_api/status.rs +++ b/nym-wallet/src-tauri/src/operations/nym_api/status.rs @@ -5,7 +5,7 @@ use crate::api_client; use crate::error::BackendError; use crate::state::WalletState; use nym_mixnet_contract_common::{ - reward_params::Performance, Coin, IdentityKeyRef, MixId, Percent, + reward_params::Performance, Coin, IdentityKeyRef, NodeId, Percent, }; use nym_validator_client::client::NymApiClientExt; use nym_validator_client::models::{ @@ -16,7 +16,7 @@ use nym_validator_client::models::{ #[tauri::command] pub async fn mixnode_core_node_status( - mix_id: MixId, + mix_id: NodeId, since: Option, state: tauri::State<'_, WalletState>, ) -> Result { @@ -46,7 +46,7 @@ pub async fn gateway_report( #[tauri::command] pub async fn mixnode_status( - mix_id: MixId, + mix_id: NodeId, state: tauri::State<'_, WalletState>, ) -> Result { Ok(api_client!(state).get_mixnode_status(mix_id).await?) @@ -54,7 +54,7 @@ pub async fn mixnode_status( #[tauri::command] pub async fn mixnode_reward_estimation( - mix_id: MixId, + mix_id: NodeId, state: tauri::State<'_, WalletState>, ) -> Result { Ok(api_client!(state) @@ -87,7 +87,7 @@ pub async fn compute_mixnode_reward_estimation( #[tauri::command] pub async fn mixnode_stake_saturation( - mix_id: MixId, + mix_id: NodeId, state: tauri::State<'_, WalletState>, ) -> Result { Ok(api_client!(state) @@ -97,7 +97,7 @@ pub async fn mixnode_stake_saturation( #[tauri::command] pub async fn mixnode_inclusion_probability( - mix_id: MixId, + mix_id: NodeId, state: tauri::State<'_, WalletState>, ) -> Result { Ok(api_client!(state) diff --git a/nym-wallet/src-tauri/src/operations/signatures/ed25519_signing_payload.rs b/nym-wallet/src-tauri/src/operations/signatures/ed25519_signing_payload.rs index d539cf5eb3..c0c3dd373a 100644 --- a/nym-wallet/src-tauri/src/operations/signatures/ed25519_signing_payload.rs +++ b/nym-wallet/src-tauri/src/operations/signatures/ed25519_signing_payload.rs @@ -8,11 +8,11 @@ use crate::operations::helpers::{ use crate::state::WalletState; use nym_mixnet_contract_common::{Gateway, MixNode}; use nym_types::currency::DecCoin; -use nym_types::mixnode::MixNodeCostParams; +use nym_types::mixnode::NodeCostParams; async fn mixnode_bonding_msg_payload( mixnode: MixNode, - cost_params: MixNodeCostParams, + cost_params: NodeCostParams, pledge: DecCoin, vesting: bool, state: tauri::State<'_, WalletState>, @@ -64,7 +64,7 @@ async fn gateway_bonding_msg_payload( #[tauri::command] pub async fn generate_mixnode_bonding_msg_payload( mixnode: MixNode, - cost_params: MixNodeCostParams, + cost_params: NodeCostParams, pledge: DecCoin, state: tauri::State<'_, WalletState>, ) -> Result { @@ -74,7 +74,7 @@ pub async fn generate_mixnode_bonding_msg_payload( #[tauri::command] pub async fn vesting_generate_mixnode_bonding_msg_payload( mixnode: MixNode, - cost_params: MixNodeCostParams, + cost_params: NodeCostParams, pledge: DecCoin, state: tauri::State<'_, WalletState>, ) -> Result { diff --git a/nym-wallet/src-tauri/src/operations/simulate/mixnet.rs b/nym-wallet/src-tauri/src/operations/simulate/mixnet.rs index c64fcc53af..afcb0a512f 100644 --- a/nym-wallet/src-tauri/src/operations/simulate/mixnet.rs +++ b/nym-wallet/src-tauri/src/operations/simulate/mixnet.rs @@ -1,17 +1,16 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use std::cmp::Ordering; - use crate::error::BackendError; use crate::operations::simulate::FeeDetails; use crate::WalletState; use nym_contracts_common::signing::MessageSignature; -use nym_mixnet_contract_common::{ExecuteMsg, Gateway, MixId, MixNode}; +use nym_mixnet_contract_common::{ExecuteMsg, Gateway, MixNode, NodeId}; use nym_mixnet_contract_common::{GatewayConfigUpdate, MixNodeConfigUpdate}; use nym_types::currency::DecCoin; -use nym_types::mixnode::MixNodeCostParams; +use nym_types::mixnode::NodeCostParams; use nym_validator_client::nyxd::contract_traits::NymContractsProvider; +use std::cmp::Ordering; async fn simulate_mixnet_operation( msg: ExecuteMsg, @@ -70,7 +69,7 @@ pub async fn simulate_unbond_gateway( #[tauri::command] pub async fn simulate_bond_mixnode( mixnode: MixNode, - cost_params: MixNodeCostParams, + cost_params: NodeCostParams, msg_signature: MessageSignature, pledge: DecCoin, state: tauri::State<'_, WalletState>, @@ -148,19 +147,14 @@ pub async fn simulate_unbond_mixnode( #[tauri::command] pub async fn simulate_update_mixnode_cost_params( - new_costs: MixNodeCostParams, + new_costs: NodeCostParams, state: tauri::State<'_, WalletState>, ) -> Result { let guard = state.read().await; let reg = guard.registered_coins()?; let new_costs = new_costs.try_convert_to_mixnet_contract_cost_params(reg)?; - simulate_mixnet_operation( - ExecuteMsg::UpdateMixnodeCostParams { new_costs }, - None, - &state, - ) - .await + simulate_mixnet_operation(ExecuteMsg::UpdateCostParams { new_costs }, None, &state).await } #[tauri::command] @@ -191,24 +185,19 @@ pub async fn simulate_update_gateway_config( #[tauri::command] pub async fn simulate_delegate_to_mixnode( - mix_id: MixId, + node_id: NodeId, amount: DecCoin, state: tauri::State<'_, WalletState>, ) -> Result { - simulate_mixnet_operation( - ExecuteMsg::DelegateToMixnode { mix_id }, - Some(amount), - &state, - ) - .await + simulate_mixnet_operation(ExecuteMsg::Delegate { node_id }, Some(amount), &state).await } #[tauri::command] pub async fn simulate_undelegate_from_mixnode( - mix_id: MixId, + node_id: NodeId, state: tauri::State<'_, WalletState>, ) -> Result { - simulate_mixnet_operation(ExecuteMsg::UndelegateFromMixnode { mix_id }, None, &state).await + simulate_mixnet_operation(ExecuteMsg::Undelegate { node_id }, None, &state).await } #[tauri::command] @@ -220,8 +209,13 @@ pub async fn simulate_claim_operator_reward( #[tauri::command] pub async fn simulate_claim_delegator_reward( - mix_id: MixId, + node_id: NodeId, state: tauri::State<'_, WalletState>, ) -> Result { - simulate_mixnet_operation(ExecuteMsg::WithdrawDelegatorReward { mix_id }, None, &state).await + simulate_mixnet_operation( + ExecuteMsg::WithdrawDelegatorReward { node_id }, + None, + &state, + ) + .await } diff --git a/nym-wallet/src-tauri/src/operations/simulate/vesting.rs b/nym-wallet/src-tauri/src/operations/simulate/vesting.rs index 9a33e2a3ed..9578575c3f 100644 --- a/nym-wallet/src-tauri/src/operations/simulate/vesting.rs +++ b/nym-wallet/src-tauri/src/operations/simulate/vesting.rs @@ -1,18 +1,17 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use std::cmp::Ordering; - use crate::error::BackendError; use crate::operations::simulate::FeeDetails; use crate::WalletState; use nym_contracts_common::signing::MessageSignature; -use nym_mixnet_contract_common::{Gateway, MixId, MixNode}; +use nym_mixnet_contract_common::{Gateway, MixNode, NodeId}; use nym_mixnet_contract_common::{GatewayConfigUpdate, MixNodeConfigUpdate}; use nym_types::currency::DecCoin; -use nym_types::mixnode::MixNodeCostParams; +use nym_types::mixnode::NodeCostParams; use nym_validator_client::nyxd::contract_traits::NymContractsProvider; use nym_vesting_contract_common::ExecuteMsg; +use std::cmp::Ordering; async fn simulate_vesting_operation( msg: ExecuteMsg, @@ -75,7 +74,7 @@ pub async fn simulate_vesting_unbond_gateway( #[tauri::command] pub async fn simulate_vesting_bond_mixnode( mixnode: MixNode, - cost_params: MixNodeCostParams, + cost_params: NodeCostParams, msg_signature: MessageSignature, pledge: DecCoin, state: tauri::State<'_, WalletState>, @@ -173,7 +172,7 @@ pub async fn simulate_vesting_unbond_mixnode( #[tauri::command] pub async fn simulate_vesting_update_mixnode_cost_params( - new_costs: MixNodeCostParams, + new_costs: NodeCostParams, state: tauri::State<'_, WalletState>, ) -> Result { let guard = state.read().await; @@ -216,7 +215,7 @@ pub async fn simulate_vesting_update_gateway_config( #[tauri::command] pub async fn simulate_vesting_delegate_to_mixnode( - mix_id: MixId, + mix_id: NodeId, amount: DecCoin, state: tauri::State<'_, WalletState>, ) -> Result { @@ -237,7 +236,7 @@ pub async fn simulate_vesting_delegate_to_mixnode( #[tauri::command] pub async fn simulate_vesting_undelegate_from_mixnode( - mix_id: MixId, + mix_id: NodeId, state: tauri::State<'_, WalletState>, ) -> Result { simulate_vesting_operation( @@ -270,7 +269,7 @@ pub async fn simulate_vesting_claim_operator_reward( #[tauri::command] pub async fn simulate_vesting_claim_delegator_reward( - mix_id: MixId, + mix_id: NodeId, state: tauri::State<'_, WalletState>, ) -> Result { simulate_vesting_operation(ExecuteMsg::ClaimDelegatorReward { mix_id }, None, &state).await diff --git a/nym-wallet/src-tauri/src/operations/vesting/bond.rs b/nym-wallet/src-tauri/src/operations/vesting/bond.rs index 8e47953109..dfa7aec1f9 100644 --- a/nym-wallet/src-tauri/src/operations/vesting/bond.rs +++ b/nym-wallet/src-tauri/src/operations/vesting/bond.rs @@ -8,7 +8,7 @@ use crate::{Gateway, MixNode}; use nym_contracts_common::signing::MessageSignature; use nym_mixnet_contract_common::{GatewayConfigUpdate, MixNodeConfigUpdate}; use nym_types::currency::DecCoin; -use nym_types::mixnode::MixNodeCostParams; +use nym_types::mixnode::NodeCostParams; use nym_types::transaction::TransactionExecuteResult; use nym_validator_client::nyxd::{contract_traits::VestingSigningClient, Fee}; use std::cmp::Ordering; @@ -76,7 +76,7 @@ pub async fn vesting_unbond_gateway( #[tauri::command] pub async fn vesting_bond_mixnode( mixnode: MixNode, - cost_params: MixNodeCostParams, + cost_params: NodeCostParams, msg_signature: MessageSignature, pledge: DecCoin, fee: Option, @@ -284,7 +284,7 @@ pub async fn withdraw_vested_coins( #[tauri::command] pub async fn vesting_update_mixnode_cost_params( - new_costs: MixNodeCostParams, + new_costs: NodeCostParams, fee: Option, state: tauri::State<'_, WalletState>, ) -> Result { diff --git a/nym-wallet/src-tauri/src/operations/vesting/delegate.rs b/nym-wallet/src-tauri/src/operations/vesting/delegate.rs index 64ac3ed736..bbb1d8df11 100644 --- a/nym-wallet/src-tauri/src/operations/vesting/delegate.rs +++ b/nym-wallet/src-tauri/src/operations/vesting/delegate.rs @@ -3,14 +3,14 @@ use crate::error::BackendError; use crate::state::WalletState; -use nym_mixnet_contract_common::MixId; +use nym_mixnet_contract_common::NodeId; use nym_types::currency::DecCoin; use nym_types::transaction::TransactionExecuteResult; use nym_validator_client::nyxd::{contract_traits::VestingSigningClient, Fee}; #[tauri::command] pub async fn vesting_delegate_to_mixnode( - mix_id: MixId, + mix_id: NodeId, amount: DecCoin, fee: Option, state: tauri::State<'_, WalletState>, @@ -40,7 +40,7 @@ pub async fn vesting_delegate_to_mixnode( #[tauri::command] pub async fn vesting_undelegate_from_mixnode( - mix_id: MixId, + mix_id: NodeId, fee: Option, state: tauri::State<'_, WalletState>, ) -> Result { diff --git a/nym-wallet/src-tauri/src/operations/vesting/migrate.rs b/nym-wallet/src-tauri/src/operations/vesting/migrate.rs index e5624cfbf4..6d5fbadf16 100644 --- a/nym-wallet/src-tauri/src/operations/vesting/migrate.rs +++ b/nym-wallet/src-tauri/src/operations/vesting/migrate.rs @@ -68,7 +68,7 @@ pub async fn migrate_vested_delegations( for delegation in &vesting_delegations { migrate_msgs.push(( ExecuteMsg::MigrateVestedDelegation { - mix_id: delegation.mix_id, + mix_id: delegation.node_id, }, Vec::new(), )); diff --git a/nym-wallet/src-tauri/src/operations/vesting/rewards.rs b/nym-wallet/src-tauri/src/operations/vesting/rewards.rs index d990b2c1c5..7cf2155cdb 100644 --- a/nym-wallet/src-tauri/src/operations/vesting/rewards.rs +++ b/nym-wallet/src-tauri/src/operations/vesting/rewards.rs @@ -3,7 +3,7 @@ use crate::error::BackendError; use crate::state::WalletState; -use nym_mixnet_contract_common::MixId; +use nym_mixnet_contract_common::NodeId; use nym_types::transaction::TransactionExecuteResult; use nym_validator_client::nyxd::contract_traits::VestingSigningClient; use nym_validator_client::nyxd::Fee; @@ -30,7 +30,7 @@ pub async fn vesting_claim_operator_reward( #[tauri::command] pub async fn vesting_claim_delegator_reward( - mix_id: MixId, + mix_id: NodeId, fee: Option, state: tauri::State<'_, WalletState>, ) -> Result { diff --git a/nym-wallet/src-tauri/src/utils.rs b/nym-wallet/src-tauri/src/utils.rs index 2a340cc5dd..f294d4c09e 100644 --- a/nym-wallet/src-tauri/src/utils.rs +++ b/nym-wallet/src-tauri/src/utils.rs @@ -5,9 +5,9 @@ use crate::error::BackendError; use crate::nyxd_client; use crate::state::WalletState; use cosmwasm_std::Decimal; -use nym_mixnet_contract_common::{IdentityKey, MixId, Percent}; +use nym_mixnet_contract_common::{IdentityKey, NodeId, Percent}; use nym_types::currency::DecCoin; -use nym_types::mixnode::MixNodeCostParams; +use nym_types::mixnode::NodeCostParams; use nym_validator_client::nyxd::contract_traits::MixnetQueryClient; use nym_wallet_types::app::AppEnv; @@ -45,11 +45,20 @@ pub async fn owns_gateway(state: tauri::State<'_, WalletState>) -> Result) -> Result { + Ok(nyxd_client!(state) + .get_owned_nym_node(&nyxd_client!(state).address()) + .await? + .details + .is_some()) +} + #[tauri::command] pub async fn try_convert_pubkey_to_mix_id( state: tauri::State<'_, WalletState>, mix_identity: IdentityKey, -) -> Result, BackendError> { +) -> Result, BackendError> { let res = nyxd_client!(state) .get_mixnode_details_by_identity(mix_identity) .await?; @@ -62,7 +71,7 @@ pub async fn try_convert_pubkey_to_mix_id( pub async fn default_mixnode_cost_params( state: tauri::State<'_, WalletState>, profit_margin_percent: Percent, -) -> Result { +) -> Result { // attaches the old pre-update default operating cost of 40 nym per interval let guard = state.read().await; @@ -71,7 +80,7 @@ pub async fn default_mixnode_cost_params( let current_network = guard.current_network(); let denom = current_network.mix_denom().display; - Ok(MixNodeCostParams { + Ok(NodeCostParams { profit_margin_percent, interval_operating_cost: DecCoin { denom: denom.into(), diff --git a/nym-wallet/src/requests/queries.ts b/nym-wallet/src/requests/queries.ts index be74041f33..dbb7bb6e18 100644 --- a/nym-wallet/src/requests/queries.ts +++ b/nym-wallet/src/requests/queries.ts @@ -1,63 +1,65 @@ import { - DecCoin, - GatewayBond, - InclusionProbabilityResponse, - MixNodeDetails, - MixnodeStatusResponse, - PendingIntervalEvent, - RewardEstimationResponse, - StakeSaturationResponse, - WrappedDelegationEvent, + DecCoin, + GatewayBond, + InclusionProbabilityResponse, + MixNodeDetails, + MixnodeStatusResponse, + PendingIntervalEvent, + RewardEstimationResponse, + StakeSaturationResponse, + WrappedDelegationEvent, + NymNodeDetails, } from '@nymproject/types'; -import { Interval, TGatewayReport, TNodeDescription } from 'src/types'; -import { invokeWrapper } from './wrapper'; +import {Interval, TGatewayReport, TNodeDescription} from 'src/types'; +import {invokeWrapper} from './wrapper'; export const getAllPendingDelegations = async () => - invokeWrapper('get_pending_delegation_events'); + invokeWrapper('get_pending_delegation_events'); export const getMixnodeBondDetails = async () => invokeWrapper('mixnode_bond_details'); export const getGatewayBondDetails = async () => invokeWrapper('gateway_bond_details'); +export const getNymNodeBondDetails = async () => invokeWrapper('nym_node_bond_details'); export const getMixnodeAvgUptime = async () => invokeWrapper('get_mixnode_avg_uptime'); export const getPendingOperatorRewards = async (address: string) => - invokeWrapper('get_pending_operator_rewards', { address }); + invokeWrapper('get_pending_operator_rewards', {address}); export const getMixnodeStakeSaturation = async (mixId: number) => - invokeWrapper('mixnode_stake_saturation', { mixId }); + invokeWrapper('mixnode_stake_saturation', {mixId}); export const getMixnodeRewardEstimation = async (mixId: number) => - invokeWrapper('mixnode_reward_estimation', { mixId }); + invokeWrapper('mixnode_reward_estimation', {mixId}); export const getMixnodeStatus = async (mixId: number) => - invokeWrapper('mixnode_status', { mixId }); + invokeWrapper('mixnode_status', {mixId}); export const checkMixnodeOwnership = async () => invokeWrapper('owns_mixnode'); export const checkGatewayOwnership = async () => invokeWrapper('owns_gateway'); export const getInclusionProbability = async (mixId: number) => - invokeWrapper('mixnode_inclusion_probability', { mixId }); + invokeWrapper('mixnode_inclusion_probability', {mixId}); export const getCurrentInterval = async () => invokeWrapper('get_current_interval'); export const getNumberOfMixnodeDelegators = async (mixId: number) => - invokeWrapper('get_number_of_mixnode_delegators', { mixId }); + invokeWrapper('get_number_of_mixnode_delegators', {mixId}); export const getNodeDescription = async (host: string, port: number) => - invokeWrapper('get_mix_node_description', { host, port }); + invokeWrapper('get_mix_node_description', {host, port}); export const getPendingIntervalEvents = async () => - invokeWrapper('get_pending_interval_events'); + invokeWrapper('get_pending_interval_events'); export const getGatewayReport = async (identity: string) => - invokeWrapper('gateway_report', { identity }); + invokeWrapper('gateway_report', {identity}); export const computeMixnodeRewardEstimation = async (args: { - mixId: number; - performance: string; - pledgeAmount: number; - totalDelegation: number; - profitMarginPercent: string; - intervalOperatingCost: { denom: 'unym'; amount: string }; + mixId: number; + performance: string; + pledgeAmount: number; + totalDelegation: number; + profitMarginPercent: string; + intervalOperatingCost: { denom: 'unym'; amount: string }; }) => invokeWrapper('compute_mixnode_reward_estimation', args); -export const getMixnodeUptime = async (mixId: number) => invokeWrapper('get_mixnode_uptime', { mixId }); +export const getMixnodeUptime = async (mixId: number) => invokeWrapper('get_mixnode_uptime', {mixId}); diff --git a/nym-wallet/src/types/rust/AppEnv.ts b/nym-wallet/src/types/rust/AppEnv.ts index 861ecd9ef5..49ebfc27d1 100644 --- a/nym-wallet/src/types/rust/AppEnv.ts +++ b/nym-wallet/src/types/rust/AppEnv.ts @@ -1,5 +1,3 @@ -export interface AppEnv { - ADMIN_ADDRESS: string | null; - SHOW_TERMINAL: string | null; - ENABLE_QA_MODE: string | null; -} +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export interface AppEnv { ADMIN_ADDRESS: string | null, SHOW_TERMINAL: string | null, ENABLE_QA_MODE: string | null, } \ No newline at end of file diff --git a/nym-wallet/src/types/rust/AppVersion.ts b/nym-wallet/src/types/rust/AppVersion.ts index 82b94c5596..b0624301c1 100644 --- a/nym-wallet/src/types/rust/AppVersion.ts +++ b/nym-wallet/src/types/rust/AppVersion.ts @@ -1,7 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export interface AppVersion { - current_version: string; - latest_version: string; - is_update_available: boolean; -} +export interface AppVersion { current_version: string, latest_version: string, is_update_available: boolean, } \ No newline at end of file diff --git a/nym-wallet/src/types/rust/Interval.ts b/nym-wallet/src/types/rust/Interval.ts index c1526be658..ab2c4c4bc9 100644 --- a/nym-wallet/src/types/rust/Interval.ts +++ b/nym-wallet/src/types/rust/Interval.ts @@ -1,8 +1,3 @@ -export interface Interval { - id: number; - epochs_in_interval: number; - current_epoch_start_unix: bigint; - current_epoch_id: number; - epoch_length_seconds: bigint; - total_elapsed_epochs: number; -} +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export interface Interval { id: number, epochs_in_interval: number, current_epoch_start_unix: bigint, current_epoch_id: number, epoch_length_seconds: bigint, total_elapsed_epochs: number, } \ No newline at end of file diff --git a/nym-wallet/src/types/rust/Network.ts b/nym-wallet/src/types/rust/Network.ts index d55f0bce67..03c73643d6 100644 --- a/nym-wallet/src/types/rust/Network.ts +++ b/nym-wallet/src/types/rust/Network.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type Network = 'QA' | 'SANDBOX' | 'MAINNET'; +export type Network = "QA" | "SANDBOX" | "MAINNET"; \ No newline at end of file diff --git a/nym-wallet/src/types/rust/OperatingCostRange.ts b/nym-wallet/src/types/rust/OperatingCostRange.ts index b3877c8347..34ea2f65a4 100644 --- a/nym-wallet/src/types/rust/OperatingCostRange.ts +++ b/nym-wallet/src/types/rust/OperatingCostRange.ts @@ -1,7 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { DecCoin } from '@nymproject/types/src/types/rust/DecCoin'; +import type { DecCoin } from "../../../../ts-packages/types/src/types/rust/DecCoin"; -export interface TauriOperatingCostRange { - minimum: DecCoin; - maximum: DecCoin; -} +export interface TauriOperatingCostRange { minimum: DecCoin, maximum: DecCoin, } \ No newline at end of file diff --git a/nym-wallet/src/types/rust/ProfitMarginRange.ts b/nym-wallet/src/types/rust/ProfitMarginRange.ts index c12d49d5d9..631a4e1097 100644 --- a/nym-wallet/src/types/rust/ProfitMarginRange.ts +++ b/nym-wallet/src/types/rust/ProfitMarginRange.ts @@ -1,6 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export interface TauriProfitMarginRange { - minimum: string; - maximum: string; -} +export interface TauriProfitMarginRange { minimum: string, maximum: string, } \ No newline at end of file diff --git a/nym-wallet/src/types/rust/StateParams.ts b/nym-wallet/src/types/rust/StateParams.ts index b909e4c734..aa6eb0366a 100644 --- a/nym-wallet/src/types/rust/StateParams.ts +++ b/nym-wallet/src/types/rust/StateParams.ts @@ -1,12 +1,6 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { DecCoin } from '@nymproject/types/src/types/rust/DecCoin'; -import type { TauriOperatingCostRange } from './OperatingCostRange'; -import type { TauriProfitMarginRange } from './ProfitMarginRange'; +import type { DecCoin } from "../../../../ts-packages/types/src/types/rust/DecCoin"; +import type { TauriOperatingCostRange } from "./OperatingCostRange"; +import type { TauriProfitMarginRange } from "./ProfitMarginRange"; -export interface TauriContractStateParams { - minimum_mixnode_pledge: DecCoin; - minimum_gateway_pledge: DecCoin; - minimum_mixnode_delegation: DecCoin | null; - operating_cost: TauriOperatingCostRange; - profit_margin: TauriProfitMarginRange; -} +export interface TauriContractStateParams { minimum_pledge: DecCoin, minimum_delegation: DecCoin | null, operating_cost: TauriOperatingCostRange, profit_margin: TauriProfitMarginRange, } \ No newline at end of file diff --git a/nym-wallet/src/types/rust/ValidatorUrl.ts b/nym-wallet/src/types/rust/ValidatorUrl.ts index 00291a57cd..7e94537f97 100644 --- a/nym-wallet/src/types/rust/ValidatorUrl.ts +++ b/nym-wallet/src/types/rust/ValidatorUrl.ts @@ -1,6 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export interface ValidatorUrl { - url: string; - name: string | null; -} +export interface ValidatorUrl { url: string, name: string | null, } \ No newline at end of file diff --git a/nym-wallet/src/types/rust/ValidatorUrls.ts b/nym-wallet/src/types/rust/ValidatorUrls.ts index 57118d6c3f..9a579b2f41 100644 --- a/nym-wallet/src/types/rust/ValidatorUrls.ts +++ b/nym-wallet/src/types/rust/ValidatorUrls.ts @@ -1,6 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ValidatorUrl } from './ValidatorUrl'; +import type { ValidatorUrl } from "./ValidatorUrl"; -export interface ValidatorUrls { - urls: Array; -} +export interface ValidatorUrls { urls: Array, } \ No newline at end of file diff --git a/sdk/rust/nym-sdk/examples/custom_topology_provider.rs b/sdk/rust/nym-sdk/examples/custom_topology_provider.rs index d21916e6c9..6737c2dcae 100644 --- a/sdk/rust/nym-sdk/examples/custom_topology_provider.rs +++ b/sdk/rust/nym-sdk/examples/custom_topology_provider.rs @@ -4,7 +4,7 @@ use nym_sdk::mixnet; use nym_sdk::mixnet::MixnetMessageSender; use nym_topology::provider_trait::{async_trait, TopologyProvider}; -use nym_topology::{nym_topology_from_detailed, NymTopology}; +use nym_topology::{nym_topology_from_basic_info, NymTopology}; use url::Url; struct MyTopologyProvider { @@ -21,23 +21,25 @@ impl MyTopologyProvider { async fn get_topology(&self) -> NymTopology { let mixnodes = self .validator_client - .get_cached_active_mixnodes() + .get_basic_mixnodes(None) .await .unwrap(); - // in our topology provider only use mixnodes that have mix_id divisible by 3 - // and have more than 100k nym (i.e. 100'000'000'000 unym) in stake + // in our topology provider only use mixnodes that have node_id divisible by 3 + // and has exactly 100 performance score // why? because this is just an example to showcase arbitrary uses and capabilities of this trait let filtered_mixnodes = mixnodes .into_iter() - .filter(|mix| { - mix.mix_id() % 3 == 0 && mix.total_stake() > "100000000000".parse().unwrap() - }) + .filter(|mix| mix.node_id % 3 == 0 && mix.performance.is_hundred()) .collect::>(); - let gateways = self.validator_client.get_cached_gateways().await.unwrap(); + let gateways = self + .validator_client + .get_basic_gateways(None) + .await + .unwrap(); - nym_topology_from_detailed(filtered_mixnodes, gateways) + nym_topology_from_basic_info(&filtered_mixnodes, &gateways) } } diff --git a/sdk/rust/nym-sdk/examples/manually_overwrite_topology.rs b/sdk/rust/nym-sdk/examples/manually_overwrite_topology.rs index cf47bba50c..bfd62d1a8f 100644 --- a/sdk/rust/nym-sdk/examples/manually_overwrite_topology.rs +++ b/sdk/rust/nym-sdk/examples/manually_overwrite_topology.rs @@ -3,7 +3,7 @@ use nym_sdk::mixnet; use nym_sdk::mixnet::MixnetMessageSender; -use nym_topology::mix::Layer; +use nym_topology::mix::LegacyMixLayer; use nym_topology::{mix, NymTopology}; use std::collections::BTreeMap; @@ -19,7 +19,7 @@ async fn main() { let mut mixnodes = BTreeMap::new(); mixnodes.insert( 1, - vec![mix::Node { + vec![mix::LegacyNode { mix_id: 63, owner: None, host: "172.105.92.48".parse().unwrap(), @@ -30,13 +30,13 @@ async fn main() { sphinx_key: "CBmYewWf43iarBq349KhbfYMc9ys2ebXWd4Vp4CLQ5Rq" .parse() .unwrap(), - layer: Layer::One, + layer: LegacyMixLayer::One, version: "1.1.0".into(), }], ); mixnodes.insert( 2, - vec![mix::Node { + vec![mix::LegacyNode { mix_id: 23, owner: None, host: "178.79.143.65".parse().unwrap(), @@ -47,13 +47,13 @@ async fn main() { sphinx_key: "8ndjk5oZ6HxUZNScLJJ7hk39XtUqGexdKgW7hSX6kpWG" .parse() .unwrap(), - layer: Layer::Two, + layer: LegacyMixLayer::Two, version: "1.1.0".into(), }], ); mixnodes.insert( 3, - vec![mix::Node { + vec![mix::LegacyNode { mix_id: 66, owner: None, host: "139.162.247.97".parse().unwrap(), @@ -64,7 +64,7 @@ async fn main() { sphinx_key: "7KyZh8Z8KxuVunqytAJ2eXFuZkCS7BLTZSzujHJZsGa2" .parse() .unwrap(), - layer: Layer::Three, + layer: LegacyMixLayer::Three, version: "1.1.0".into(), }], ); diff --git a/tools/internal/testnet-manager/dkg-bypass-contract/Makefile b/tools/internal/testnet-manager/dkg-bypass-contract/Makefile index 2aa57e5ef9..126fde637f 100644 --- a/tools/internal/testnet-manager/dkg-bypass-contract/Makefile +++ b/tools/internal/testnet-manager/dkg-bypass-contract/Makefile @@ -1,5 +1,5 @@ all: build build: - RUSTFLAGS='-C link-arg=-s' cargo build --release --lib --target wasm32-unknown-unknown + RUSTFLAGS='-C link-arg=-s' cargo build --release --lib --target wasm32-unknown-unknown --no-default-features wasm-opt --signext-lowering -O ../../../../target/wasm32-unknown-unknown/release/dkg_bypass_contract.wasm -o ../../../../target/wasm32-unknown-unknown/release/dkg_bypass_contract.wasm \ No newline at end of file diff --git a/tools/internal/testnet-manager/src/manager/local_apis.rs b/tools/internal/testnet-manager/src/manager/local_apis.rs index 9fc92d17d0..0351ce4d2b 100644 --- a/tools/internal/testnet-manager/src/manager/local_apis.rs +++ b/tools/internal/testnet-manager/src/manager/local_apis.rs @@ -119,11 +119,11 @@ impl NetworkManager { let pub_id = &storage_paths["public_identity_key_file"] .as_str() .expect("nym-api config serialisation has changed"); - let ecash = &parsed_config["coconut_signer"] + let ecash = &parsed_config["ecash_signer"] .as_table() .expect("nym-api config serialisation has changed")["storage_paths"] .as_table() - .expect("nym-api config serialisation has changed")["coconut_key_path"] + .expect("nym-api config serialisation has changed")["ecash_key_path"] .as_str() .expect("nym-api config serialisation has changed"); diff --git a/tools/internal/testnet-manager/src/manager/local_nodes.rs b/tools/internal/testnet-manager/src/manager/local_nodes.rs index c8c9cf1fd6..d5ea4be530 100644 --- a/tools/internal/testnet-manager/src/manager/local_nodes.rs +++ b/tools/internal/testnet-manager/src/manager/local_nodes.rs @@ -7,7 +7,8 @@ use crate::manager::network::LoadedNetwork; use crate::manager::node::NymNode; use crate::manager::NetworkManager; use console::style; -use nym_mixnet_contract_common::{Layer, LayerAssignment}; +use nym_mixnet_contract_common::nym_node::Role; +use nym_mixnet_contract_common::RoleAssignment; use nym_validator_client::nyxd::contract_traits::{MixnetQueryClient, MixnetSigningClient}; use nym_validator_client::DirectSigningHttpRpcNyxdClient; use serde::{Deserialize, Serialize}; @@ -385,17 +386,92 @@ impl NetworkManager { let fut = rewarder.reconcile_epoch_events(None, None); ctx.async_with_progress(fut).await?; - ctx.set_pb_message("finally assigning the active set..."); - let fut = rewarder.get_rewarding_parameters(); - let rewarding_params = ctx.async_with_progress(fut).await?; - let active_set_size = rewarding_params.active_set_size; + ctx.set_pb_message("[BROKEN] finally assigning the active set..."); + // let fut = rewarder.get_rewarding_parameters(); + // let rewarding_params = ctx.async_with_progress(fut).await?; + // let active_set_size = rewarding_params.active_set_size; - let layer_assignment = vec![ - LayerAssignment::new(1, Layer::One), - LayerAssignment::new(2, Layer::Two), - LayerAssignment::new(3, Layer::Three), - ]; - let fut = rewarder.advance_current_epoch(layer_assignment, active_set_size, None); + let unused_variable = "this has to be fixed up and refactored...."; + /* + fn generate_role_assignment_messages( + &self, + rewarded_set: RewardedSet, + ) -> Vec<(ExecuteMsg, Vec)> { + // currently we just assign all of them together, + // but the contract is ready to handle them separately should we need it + // if the tx is too big + let mut msgs = Vec::new(); + for (role, nodes) in [ + (Role::ExitGateway, rewarded_set.exit_gateways), + (Role::EntryGateway, rewarded_set.entry_gateways), + (Role::Layer1, rewarded_set.layer1), + (Role::Layer2, rewarded_set.layer2), + (Role::Layer3, rewarded_set.layer3), + (Role::Standby, rewarded_set.standby), + ] { + msgs.push(( + ExecuteMsg::AssignRoles { + assignment: RoleAssignment { role, nodes }, + }, + Vec::new(), + )); + } + msgs + } + */ + + let fut = rewarder.assign_roles( + RoleAssignment { + role: Role::ExitGateway, + nodes: vec![4], + }, + None, + ); + ctx.async_with_progress(fut).await?; + + let fut = rewarder.assign_roles( + RoleAssignment { + role: Role::EntryGateway, + nodes: vec![], + }, + None, + ); + ctx.async_with_progress(fut).await?; + + let fut = rewarder.assign_roles( + RoleAssignment { + role: Role::Layer1, + nodes: vec![1], + }, + None, + ); + ctx.async_with_progress(fut).await?; + + let fut = rewarder.assign_roles( + RoleAssignment { + role: Role::Layer2, + nodes: vec![2], + }, + None, + ); + ctx.async_with_progress(fut).await?; + + let fut = rewarder.assign_roles( + RoleAssignment { + role: Role::Layer3, + nodes: vec![3], + }, + None, + ); + ctx.async_with_progress(fut).await?; + + let fut = rewarder.assign_roles( + RoleAssignment { + role: Role::Standby, + nodes: vec![], + }, + None, + ); ctx.async_with_progress(fut).await?; Ok(()) diff --git a/tools/internal/testnet-manager/src/manager/network_init.rs b/tools/internal/testnet-manager/src/manager/network_init.rs index 568632f734..bdbf39cf32 100644 --- a/tools/internal/testnet-manager/src/manager/network_init.rs +++ b/tools/internal/testnet-manager/src/manager/network_init.rs @@ -11,6 +11,7 @@ use cw_utils::Threshold; use indicatif::HumanDuration; use nym_coconut_dkg_common::types::TimeConfiguration; use nym_config::defaults::NymNetworkDetails; +use nym_mixnet_contract_common::reward_params::RewardedSetParams; use nym_mixnet_contract_common::{Decimal, InitialRewardingParams, Percent}; use nym_validator_client::nyxd::cosmwasm_client::types::InstantiateOptions; use nym_validator_client::nyxd::Config; @@ -151,8 +152,12 @@ impl NetworkManager { sybil_resistance: Percent::from_percentage_value(30).unwrap(), active_set_work_factor: Decimal::from_atomics(10u32, 0).unwrap(), interval_pool_emission: Percent::from_percentage_value(2).unwrap(), - rewarded_set_size: 240, - active_set_size: 240, + rewarded_set_params: RewardedSetParams { + entry_gateways: 70, + exit_gateways: 50, + mixnodes: 120, + standby: 0, + }, }, profit_margin: Default::default(), interval_operating_cost: Default::default(), diff --git a/tools/internal/testnet-manager/src/manager/node.rs b/tools/internal/testnet-manager/src/manager/node.rs index e0c6d6981f..3cf3443c24 100644 --- a/tools/internal/testnet-manager/src/manager/node.rs +++ b/tools/internal/testnet-manager/src/manager/node.rs @@ -7,7 +7,7 @@ use nym_contracts_common::signing::MessageSignature; use nym_contracts_common::Percent; use nym_mixnet_contract_common::{ construct_gateway_bonding_sign_payload, construct_mixnode_bonding_sign_payload, Gateway, - MixNode, MixNodeCostParams, + MixNode, NodeCostParams, }; use nym_validator_client::nyxd::CosmWasmCoin; @@ -68,8 +68,8 @@ impl NymNode { } } - pub(crate) fn cost_params(&self) -> MixNodeCostParams { - MixNodeCostParams { + pub(crate) fn cost_params(&self) -> NodeCostParams { + NodeCostParams { profit_margin_percent: Percent::from_percentage_value(10).unwrap(), interval_operating_cost: CosmWasmCoin::new(40_000000, "unym"), } diff --git a/tools/nym-cli/src/validator/mixnet/operators/gateways/mod.rs b/tools/nym-cli/src/validator/mixnet/operators/gateways/mod.rs index 630ebccb00..d5a525faba 100644 --- a/tools/nym-cli/src/validator/mixnet/operators/gateways/mod.rs +++ b/tools/nym-cli/src/validator/mixnet/operators/gateways/mod.rs @@ -1,36 +1,26 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use anyhow::bail; use nym_cli_commands::context::{create_signing_client, ClientArgs}; +use nym_cli_commands::validator::mixnet::operators::gateway::{ + MixnetOperatorsGateway, MixnetOperatorsGatewayCommands, +}; use nym_network_defaults::NymNetworkDetails; pub(crate) mod settings; pub(crate) async fn execute( global_args: ClientArgs, - gateway: nym_cli_commands::validator::mixnet::operators::gateway::MixnetOperatorsGateway, + gateway: MixnetOperatorsGateway, network_details: &NymNetworkDetails, ) -> anyhow::Result<()> { match gateway.command { - nym_cli_commands::validator::mixnet::operators::gateway::MixnetOperatorsGatewayCommands::Bond(args) => { - nym_cli_commands::validator::mixnet::operators::gateway::bond_gateway::bond_gateway(args, create_signing_client(global_args, network_details)?).await - }, - nym_cli_commands::validator::mixnet::operators::gateway::MixnetOperatorsGatewayCommands::Unbond(_args) => { + MixnetOperatorsGatewayCommands::Unbond(_args) => { nym_cli_commands::validator::mixnet::operators::gateway::unbond_gateway::unbond_gateway(create_signing_client(global_args, network_details)?).await }, - nym_cli_commands::validator::mixnet::operators::gateway::MixnetOperatorsGatewayCommands::CreateGatewayBondingSignPayload(args) => { - nym_cli_commands::validator::mixnet::operators::gateway::gateway_bonding_sign_payload::create_payload(args,create_signing_client(global_args, network_details)?).await - } - nym_cli_commands::validator::mixnet::operators::gateway::MixnetOperatorsGatewayCommands::Settings(settings) => { - settings::execute(global_args, settings, network_details).await? - } - nym_cli_commands::validator::mixnet::operators::gateway::MixnetOperatorsGatewayCommands::VestingBond(args) => { - nym_cli_commands::validator::mixnet::operators::gateway::vesting_bond_gateway::vesting_bond_gateway(args, create_signing_client(global_args, network_details)?).await - } - nym_cli_commands::validator::mixnet::operators::gateway::MixnetOperatorsGatewayCommands::VestingUnbond(_args) => { - nym_cli_commands::validator::mixnet::operators::gateway::vesting_unbond_gateway::vesting_unbond_gateway(create_signing_client(global_args, network_details)?).await - - } + MixnetOperatorsGatewayCommands::MigrateToNymnode(args) => nym_cli_commands::validator::mixnet::operators::gateway::nymnode_migration::migrate_to_nymnode(args, create_signing_client(global_args, network_details)?).await, + _ => bail!("this command is no longer available. please migrate your mixnode into a Nym-Node via `migrate-to-nymnode` command") } Ok(()) } diff --git a/tools/nym-cli/src/validator/mixnet/operators/gateways/settings/mod.rs b/tools/nym-cli/src/validator/mixnet/operators/gateways/settings/mod.rs index b909063125..d87ee0d37a 100644 --- a/tools/nym-cli/src/validator/mixnet/operators/gateways/settings/mod.rs +++ b/tools/nym-cli/src/validator/mixnet/operators/gateways/settings/mod.rs @@ -4,6 +4,7 @@ use nym_cli_commands::context::{create_signing_client, ClientArgs}; use nym_network_defaults::NymNetworkDetails; +#[allow(dead_code)] pub(crate) async fn execute( global_args: ClientArgs, settings: nym_cli_commands::validator::mixnet::operators::gateway::settings::MixnetOperatorsGatewaySettings, diff --git a/tools/nym-cli/src/validator/mixnet/operators/mixnodes/families/mod.rs b/tools/nym-cli/src/validator/mixnet/operators/mixnodes/families/mod.rs deleted file mode 100644 index 0f10e79fa3..0000000000 --- a/tools/nym-cli/src/validator/mixnet/operators/mixnodes/families/mod.rs +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use nym_cli_commands::context::{create_query_client, create_signing_client, ClientArgs}; -use nym_cli_commands::validator::mixnet::operators::mixnode::families::MixnetOperatorsMixnodeFamiliesCommands; -use nym_network_defaults::NymNetworkDetails; - -pub(crate) async fn execute( - global_args: ClientArgs, - families: nym_cli_commands::validator::mixnet::operators::mixnode::families::MixnetOperatorsMixnodeFamilies, - network_details: &NymNetworkDetails, -) -> anyhow::Result<()> { - match families.command { - MixnetOperatorsMixnodeFamiliesCommands::CreateFamily(args) => { - nym_cli_commands::validator::mixnet::operators::mixnode::families::create_family::create_family( - args, - create_signing_client(global_args, network_details)?, - ) - .await - } - MixnetOperatorsMixnodeFamiliesCommands::JoinFamily(args) => { - nym_cli_commands::validator::mixnet::operators::mixnode::families::join_family::join_family( - args, - create_signing_client(global_args, network_details)?, - ) - .await - } - MixnetOperatorsMixnodeFamiliesCommands::LeaveFamily(args) => { - nym_cli_commands::validator::mixnet::operators::mixnode::families::leave_family::leave_family( - args, - create_signing_client(global_args, network_details)?, - ) - .await - } - MixnetOperatorsMixnodeFamiliesCommands::KickFamilyMember(args) => { - nym_cli_commands::validator::mixnet::operators::mixnode::families::kick_family_member::kick_family_member( - args, - create_signing_client(global_args, network_details)?, - ) - .await - } - MixnetOperatorsMixnodeFamiliesCommands::CreateFamilyJoinPermitSignPayload(args) => { - nym_cli_commands::validator::mixnet::operators::mixnode::families::create_family_join_permit_sign_payload::create_family_join_permit_sign_payload(args, create_query_client(network_details)?).await - } - } - Ok(()) -} diff --git a/tools/nym-cli/src/validator/mixnet/operators/mixnodes/keys/mod.rs b/tools/nym-cli/src/validator/mixnet/operators/mixnodes/keys/mod.rs index 3923093dbb..c881e4b9c4 100644 --- a/tools/nym-cli/src/validator/mixnet/operators/mixnodes/keys/mod.rs +++ b/tools/nym-cli/src/validator/mixnet/operators/mixnodes/keys/mod.rs @@ -1,6 +1,7 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +#[allow(dead_code)] pub(crate) async fn execute( keys: nym_cli_commands::validator::mixnet::operators::mixnode::keys::MixnetOperatorsMixnodeKeys, ) -> anyhow::Result<()> { diff --git a/tools/nym-cli/src/validator/mixnet/operators/mixnodes/mod.rs b/tools/nym-cli/src/validator/mixnet/operators/mixnodes/mod.rs index ac2d301f4f..4ebe8e94d8 100644 --- a/tools/nym-cli/src/validator/mixnet/operators/mixnodes/mod.rs +++ b/tools/nym-cli/src/validator/mixnet/operators/mixnodes/mod.rs @@ -1,10 +1,11 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use anyhow::bail; use nym_cli_commands::context::{create_signing_client, ClientArgs}; +use nym_cli_commands::validator::mixnet::operators::mixnode::MixnetOperatorsMixnodeCommands; use nym_network_defaults::NymNetworkDetails; -pub(crate) mod families; pub(crate) mod keys; pub(crate) mod rewards; pub(crate) mod settings; @@ -15,43 +16,14 @@ pub(crate) async fn execute( network_details: &NymNetworkDetails, ) -> anyhow::Result<()> { match mixnode.command { - nym_cli_commands::validator::mixnet::operators::mixnode::MixnetOperatorsMixnodeCommands::Keys(keys) => { - keys::execute(keys).await? - } - nym_cli_commands::validator::mixnet::operators::mixnode::MixnetOperatorsMixnodeCommands::Rewards(rewards) => { - rewards::execute(global_args, rewards, network_details).await? - } - nym_cli_commands::validator::mixnet::operators::mixnode::MixnetOperatorsMixnodeCommands::Settings(settings) => { - settings::execute(global_args, settings, network_details).await? - } - nym_cli_commands::validator::mixnet::operators::mixnode::MixnetOperatorsMixnodeCommands::Families(families) => { - families::execute(global_args, families, network_details).await? - } - nym_cli_commands::validator::mixnet::operators::mixnode::MixnetOperatorsMixnodeCommands::Bond(args) => { - nym_cli_commands::validator::mixnet::operators::mixnode::bond_mixnode::bond_mixnode(args, create_signing_client(global_args, network_details)?).await - } - nym_cli_commands::validator::mixnet::operators::mixnode::MixnetOperatorsMixnodeCommands::Unbond(args) => { + MixnetOperatorsMixnodeCommands::Unbond(args) => { nym_cli_commands::validator::mixnet::operators::mixnode::unbond_mixnode::unbond_mixnode(args, create_signing_client(global_args, network_details)?).await } - nym_cli_commands::validator::mixnet::operators::mixnode::MixnetOperatorsMixnodeCommands::CreateMixnodeBondingSignPayload(args) => { - nym_cli_commands::validator::mixnet::operators::mixnode::mixnode_bonding_sign_payload::create_payload(args,create_signing_client(global_args, network_details)?).await - } - nym_cli_commands::validator::mixnet::operators::mixnode::MixnetOperatorsMixnodeCommands::PledgeMore(args) => { - nym_cli_commands::validator::mixnet::operators::mixnode::pledge_more::pledge_more(args, create_signing_client(global_args, network_details)?).await - } - nym_cli_commands::validator::mixnet::operators::mixnode::MixnetOperatorsMixnodeCommands::PledgeMoreVesting(args) => { - nym_cli_commands::validator::mixnet::operators::mixnode::vesting_pledge_more::vesting_pledge_more(args, create_signing_client(global_args, network_details)?).await - } - nym_cli_commands::validator::mixnet::operators::mixnode::MixnetOperatorsMixnodeCommands::DecreasePledge(args) => { - nym_cli_commands::validator::mixnet::operators::mixnode::decrease_pledge::decrease_pledge(args, create_signing_client(global_args, network_details)?).await - } - nym_cli_commands::validator::mixnet::operators::mixnode::MixnetOperatorsMixnodeCommands::DecreasePledgeVesting(args) => { - nym_cli_commands::validator::mixnet::operators::mixnode::vesting_decrease_pledge::vesting_decrease_pledge(args, create_signing_client(global_args, network_details)?).await - } - nym_cli_commands::validator::mixnet::operators::mixnode::MixnetOperatorsMixnodeCommands::MigrateVestedNode(args) => { + MixnetOperatorsMixnodeCommands::MigrateVestedNode(args) => { nym_cli_commands::validator::mixnet::operators::mixnode::migrate_vested_mixnode::migrate_vested_mixnode(args, create_signing_client(global_args, network_details)?).await } - _ => unreachable!() + MixnetOperatorsMixnodeCommands::MigrateToNymnode(args) => nym_cli_commands::validator::mixnet::operators::mixnode::nymnode_migration::migrate_to_nymnode(args, create_signing_client(global_args, network_details)?).await, + _ => bail!("this command is no longer available. please migrate your mixnode into a Nym-Node via `migrate-to-nymnode` command") } Ok(()) } diff --git a/tools/nym-cli/src/validator/mixnet/operators/mixnodes/rewards/mod.rs b/tools/nym-cli/src/validator/mixnet/operators/mixnodes/rewards/mod.rs index d485a5131b..b93f981c5b 100644 --- a/tools/nym-cli/src/validator/mixnet/operators/mixnodes/rewards/mod.rs +++ b/tools/nym-cli/src/validator/mixnet/operators/mixnodes/rewards/mod.rs @@ -4,6 +4,7 @@ use nym_cli_commands::context::{create_signing_client, ClientArgs}; use nym_network_defaults::NymNetworkDetails; +#[allow(dead_code)] pub(crate) async fn execute( global_args: ClientArgs, rewards: nym_cli_commands::validator::mixnet::operators::mixnode::rewards::MixnetOperatorsMixnodeRewards, diff --git a/tools/nym-cli/src/validator/mixnet/operators/mixnodes/settings/mod.rs b/tools/nym-cli/src/validator/mixnet/operators/mixnodes/settings/mod.rs index a5d5c02883..a431c19ab0 100644 --- a/tools/nym-cli/src/validator/mixnet/operators/mixnodes/settings/mod.rs +++ b/tools/nym-cli/src/validator/mixnet/operators/mixnodes/settings/mod.rs @@ -4,6 +4,7 @@ use nym_cli_commands::context::{create_signing_client, ClientArgs}; use nym_network_defaults::NymNetworkDetails; +#[allow(dead_code)] pub(crate) async fn execute( global_args: ClientArgs, settings: nym_cli_commands::validator::mixnet::operators::mixnode::settings::MixnetOperatorsMixnodeSettings, @@ -14,7 +15,7 @@ pub(crate) async fn execute( nym_cli_commands::validator::mixnet::operators::mixnode::settings::update_config::update_config(args, create_signing_client(global_args, network_details)?).await } nym_cli_commands::validator::mixnet::operators::mixnode::settings::MixnetOperatorsMixnodeSettingsCommands::UpdateCostParameters(args) => { - nym_cli_commands::validator::mixnet::operators::mixnode::settings::update_cost_params::update_cost_params(args, create_signing_client(global_args, network_details)?).await + nym_cli_commands::validator::mixnet::operators::mixnode::settings::update_cost_params::update_cost_params(args, create_signing_client(global_args, network_details)?).await? } _ => unreachable!(), } diff --git a/tools/nym-cli/src/validator/mixnet/operators/mod.rs b/tools/nym-cli/src/validator/mixnet/operators/mod.rs index 0e14e36a21..9739f2f428 100644 --- a/tools/nym-cli/src/validator/mixnet/operators/mod.rs +++ b/tools/nym-cli/src/validator/mixnet/operators/mod.rs @@ -2,11 +2,13 @@ // SPDX-License-Identifier: Apache-2.0 use nym_cli_commands::context::ClientArgs; +use nym_cli_commands::validator::mixnet::operators::MixnetOperatorsCommands; use nym_network_defaults::NymNetworkDetails; pub(crate) mod gateways; pub(crate) mod identity_key; pub(crate) mod mixnodes; +mod nymnodes; pub(crate) async fn execute( global_args: ClientArgs, @@ -14,14 +16,17 @@ pub(crate) async fn execute( network_details: &NymNetworkDetails, ) -> anyhow::Result<()> { match operators.command { - nym_cli_commands::validator::mixnet::operators::MixnetOperatorsCommands::Gateway( - gateway, - ) => gateways::execute(global_args, gateway, network_details).await, - nym_cli_commands::validator::mixnet::operators::MixnetOperatorsCommands::Mixnode( - mixnode, - ) => mixnodes::execute(global_args, mixnode, network_details).await, - nym_cli_commands::validator::mixnet::operators::MixnetOperatorsCommands::IdentityKey( - identity_key, - ) => identity_key::execute(global_args, identity_key, network_details).await, + MixnetOperatorsCommands::Nymnode(nymnode) => { + nymnodes::execute(global_args, nymnode, network_details).await + } + MixnetOperatorsCommands::Gateway(gateway) => { + gateways::execute(global_args, gateway, network_details).await + } + MixnetOperatorsCommands::Mixnode(mixnode) => { + mixnodes::execute(global_args, mixnode, network_details).await + } + MixnetOperatorsCommands::IdentityKey(identity_key) => { + identity_key::execute(global_args, identity_key, network_details).await + } } } diff --git a/tools/nym-cli/src/validator/mixnet/operators/nymnodes/keys.rs b/tools/nym-cli/src/validator/mixnet/operators/nymnodes/keys.rs new file mode 100644 index 0000000000..b1a0554d42 --- /dev/null +++ b/tools/nym-cli/src/validator/mixnet/operators/nymnodes/keys.rs @@ -0,0 +1,15 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_cli_commands::validator::mixnet::operators::nymnode::keys::MixnetOperatorsNymNodeKeysCommands; + +pub(crate) async fn execute( + keys: nym_cli_commands::validator::mixnet::operators::nymnode::keys::MixnetOperatorsNymNodeKeys, +) -> anyhow::Result<()> { + match keys.command { + MixnetOperatorsNymNodeKeysCommands::DecodeNodeKey(args) => { + nym_cli_commands::validator::mixnet::operators::nymnode::keys::decode_node_key::decode_node_key(args) + } + } + Ok(()) +} diff --git a/tools/nym-cli/src/validator/mixnet/operators/nymnodes/mod.rs b/tools/nym-cli/src/validator/mixnet/operators/nymnodes/mod.rs new file mode 100644 index 0000000000..24bd998aa5 --- /dev/null +++ b/tools/nym-cli/src/validator/mixnet/operators/nymnodes/mod.rs @@ -0,0 +1,49 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_cli_commands::context::{create_signing_client, ClientArgs}; +use nym_cli_commands::validator::mixnet::operators::nymnode::bond_nymnode::bond_nymnode; +use nym_cli_commands::validator::mixnet::operators::nymnode::unbond_nymnode::unbond_nymnode; +use nym_cli_commands::validator::mixnet::operators::nymnode::{ + nymnode_bonding_sign_payload, MixnetOperatorsNymNodeCommands, +}; +use nym_network_defaults::NymNetworkDetails; + +pub(crate) mod keys; +pub(crate) mod pledge; +pub(crate) mod rewards; +pub(crate) mod settings; + +pub(crate) async fn execute( + global_args: ClientArgs, + nymnode: nym_cli_commands::validator::mixnet::operators::nymnode::MixnetOperatorsNymNode, + network_details: &NymNetworkDetails, +) -> anyhow::Result<()> { + match nymnode.command { + MixnetOperatorsNymNodeCommands::CreateNodeBondingSignPayload(args) => { + nymnode_bonding_sign_payload::create_payload( + args, + create_signing_client(global_args, network_details)?, + ) + .await + } + MixnetOperatorsNymNodeCommands::Keys(keys) => keys::execute(keys).await?, + MixnetOperatorsNymNodeCommands::Rewards(rewards) => { + rewards::execute(global_args, rewards, network_details).await? + } + MixnetOperatorsNymNodeCommands::Settings(settings) => { + settings::execute(global_args, settings, network_details).await? + } + MixnetOperatorsNymNodeCommands::Pledge(pledge) => { + pledge::execute(global_args, pledge, network_details).await? + } + MixnetOperatorsNymNodeCommands::Bond(args) => { + bond_nymnode(args, create_signing_client(global_args, network_details)?).await + } + MixnetOperatorsNymNodeCommands::Unbond(args) => { + unbond_nymnode(args, create_signing_client(global_args, network_details)?).await + } + } + + Ok(()) +} diff --git a/tools/nym-cli/src/validator/mixnet/operators/nymnodes/pledge.rs b/tools/nym-cli/src/validator/mixnet/operators/nymnodes/pledge.rs new file mode 100644 index 0000000000..d27a91b422 --- /dev/null +++ b/tools/nym-cli/src/validator/mixnet/operators/nymnodes/pledge.rs @@ -0,0 +1,22 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_cli_commands::context::{create_signing_client, ClientArgs}; +use nym_cli_commands::validator::mixnet::operators::nymnode::pledge::MixnetOperatorsNymNodePledgeCommands; +use nym_network_defaults::NymNetworkDetails; + +pub(crate) async fn execute( + global_args: ClientArgs, + pledge: nym_cli_commands::validator::mixnet::operators::nymnode::pledge::MixnetOperatorsNymNodePledge, + network_details: &NymNetworkDetails, +) -> anyhow::Result<()> { + match pledge.command { + MixnetOperatorsNymNodePledgeCommands::Increase(args) => { + nym_cli_commands::validator::mixnet::operators::nymnode::pledge::increase_pledge::increase_pledge(args, create_signing_client(global_args, network_details)?).await + }, + MixnetOperatorsNymNodePledgeCommands::Decrease(args) => { + nym_cli_commands::validator::mixnet::operators::nymnode::pledge::decrease_pledge::decrease_pledge(args, create_signing_client(global_args, network_details)?).await + } + } + Ok(()) +} diff --git a/tools/nym-cli/src/validator/mixnet/operators/nymnodes/rewards.rs b/tools/nym-cli/src/validator/mixnet/operators/nymnodes/rewards.rs new file mode 100644 index 0000000000..261449d41b --- /dev/null +++ b/tools/nym-cli/src/validator/mixnet/operators/nymnodes/rewards.rs @@ -0,0 +1,19 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_cli_commands::context::{create_signing_client, ClientArgs}; +use nym_cli_commands::validator::mixnet::operators::nymnode::rewards::MixnetOperatorsNymNodeRewardsCommands; +use nym_network_defaults::NymNetworkDetails; + +pub(crate) async fn execute( + global_args: ClientArgs, + rewards: nym_cli_commands::validator::mixnet::operators::nymnode::rewards::MixnetOperatorsNymNodeRewards, + network_details: &NymNetworkDetails, +) -> anyhow::Result<()> { + match rewards.command { + MixnetOperatorsNymNodeRewardsCommands::Claim(args) => { + nym_cli_commands::validator::mixnet::operators::nymnode::rewards::claim_operator_reward::claim_operator_reward(args, create_signing_client(global_args, network_details)?).await + } + } + Ok(()) +} diff --git a/tools/nym-cli/src/validator/mixnet/operators/nymnodes/settings.rs b/tools/nym-cli/src/validator/mixnet/operators/nymnodes/settings.rs new file mode 100644 index 0000000000..babb8e4244 --- /dev/null +++ b/tools/nym-cli/src/validator/mixnet/operators/nymnodes/settings.rs @@ -0,0 +1,22 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_cli_commands::context::{create_signing_client, ClientArgs}; +use nym_cli_commands::validator::mixnet::operators::nymnode::settings::MixnetOperatorsNymNodeSettingsCommands; +use nym_network_defaults::NymNetworkDetails; + +pub(crate) async fn execute( + global_args: ClientArgs, + settings: nym_cli_commands::validator::mixnet::operators::nymnode::settings::MixnetOperatorsNymNodeSettings, + network_details: &NymNetworkDetails, +) -> anyhow::Result<()> { + match settings.command { + MixnetOperatorsNymNodeSettingsCommands::UpdateConfig(args) => { + nym_cli_commands::validator::mixnet::operators::nymnode::settings::update_config::update_config(args, create_signing_client(global_args, network_details)?).await + }, + MixnetOperatorsNymNodeSettingsCommands::UpdateCostParameters(args) => { + nym_cli_commands::validator::mixnet::operators::nymnode::settings::update_cost_params::update_cost_params(args, create_signing_client(global_args, network_details)?).await? + } + } + Ok(()) +} diff --git a/tools/ts-rs-cli/src/main.rs b/tools/ts-rs-cli/src/main.rs index e3728bd234..8a7720664f 100644 --- a/tools/ts-rs-cli/src/main.rs +++ b/tools/ts-rs-cli/src/main.rs @@ -6,7 +6,7 @@ use nym_api_requests::models::{ use nym_mixnet_contract_common::rewarding::RewardEstimate; use nym_mixnet_contract_common::{ GatewayConfigUpdate, Interval as ContractInterval, IntervalRewardParams, - IntervalRewardingParamsUpdate, MixNode, MixNodeConfigUpdate, RewardedSetNodeStatus, + IntervalRewardingParamsUpdate, MixNode, MixNodeConfigUpdate, NymNode, PendingNodeChanges, RewardingParams, UnbondedMixnode, }; use nym_types::account::{Account, AccountEntry, AccountWithMnemonic, Balance}; @@ -18,7 +18,8 @@ use nym_types::deprecated::{DelegationEvent, DelegationEventKind, WrappedDelegat use nym_types::fees::{self, FeeDetails}; use nym_types::gas::{Gas, GasInfo}; use nym_types::gateway::{Gateway, GatewayBond}; -use nym_types::mixnode::{MixNodeBond, MixNodeCostParams, MixNodeDetails, MixNodeRewarding}; +use nym_types::mixnode::{MixNodeBond, MixNodeDetails, NodeCostParams, NodeRewarding}; +use nym_types::nym_node::{NymNodeBond, NymNodeDetails}; use nym_types::pending_events::{ PendingEpochEvent, PendingEpochEventData, PendingIntervalEvent, PendingIntervalEventData, }; @@ -69,10 +70,11 @@ fn main() { do_export!(MixNode); do_export!(MixNodeConfigUpdate); do_export!(RewardingParams); - do_export!(RewardedSetNodeStatus); do_export!(UnbondedMixnode); do_export!(RewardEstimate); do_export!(ContractInterval); + do_export!(NymNode); + do_export!(PendingNodeChanges); // common/types/src do_export!(Account); @@ -99,9 +101,14 @@ fn main() { do_export!(CurrencyDenom); do_export!(DecCoin); do_export!(MixNodeBond); - do_export!(MixNodeCostParams); + do_export!(NodeCostParams); do_export!(MixNodeDetails); - do_export!(MixNodeRewarding); + + // for nym-node: + do_export!(NymNodeDetails); + do_export!(NymNodeBond); + + do_export!(NodeRewarding); do_export!(OriginalVestingResponse); do_export!(PendingEpochEvent); do_export!(PendingEpochEventData); @@ -155,6 +162,7 @@ fn main() { && !path.starts_with("./Cargo.toml") && !path.starts_with("./.gitignore") && f.file_type().is_file() + && f.path().extension() == Some("ts".as_ref()) }) { // construct the source and destination paths that can be used to replace the output file diff --git a/ts-packages/types/src/types/rust/NodeRewarding.ts b/ts-packages/types/src/types/rust/NodeRewarding.ts new file mode 100644 index 0000000000..3fa37c30bd --- /dev/null +++ b/ts-packages/types/src/types/rust/NodeRewarding.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { NodeCostParams } from "./MixNodeCostParams"; + +export interface NodeRewarding { cost_params: NodeCostParams, operator: string, delegates: string, total_unit_reward: string, unit_delegation: string, last_rewarded_epoch: number, unique_delegations: number, } \ No newline at end of file diff --git a/ts-packages/types/src/types/rust/NymNode.ts b/ts-packages/types/src/types/rust/NymNode.ts new file mode 100644 index 0000000000..78bddc8007 --- /dev/null +++ b/ts-packages/types/src/types/rust/NymNode.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export interface NymNode { host: string, custom_http_port: number | null, identity_key: string, } \ No newline at end of file diff --git a/ts-packages/types/src/types/rust/NymNodeBond.ts b/ts-packages/types/src/types/rust/NymNodeBond.ts new file mode 100644 index 0000000000..b8f608f2c7 --- /dev/null +++ b/ts-packages/types/src/types/rust/NymNodeBond.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { DecCoin } from "./DecCoin"; + +export interface NymNodeBond { node_id: number, owner: string, original_pledge: DecCoin, bonding_height: bigint, is_unbonding: boolean, host: string, custom_http_port: number | null, identity_key: string, } \ No newline at end of file diff --git a/ts-packages/types/src/types/rust/NymNodeDetails.ts b/ts-packages/types/src/types/rust/NymNodeDetails.ts new file mode 100644 index 0000000000..7b6c0b5f2a --- /dev/null +++ b/ts-packages/types/src/types/rust/NymNodeDetails.ts @@ -0,0 +1,6 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { NodeRewarding } from "./NodeRewarding"; +import type { NymNodeBond } from "./NymNodeBond"; +import type { PendingNodeChanges } from "./PendingNodeChanges"; + +export interface NymNodeDetails { bond_information: NymNodeBond, rewarding_details: NodeRewarding, pending_changes: PendingNodeChanges, } \ No newline at end of file diff --git a/ts-packages/types/src/types/rust/PendingNodeChanges.ts b/ts-packages/types/src/types/rust/PendingNodeChanges.ts new file mode 100644 index 0000000000..3018f1e041 --- /dev/null +++ b/ts-packages/types/src/types/rust/PendingNodeChanges.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export interface PendingNodeChanges { pledge_change: number | null, cost_params_change: number | null, } \ No newline at end of file diff --git a/ts-packages/types/src/types/rust/index.ts b/ts-packages/types/src/types/rust/index.ts index af9d8b5c99..02f817b113 100644 --- a/ts-packages/types/src/types/rust/index.ts +++ b/ts-packages/types/src/types/rust/index.ts @@ -54,3 +54,7 @@ export * from './UnbondedMixnode'; export * from './VestingAccountInfo'; export * from './VestingPeriod'; export * from './WrappedDelegationEvent'; +export * from './NymNode'; +export * from './NymNodeBond'; +export * from './NymNodeDetails'; +export * from './NodeRewarding';