diff --git a/Cargo.lock b/Cargo.lock index f70dc814233..eb1407a66d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6293,6 +6293,23 @@ dependencies = [ "tracing", ] +[[package]] +name = "nym-mixnet-contract" +version = "1.5.1" +dependencies = [ + "bs58", + "cosmwasm-std", + "cw-controllers", + "cw-storage-plus", + "cw2", + "nym-contracts-common", + "nym-contracts-common-testing", + "nym-mixnet-contract-common", + "nym-vesting-contract-common", + "semver 1.0.26", + "serde", +] + [[package]] name = "nym-mixnet-contract-common" version = "0.6.0" @@ -6812,6 +6829,25 @@ dependencies = [ "zeroize", ] +[[package]] +name = "nym-performance-contract" +version = "0.1.0" +dependencies = [ + "anyhow", + "cosmwasm-schema", + "cosmwasm-std", + "cw-controllers", + "cw-storage-plus", + "cw2", + "nym-contracts-common", + "nym-contracts-common-testing", + "nym-crypto", + "nym-mixnet-contract", + "nym-mixnet-contract-common", + "nym-performance-contract-common", + "serde", +] + [[package]] name = "nym-performance-contract-common" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 85e43d76e2b..3a761308b35 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,7 @@ members = [ "common/nym-common", "common/config", "common/cosmwasm-smart-contracts/coconut-dkg", + "contracts/performance", "common/cosmwasm-smart-contracts/contracts-common", "common/cosmwasm-smart-contracts/contracts-common-testing", "common/cosmwasm-smart-contracts/easy_addr", diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/performance_query_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/performance_query_client.rs index 470a412e8d3..268e591be8a 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/performance_query_client.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/performance_query_client.rs @@ -14,7 +14,7 @@ pub use nym_performance_contract_common::{ EpochMeasurementsPagedResponse, EpochNodePerformance, EpochPerformancePagedResponse, FullHistoricalPerformancePagedResponse, HistoricalPerformance, LastSubmission, NetworkMonitorInformation, NetworkMonitorsPagedResponse, NodeId, NodeMeasurement, - NodeMeasurementsResponse, NodePerformance, NodePerformancePagedResponse, + NodeMeasurementsPerKindResponse, NodePerformance, NodePerformancePagedResponse, NodePerformanceResponse, RetiredNetworkMonitor, RetiredNetworkMonitorsPagedResponse, }; @@ -60,7 +60,7 @@ pub trait PerformanceQueryClient { &self, epoch_id: EpochId, node_id: NodeId, - ) -> Result { + ) -> Result { self.query_performance_contract(PerformanceQueryMsg::NodeMeasurements { epoch_id, node_id }) .await } diff --git a/common/cosmwasm-smart-contracts/nym-performance-contract/src/constants.rs b/common/cosmwasm-smart-contracts/nym-performance-contract/src/constants.rs index 5f98a8e4398..bbff2a5b21c 100644 --- a/common/cosmwasm-smart-contracts/nym-performance-contract/src/constants.rs +++ b/common/cosmwasm-smart-contracts/nym-performance-contract/src/constants.rs @@ -9,6 +9,8 @@ pub mod storage_keys { pub const AUTHORISED_COUNT: &str = "authorised-count"; pub const AUTHORISED: &str = "authorised"; pub const RETIRED: &str = "retired"; - pub const PERFORMANCE_RESULTS: &str = "performance-results"; + pub const PERFORMANCE_RESULTS_PER_KIND: &str = "performance-results-per-kind"; + pub const PERFORMANCE_DEFINED_KINDS: &str = "performance-defined-kinds"; + pub const SUBMISSION_METADATA: &str = "submission-metadata"; } diff --git a/common/cosmwasm-smart-contracts/nym-performance-contract/src/error.rs b/common/cosmwasm-smart-contracts/nym-performance-contract/src/error.rs index adc86db0684..6d3c1060cec 100644 --- a/common/cosmwasm-smart-contracts/nym-performance-contract/src/error.rs +++ b/common/cosmwasm-smart-contracts/nym-performance-contract/src/error.rs @@ -23,6 +23,12 @@ pub enum NymPerformanceContractError { #[error("{address} is not an authorised network monitor")] NotAuthorised { address: Addr }, + #[error("{kind} not a valid measurement kind")] + UnsupportedMeasurementKind { kind: String }, + + #[error("Invalid input: {0}")] + InvalidInput(String), + #[error( "attempted to submit performance data for epoch {epoch_id} and node {node_id} whilst last submitted was {last_epoch_id} for node {last_node_id}" )] diff --git a/common/cosmwasm-smart-contracts/nym-performance-contract/src/msg.rs b/common/cosmwasm-smart-contracts/nym-performance-contract/src/msg.rs index 74f512d8915..ae85c1fe5b2 100644 --- a/common/cosmwasm-smart-contracts/nym-performance-contract/src/msg.rs +++ b/common/cosmwasm-smart-contracts/nym-performance-contract/src/msg.rs @@ -8,7 +8,7 @@ use cosmwasm_schema::cw_serde; use crate::types::{ EpochMeasurementsPagedResponse, EpochPerformancePagedResponse, FullHistoricalPerformancePagedResponse, LastSubmission, NetworkMonitorResponse, - NetworkMonitorsPagedResponse, NodeMeasurementsResponse, NodePerformancePagedResponse, + NetworkMonitorsPagedResponse, NodeMeasurementsPerKindResponse, NodePerformancePagedResponse, NodePerformanceResponse, RetiredNetworkMonitorsPagedResponse, }; @@ -35,6 +35,14 @@ pub enum ExecuteMsg { data: Vec, }, + /// Measurement kind needs to be defined by the admin before measurements of + /// that kind can be submitted. + DefineMeasurementKind { measurement_kind: String }, + + /// After this action is done, measurements of this kind are erased. + /// New measurements of this kind cannot be submitted + RetireMeasurementKind { measurement_kind: String }, + /// Attempt to authorise new network monitor for submitting performance data AuthoriseNetworkMonitor { address: String }, @@ -69,9 +77,17 @@ pub enum QueryMsg { limit: Option, }, - /// Returns all submitted measurements for the particular node + /// Returns all measurements of a specific kind for the particular node + #[cfg_attr(feature = "schema", returns(NodeMeasurementsResponse))] + NodeMeasurements { + epoch_id: EpochId, + node_id: NodeId, + kind: String, + }, + + // TODO dz add paged variant ? #[cfg_attr(feature = "schema", returns(NodeMeasurementsResponse))] - NodeMeasurements { epoch_id: EpochId, node_id: NodeId }, + AllNodeMeasurements { epoch_id: EpochId, node_id: NodeId }, /// Returns (paged) measurements for particular epoch #[cfg_attr(feature = "schema", returns(EpochMeasurementsPagedResponse))] diff --git a/common/cosmwasm-smart-contracts/nym-performance-contract/src/types.rs b/common/cosmwasm-smart-contracts/nym-performance-contract/src/types.rs index 383fb082802..8cf0cf42f30 100644 --- a/common/cosmwasm-smart-contracts/nym-performance-contract/src/types.rs +++ b/common/cosmwasm-smart-contracts/nym-performance-contract/src/types.rs @@ -1,6 +1,8 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use std::collections::HashMap; + use cosmwasm_schema::cw_serde; use cosmwasm_std::{Addr, Env, Timestamp}; use nym_contracts_common::Percent; @@ -49,11 +51,13 @@ pub struct RetiredNetworkMonitor { } #[cw_serde] -#[derive(Copy)] pub struct NodePerformance { #[serde(rename = "n")] pub node_id: NodeId, + #[serde(rename = "m")] + pub measurement_kind: MeasurementKind, + // note: value is rounded to 2 decimal places. #[serde(rename = "p")] pub performance: Percent, @@ -97,25 +101,35 @@ impl NodeResults { } pub fn inner(&self) -> &[Percent] { - &self.0 + &self.0.as_slice() } } +pub type MeasurementKind = String; + +/// maps measurement kind to the value of that measurement for a node +/// (present only if measured) #[cw_serde] pub struct NodePerformanceResponse { - pub performance: Option, + pub performance: HashMap, } #[cw_serde] -pub struct NodeMeasurementsResponse { +pub struct NodeMeasurementsPerKindResponse { pub measurements: Option, } #[cw_serde] -#[derive(Copy)] +pub struct AllNodeMeasurementsResponse { + // Option is used because if a measurement has been defined, that doesn't + // mean the node had actually been measured at the time of the query + pub measurements: HashMap>, +} + +#[cw_serde] pub struct EpochNodePerformance { pub epoch: EpochId, - pub performance: Option, + pub performance: HashMap, } #[cw_serde] @@ -133,24 +147,23 @@ pub struct EpochPerformancePagedResponse { } #[cw_serde] -pub struct NodeMeasurement { +pub struct NodeMeasurements { pub node_id: NodeId, - pub measurements: NodeResults, + pub measurements_per_kind: HashMap, } #[cw_serde] pub struct EpochMeasurementsPagedResponse { pub epoch_id: EpochId, - pub measurements: Vec, + pub measurements: Vec, pub start_next_after: Option, } #[cw_serde] -#[derive(Copy)] pub struct HistoricalPerformance { pub epoch_id: EpochId, pub node_id: NodeId, - pub performance: Percent, + pub performance: HashMap, } #[cw_serde] @@ -187,11 +200,14 @@ pub struct RemoveEpochMeasurementsResponse { pub additional_entries_to_remove_remaining: bool, } +/// return details about submissions: whether they were accepted, or why they +/// were rejected #[cw_serde] #[derive(Default)] pub struct BatchSubmissionResult { pub accepted_scores: u64, pub non_existent_nodes: Vec, + pub non_existent_measurement_kind: Vec, } #[cfg(test)] diff --git a/contracts/performance/src/contract.rs b/contracts/performance/src/contract.rs index 2f488f39618..e7fdccf774c 100644 --- a/contracts/performance/src/contract.rs +++ b/contracts/performance/src/contract.rs @@ -2,19 +2,20 @@ // SPDX-License-Identifier: Apache-2.0 use crate::queries::{ - query_admin, query_epoch_measurements_paged, query_epoch_performance_paged, - query_full_historical_performance_paged, query_last_submission, query_network_monitor_details, - query_network_monitors_paged, query_node_measurements, query_node_performance, - query_node_performance_paged, query_retired_network_monitors_paged, + query_admin, query_all_node_measurements, query_epoch_measurements_paged, + query_epoch_performance_paged, query_full_historical_performance_paged, query_last_submission, + query_network_monitor_details, query_network_monitors_paged, query_node_measurements_for_kind, + query_node_performance, query_node_performance_paged, query_retired_network_monitors_paged, }; use crate::storage::NYM_PERFORMANCE_CONTRACT_STORAGE; use crate::transactions::{ try_authorise_network_monitor, try_batch_submit_performance_results, - try_remove_epoch_measurements, try_remove_node_measurements, try_retire_network_monitor, - try_submit_performance_results, try_update_contract_admin, + try_define_measurement_kind, try_remove_epoch_measurements, try_remove_node_measurements, + try_retire_measurement_kind, try_retire_network_monitor, try_submit_performance_results, + try_update_contract_admin, }; use cosmwasm_std::{ - entry_point, to_json_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, + Binary, Deps, DepsMut, Env, MessageInfo, Response, entry_point, to_json_binary, }; use nym_contracts_common::set_build_information; use nym_performance_contract_common::{ @@ -62,12 +63,20 @@ pub fn execute( ExecuteMsg::BatchSubmit { epoch, data } => { try_batch_submit_performance_results(deps, env, info, epoch, data) } + ExecuteMsg::DefineMeasurementKind { measurement_kind } => { + try_define_measurement_kind(deps, &info.sender, measurement_kind) + } + ExecuteMsg::RetireMeasurementKind { measurement_kind } => { + try_retire_measurement_kind(deps, &info.sender, measurement_kind) + } ExecuteMsg::AuthoriseNetworkMonitor { address } => { try_authorise_network_monitor(deps, env, info, address) } ExecuteMsg::RetireNetworkMonitor { address } => { try_retire_network_monitor(deps, env, info, address) } + // TODO dz removing measurement for only a certain node shouldn't be allowed + // remove this message and corresponding path ExecuteMsg::RemoveNodeMeasurements { epoch_id, node_id } => { try_remove_node_measurements(deps, info, epoch_id, node_id) } @@ -116,9 +125,17 @@ pub fn query(deps: Deps, _: Env, msg: QueryMsg) -> Result Ok(to_json_binary( &query_retired_network_monitors_paged(deps, start_after, limit)?, )?), - QueryMsg::NodeMeasurements { epoch_id, node_id } => Ok(to_json_binary( - &query_node_measurements(deps, epoch_id, node_id)?, + QueryMsg::NodeMeasurements { + epoch_id, + node_id, + kind, + } => Ok(to_json_binary(&query_node_measurements_for_kind( + deps, epoch_id, node_id, kind, + )?)?), + QueryMsg::AllNodeMeasurements { epoch_id, node_id } => Ok(to_json_binary( + &query_all_node_measurements(deps, epoch_id, node_id)?, )?), + QueryMsg::EpochMeasurementsPaged { epoch_id, start_after, diff --git a/contracts/performance/src/helpers.rs b/contracts/performance/src/helpers.rs index 82199107877..9a16cdabf41 100644 --- a/contracts/performance/src/helpers.rs +++ b/contracts/performance/src/helpers.rs @@ -1,7 +1,7 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use cosmwasm_std::{from_json, Binary, CustomQuery, QuerierWrapper, StdError, StdResult}; +use cosmwasm_std::{Binary, CustomQuery, QuerierWrapper, StdError, StdResult, from_json}; use cw_storage_plus::{Key, Namespace, Path, PrimaryKey}; use nym_mixnet_contract_common::{Interval, NymNodeBond}; use nym_performance_contract_common::{EpochId, NodeId}; diff --git a/contracts/performance/src/queries.rs b/contracts/performance/src/queries.rs index 5fe2c3ed91d..9afd4ed0eb6 100644 --- a/contracts/performance/src/queries.rs +++ b/contracts/performance/src/queries.rs @@ -1,16 +1,20 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::storage::{retrieval_limits, NYM_PERFORMANCE_CONTRACT_STORAGE}; +use std::collections::HashMap; + +use crate::storage::{MeasurementKind, NYM_PERFORMANCE_CONTRACT_STORAGE, retrieval_limits}; use cosmwasm_std::{Addr, Deps, Order, StdResult}; use cw_controllers::AdminResponse; use cw_storage_plus::Bound; +use nym_contracts_common::Percent; use nym_performance_contract_common::{ - EpochId, EpochMeasurementsPagedResponse, EpochNodePerformance, EpochPerformancePagedResponse, - FullHistoricalPerformancePagedResponse, HistoricalPerformance, LastSubmission, - NetworkMonitorInformation, NetworkMonitorResponse, NetworkMonitorsPagedResponse, NodeId, - NodeMeasurement, NodeMeasurementsResponse, NodePerformance, NodePerformancePagedResponse, - NodePerformanceResponse, NymPerformanceContractError, RetiredNetworkMonitorsPagedResponse, + AllNodeMeasurementsResponse, EpochId, EpochMeasurementsPagedResponse, EpochNodePerformance, + EpochPerformancePagedResponse, FullHistoricalPerformancePagedResponse, HistoricalPerformance, + LastSubmission, NetworkMonitorInformation, NetworkMonitorResponse, + NetworkMonitorsPagedResponse, NodeId, NodeMeasurements, NodeMeasurementsPerKindResponse, + NodePerformance, NodePerformancePagedResponse, NodePerformanceResponse, NodeResults, + NymPerformanceContractError, RetiredNetworkMonitorsPagedResponse, }; pub fn query_admin(deps: Deps) -> Result { @@ -30,16 +34,45 @@ pub fn query_node_performance( Ok(NodePerformanceResponse { performance }) } -pub fn query_node_measurements( +pub fn query_node_measurements_for_kind( deps: Deps, epoch_id: EpochId, node_id: NodeId, -) -> Result { - let measurements = NYM_PERFORMANCE_CONTRACT_STORAGE + measurement_kind: String, +) -> Result { + let measurements = NYM_PERFORMANCE_CONTRACT_STORAGE.try_load_measurement_kind( + deps.storage, + epoch_id, + node_id, + measurement_kind, + )?; + + Ok(NodeMeasurementsPerKindResponse { measurements }) +} + +pub fn query_all_node_measurements( + deps: Deps, + epoch_id: EpochId, + node_id: NodeId, +) -> Result { + let measurements = NYM_PERFORMANCE_CONTRACT_STORAGE.performance_results.results; + + // retrieve a list of currently defined measurements, only return results for those + // (storage may contain measurements that have since been deleted by admin - + // this way, they won't be retrieved) + let possible_measurements = NYM_PERFORMANCE_CONTRACT_STORAGE .performance_results - .results - .may_load(deps.storage, (epoch_id, node_id))?; - Ok(NodeMeasurementsResponse { measurements }) + .defined_measurements(deps.storage)?; + let mut node_measurements = HashMap::new(); + for measure_name in possible_measurements { + let key = (epoch_id, node_id, measure_name.clone()); + let node_measurement = measurements.may_load(deps.storage, key)?; + node_measurements.insert(measure_name, node_measurement); + } + + Ok(AllNodeMeasurementsResponse { + measurements: node_measurements, + }) } pub fn query_node_performance_paged( @@ -107,18 +140,19 @@ pub fn query_epoch_performance_paged( .unwrap_or(retrieval_limits::NODE_EPOCH_PERFORMANCE_DEFAULT_LIMIT) .min(retrieval_limits::NODE_EPOCH_PERFORMANCE_MAX_LIMIT) as usize; - let start = start_after.map(Bound::exclusive); + let start = start_after.map(|node_id| Bound::exclusive((node_id + 1, String::new()))); let performance = NYM_PERFORMANCE_CONTRACT_STORAGE .performance_results .results - .prefix(epoch_id) + .sub_prefix(epoch_id) .range(deps.storage, start, None, Order::Ascending) .take(limit) .map(|record| { - record.map(|(node_id, results)| NodePerformance { + record.map(|((node_id, measurement_kind), results)| NodePerformance { node_id, performance: results.median(), + measurement_kind, }) }) .collect::>>()?; @@ -142,27 +176,51 @@ pub fn query_epoch_measurements_paged( .unwrap_or(retrieval_limits::NODE_EPOCH_MEASUREMENTS_DEFAULT_LIMIT) .min(retrieval_limits::NODE_EPOCH_MEASUREMENTS_MAX_LIMIT) as usize; - let start = start_after.map(Bound::exclusive); + let start = start_after.map(|node_id| Bound::exclusive((node_id + 1, String::new()))); + // because API aggregates per NodeId, and the storage doesn't, we have to + // first collect all different measurements for a node and use an + // intermediary struct to map from storage to the object returned on the API + let mut measurements_per_node: HashMap> = + HashMap::new(); let measurements = NYM_PERFORMANCE_CONTRACT_STORAGE .performance_results .results - .prefix(epoch_id) + .sub_prefix(epoch_id) .range(deps.storage, start, None, Order::Ascending) .take(limit) .map(|record| { - record.map(|(node_id, measurements)| NodeMeasurement { - node_id, - measurements, + record.inspect(|((node_id, kind), measurements)| { + measurements_per_node + .entry(*node_id) + .and_modify(|vec| vec.push((kind.to_string(), measurements.to_owned()))) + .or_insert_with(|| vec![(kind.to_string(), measurements.to_owned())]); }) }) .collect::>>()?; - let start_next_after = measurements.last().map(|last| last.node_id); + // transforming collected data into a returning type + let mut returning = Vec::new(); + for (node_id, measurements_per_kind) in measurements_per_node.into_iter() { + let mut measurements = HashMap::new(); + for (measurement_kind, results) in measurements_per_kind { + measurements.insert(measurement_kind, results); + } + returning.push(NodeMeasurements { + node_id, + measurements_per_kind: measurements, + }); + } + + // storage keeps nodes in ascending order for pagination + // intermediary hashmap doesn't have deterministic order so we need to order + // explicitly here before returning + returning.sort_by_key(|elem| elem.node_id); + let start_next_after = measurements.last().map(|((last, _), _)| *last); Ok(EpochMeasurementsPagedResponse { epoch_id, - measurements, + measurements: returning, start_next_after, }) } @@ -176,26 +234,63 @@ pub fn query_full_historical_performance_paged( .unwrap_or(retrieval_limits::NODE_HISTORICAL_PERFORMANCE_DEFAULT_LIMIT) .min(retrieval_limits::NODE_HISTORICAL_PERFORMANCE_MAX_LIMIT) as usize; - let start = start_after.map(Bound::exclusive); + // because results are sorted first by epoch_id, then by node_id, start at the next node_id + // (from the lexicographically first measurement_kind, which is empty string) + let start = start_after.map(|(epoch, node)| Bound::exclusive((epoch, node + 1, String::new()))); - let performance = NYM_PERFORMANCE_CONTRACT_STORAGE + // storage keeps (epoch_id, node_id, measurement_kind) tuples, + // but the API needs results aggregated by (epoch_id, node_id) pairs + // so we need an intermediary struct that collects all measurements + // per (epoch_id, node_id) pair (and calculates a performance) + let mut res_per_epoch_and_node: HashMap<(EpochId, NodeId), HashMap> = + HashMap::new(); + NYM_PERFORMANCE_CONTRACT_STORAGE .performance_results .results .range(deps.storage, start, None, Order::Ascending) - .take(limit) + // we can't cut a pagination limit here becasue we don't want to + // cut across different measurement kinds of the same node_id .map(|record| { - record.map(|((epoch_id, node_id), results)| HistoricalPerformance { - epoch_id, - node_id, - performance: results.median(), + record.map(|((epoch_id, node_id, measurement_kind), results)| { + // inside map we access elements to populate the intermediary struct + let key = (epoch_id, node_id); + res_per_epoch_and_node + .entry(key) + .and_modify(|measurements| { + measurements.insert(measurement_kind.clone(), results.median()); + }) + .or_insert_with(|| { + let mut new = HashMap::new(); + // instead of taking all measurements, calculate performance (median) + new.insert(measurement_kind, results.median()); + new + }); + + // what we return here is irrelevant, it isn't used + key }) }) .collect::>>()?; - let start_next_after = performance.last().map(|last| (last.epoch_id, last.node_id)); + // map intermediary struct to the format expected on the API + let mut res = Vec::new(); + for ((epoch_id, node_id), performance) in res_per_epoch_and_node.into_iter() { + res.push(HistoricalPerformance { + epoch_id, + node_id, + performance, + }); + } + // Storage keeps elements sorted by epoch_id, then node_id. Hashmap shuffles this. + // Sort by those two before returning + res.sort_by_key(|perf| (perf.epoch_id, perf.node_id)); + let res: Vec<_> = res.into_iter().take(limit).collect(); + + // cut for pagination after sorting + let start_next_after = res.last().map(|perf| (perf.epoch_id, perf.node_id)); Ok(FullHistoricalPerformancePagedResponse { - performance, + performance: res, start_next_after, }) } @@ -315,9 +410,11 @@ pub fn query_last_submission(deps: Deps) -> Result anyhow::Result<()> { + let mut test = init_contract_tester(); + + // test setup + let nm1 = test.generate_account(); + let nm2 = test.generate_account(); + test.authorise_network_monitor(&nm1)?; + test.authorise_network_monitor(&nm2)?; + + let admin = test.admin_unchecked(); + let kind_mixnet = String::from("mixnet"); + let kind_wireguard = String::from("wireguard"); + test.execute_raw( + admin.clone(), + ExecuteMsg::DefineMeasurementKind { + measurement_kind: kind_mixnet.clone(), + }, + )?; + test.execute_raw( + admin, + ExecuteMsg::DefineMeasurementKind { + measurement_kind: kind_wireguard.clone(), + }, + )?; + + let node1 = test.bond_dummy_nymnode()?; + let node2 = test.bond_dummy_nymnode()?; + + let epoch_1 = 1; + test.set_mixnet_epoch(epoch_1)?; + + let deps = test.deps(); + + // ===== Test: undefined measurement kind ===== + let undefined_kind = String::from("undefined"); + let res = query_node_measurements_for_kind(deps, epoch_1, node1, undefined_kind); + assert!(res.is_err()); + assert!(matches!( + res.unwrap_err(), + NymPerformanceContractError::UnsupportedMeasurementKind { .. } + )); + + // ===== Test: query returns None for defined kind with no data ===== + let res = query_node_measurements_for_kind(deps, epoch_1, node1, kind_mixnet.clone())?; + assert!(res.measurements.is_none()); + + // ===== Test happy path: single measurement from one monitor ===== + test.insert_raw_performance(&nm1, node1, kind_mixnet.clone(), "0.5")?; + let res = + query_node_measurements_for_kind(test.deps(), epoch_1, node1, kind_mixnet.clone())?; + let measurements = res.measurements.unwrap(); + assert_eq!(measurements.inner().len(), 1); + assert_eq!(measurements.inner()[0], "0.5".parse()?); + + // Verify against raw storage + let expected = test.read_raw_scores(epoch_1, node1, kind_mixnet.clone())?; + assert_eq!(measurements.inner(), expected.inner()); + + // ===== Test: multiple measurements from different monitors ===== + // each monitor can only submit once per (epoch, node) pair + test.insert_raw_performance(&nm2, node1, kind_mixnet.clone(), "0.3")?; + let res = + query_node_measurements_for_kind(test.deps(), epoch_1, node1, kind_mixnet.clone())?; + let measurements = res.measurements.unwrap(); + assert_eq!(measurements.inner().len(), 2); + + // ===== Test: multiple measurement kinds are independent ===== + // we need a new epoch since monitors already submitted in this epoch + let epoch_2 = 2; + test.set_mixnet_epoch(epoch_2)?; + + // now submit + test.insert_raw_performance(&nm1, node1, kind_wireguard.clone(), "0.8")?; + test.insert_raw_performance(&nm2, node1, kind_wireguard.clone(), "0.9")?; + + // verify data for submitted kind is there + let res_wireguard_e11 = + query_node_measurements_for_kind(test.deps(), epoch_2, node1, kind_wireguard.clone())?; + let wg_measurements = res_wireguard_e11.measurements.unwrap(); + assert_eq!(wg_measurements.inner().len(), 2); + assert_eq!(wg_measurements.inner()[0], "0.8".parse()?); + assert_eq!(wg_measurements.inner()[1], "0.9".parse()?); + + // not submitted for this kind in this epoch: should have no data + let res_mixnet_e11 = + query_node_measurements_for_kind(test.deps(), epoch_2, node1, kind_mixnet.clone())?; + assert!(res_mixnet_e11.measurements.is_none()); + + // however, mixnet kind should still have old data in previous epoch + let res_mixnet_e10 = + query_node_measurements_for_kind(test.deps(), epoch_1, node1, kind_mixnet.clone())?; + let mixnet_measurements = res_mixnet_e10.measurements.unwrap(); + assert_eq!(mixnet_measurements.inner().len(), 2); + assert_eq!(mixnet_measurements.inner()[0], "0.3".parse()?); + assert_eq!(mixnet_measurements.inner()[1], "0.5".parse()?); + + // ===== Test: different epochs are independent ===== + // advance epoch again & submit something + let epoch_3 = 3; + test.set_mixnet_epoch(epoch_3)?; + test.insert_raw_performance(&nm1, node1, kind_mixnet.clone(), "0.25")?; + + // epoch 3 should have new data + let res_epoch12 = + query_node_measurements_for_kind(test.deps(), epoch_3, node1, kind_mixnet.clone())?; + assert!(res_epoch12.measurements.is_some()); + let epoch12_measurements = res_epoch12.measurements.unwrap(); + assert_eq!(epoch12_measurements.inner().len(), 1); + assert_eq!(epoch12_measurements.inner()[0], "0.25".parse()?); + + // epoch 1 should still have old data + let res_epoch10 = + query_node_measurements_for_kind(test.deps(), epoch_1, node1, kind_mixnet.clone())?; + assert_eq!(res_epoch10.measurements.unwrap().inner().len(), 2); + + // epoch 2 with mixnet (no data) should return None + let res_epoch11_mixnet = + query_node_measurements_for_kind(test.deps(), epoch_2, node1, kind_mixnet.clone())?; + assert!(res_epoch11_mixnet.measurements.is_none()); + + // ===== Test: different nodes are independent ===== + // nm1 can now submit for node2 in epoch 3 since node2 > node1 + test.insert_raw_performance(&nm1, node2, kind_mixnet.clone(), "0.42")?; + + // Query node1 in epoch 3 - should have nm1's data + let res_node1_e12 = + query_node_measurements_for_kind(test.deps(), epoch_3, node1, kind_mixnet.clone())?; + assert_eq!(res_node1_e12.measurements.unwrap().inner().len(), 1); + + // Query node2 in epoch 3 - should have different data + let res_node2_e12 = + query_node_measurements_for_kind(test.deps(), epoch_3, node2, kind_mixnet.clone())?; + let node2_measurements = res_node2_e12.measurements.unwrap(); + assert_eq!(node2_measurements.inner().len(), 1); + assert_eq!(node2_measurements.inner()[0], "0.42".parse()?); + + // Query node2 in epoch 1 (no data) - should return None + let res_node2_e10 = + query_node_measurements_for_kind(test.deps(), epoch_1, node2, kind_mixnet.clone())?; + assert!(res_node2_e10.measurements.is_none()); + + // verify against raw data + let raw_scores = test.read_raw_scores(epoch_1, node1, kind_mixnet.clone())?; + let query_result = + query_node_measurements_for_kind(test.deps(), epoch_1, node1, kind_mixnet.clone())?; + assert_eq!( + query_result.measurements.unwrap().inner(), + raw_scores.inner() + ); + + Ok(()) + } + #[test] fn querying_node_performance_paged() -> anyhow::Result<()> { let mut test = init_contract_tester(); @@ -363,29 +614,30 @@ mod tests { let node_id = test.bond_dummy_nymnode()?; let nm = test.generate_account(); test.authorise_network_monitor(&nm)?; + let measurement_kind = test.define_dummy_measurement_kind().unwrap(); // epoch 0 - test.insert_raw_performance(&nm, node_id, "0")?; + test.insert_raw_performance(&nm, node_id, measurement_kind.clone(), "0")?; // epoch 1 test.advance_mixnet_epoch()?; - test.insert_raw_performance(&nm, node_id, "0.1")?; + test.insert_raw_performance(&nm, node_id, measurement_kind.clone(), "0.1")?; // epoch 2 test.advance_mixnet_epoch()?; - test.insert_raw_performance(&nm, node_id, "0.2")?; + test.insert_raw_performance(&nm, node_id, measurement_kind.clone(), "0.2")?; // epoch 3 test.advance_mixnet_epoch()?; - test.insert_raw_performance(&nm, node_id, "0.3")?; + test.insert_raw_performance(&nm, node_id, measurement_kind.clone(), "0.3")?; // epoch 4 test.advance_mixnet_epoch()?; - test.insert_raw_performance(&nm, node_id, "0.4")?; + test.insert_raw_performance(&nm, node_id, measurement_kind.clone(), "0.4")?; // epoch 5 test.advance_mixnet_epoch()?; - test.insert_raw_performance(&nm, node_id, "0.5")?; + test.insert_raw_performance(&nm, node_id, measurement_kind.clone(), "0.5")?; let deps = test.deps(); let res = query_node_performance_paged(deps, node_id, Some(5), None)?; @@ -400,10 +652,11 @@ mod tests { assert!(res.start_next_after.is_none()); assert_eq!( res.performance, - vec![EpochNodePerformance { - epoch: 5, - performance: Some("0.5".parse()?), - }] + vec![epoch_node_performance_unchecked( + 5, + measurement_kind.clone(), + "0.5" + )] ); let res = query_node_performance_paged(deps, node_id, Some(2), None)?; @@ -411,18 +664,9 @@ mod tests { assert_eq!( res.performance, vec![ - EpochNodePerformance { - epoch: 3, - performance: Some("0.3".parse()?), - }, - EpochNodePerformance { - epoch: 4, - performance: Some("0.4".parse()?), - }, - EpochNodePerformance { - epoch: 5, - performance: Some("0.5".parse()?), - } + epoch_node_performance_unchecked(3, measurement_kind.clone(), "0.3"), + epoch_node_performance_unchecked(4, measurement_kind.clone(), "0.4"), + epoch_node_performance_unchecked(5, measurement_kind.clone(), "0.5"), ] ); @@ -431,30 +675,12 @@ mod tests { assert_eq!( res.performance, vec![ - EpochNodePerformance { - epoch: 0, - performance: Some("0".parse()?), - }, - EpochNodePerformance { - epoch: 1, - performance: Some("0.1".parse()?), - }, - EpochNodePerformance { - epoch: 2, - performance: Some("0.2".parse()?), - }, - EpochNodePerformance { - epoch: 3, - performance: Some("0.3".parse()?), - }, - EpochNodePerformance { - epoch: 4, - performance: Some("0.4".parse()?), - }, - EpochNodePerformance { - epoch: 5, - performance: Some("0.5".parse()?), - } + epoch_node_performance_unchecked(0, measurement_kind.clone(), "0"), + epoch_node_performance_unchecked(1, measurement_kind.clone(), "0.1"), + epoch_node_performance_unchecked(2, measurement_kind.clone(), "0.2"), + epoch_node_performance_unchecked(3, measurement_kind.clone(), "0.3"), + epoch_node_performance_unchecked(4, measurement_kind.clone(), "0.4"), + epoch_node_performance_unchecked(5, measurement_kind.clone(), "0.5"), ] ); @@ -462,10 +688,11 @@ mod tests { assert_eq!(res.start_next_after, Some(3)); assert_eq!( res.performance, - vec![EpochNodePerformance { - epoch: 3, - performance: Some("0.3".parse()?), - }] + vec![epoch_node_performance_unchecked( + 3, + measurement_kind.clone(), + "0.3" + )] ); Ok(()) @@ -477,6 +704,7 @@ mod tests { let nm = test.generate_account(); test.authorise_network_monitor(&nm)?; + let measurement_kind = test.define_dummy_measurement_kind().unwrap(); let mut nodes = Vec::new(); for _ in 0..10 { @@ -486,12 +714,12 @@ mod tests { let epoch_id = 5; test.set_mixnet_epoch(epoch_id)?; - test.insert_raw_performance(&nm, nodes[1], "0.1")?; - test.insert_raw_performance(&nm, nodes[2], "0.2")?; - test.insert_raw_performance(&nm, nodes[3], "0.3")?; + test.insert_raw_performance(&nm, nodes[1], measurement_kind.clone(), "0.1")?; + test.insert_raw_performance(&nm, nodes[2], measurement_kind.clone(), "0.2")?; + test.insert_raw_performance(&nm, nodes[3], measurement_kind.clone(), "0.3")?; // 4 is missing - test.insert_raw_performance(&nm, nodes[5], "0.5")?; - test.insert_raw_performance(&nm, nodes[6], "0.6")?; + test.insert_raw_performance(&nm, nodes[5], measurement_kind.clone(), "0.5")?; + test.insert_raw_performance(&nm, nodes[6], measurement_kind.clone(), "0.6")?; let deps = test.deps(); let res = query_epoch_performance_paged(deps, epoch_id, Some(nodes[6]), None)?; @@ -510,10 +738,12 @@ mod tests { NodePerformance { node_id: nodes[5], performance: "0.5".parse()?, + measurement_kind: measurement_kind.clone() }, NodePerformance { node_id: nodes[6], performance: "0.6".parse()?, + measurement_kind: measurement_kind.clone() } ] ); @@ -525,10 +755,12 @@ mod tests { NodePerformance { node_id: nodes[5], performance: "0.5".parse()?, + measurement_kind: measurement_kind.clone() }, NodePerformance { node_id: nodes[6], performance: "0.6".parse()?, + measurement_kind: measurement_kind.clone() } ] ); @@ -541,14 +773,17 @@ mod tests { NodePerformance { node_id: nodes[3], performance: "0.3".parse()?, + measurement_kind: measurement_kind.clone() }, NodePerformance { node_id: nodes[5], performance: "0.5".parse()?, + measurement_kind: measurement_kind.clone() }, NodePerformance { node_id: nodes[6], performance: "0.6".parse()?, + measurement_kind: measurement_kind.clone() } ] ); @@ -561,22 +796,27 @@ mod tests { NodePerformance { node_id: nodes[1], performance: "0.1".parse()?, + measurement_kind: measurement_kind.clone() }, NodePerformance { node_id: nodes[2], performance: "0.2".parse()?, + measurement_kind: measurement_kind.clone() }, NodePerformance { node_id: nodes[3], performance: "0.3".parse()?, + measurement_kind: measurement_kind.clone() }, NodePerformance { node_id: nodes[5], performance: "0.5".parse()?, + measurement_kind: measurement_kind.clone() }, NodePerformance { node_id: nodes[6], performance: "0.6".parse()?, + measurement_kind: measurement_kind.clone() } ] ); @@ -588,12 +828,277 @@ mod tests { vec![NodePerformance { node_id: nodes[3], performance: "0.3".parse()?, + measurement_kind: measurement_kind.clone() }] ); Ok(()) } + #[test] + fn querying_epoch_measurements_paged() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + + let nm = test.generate_account(); + test.authorise_network_monitor(&nm)?; + let measurement_kind = test.define_dummy_measurement_kind().unwrap(); + + let mut nodes = Vec::new(); + for _ in 0..10 { + nodes.push(test.bond_dummy_nymnode()?); + } + + let epoch_id = 5; + test.set_mixnet_epoch(epoch_id)?; + + test.insert_raw_performance(&nm, nodes[1], measurement_kind.clone(), "0.1")?; + test.insert_raw_performance(&nm, nodes[2], measurement_kind.clone(), "0.2")?; + test.insert_raw_performance(&nm, nodes[3], measurement_kind.clone(), "0.3")?; + // 4 is missing + test.insert_raw_performance(&nm, nodes[5], measurement_kind.clone(), "0.5")?; + test.insert_raw_performance(&nm, nodes[6], measurement_kind.clone(), "0.6")?; + + let deps = test.deps(); + + // query starting after nodes[6] + let res = query_epoch_measurements_paged(deps, epoch_id, Some(nodes[6]), None)?; + assert!(res.start_next_after.is_none()); + assert!(res.measurements.is_empty()); + + // query after non-existent high node ID + let res = query_epoch_measurements_paged(deps, epoch_id, Some(42), None)?; + assert!(res.start_next_after.is_none()); + assert!(res.measurements.is_empty()); + + // query starting after nodes[4] (should return nodes 5 and 6) + let res = query_epoch_measurements_paged(deps, epoch_id, Some(nodes[4]), None)?; + assert_eq!(res.start_next_after, Some(nodes[6])); + assert_eq!(res.measurements.len(), 2); + + assert_eq!(res.measurements[0].node_id, nodes[5]); + assert_eq!(res.measurements[1].node_id, nodes[6]); + + // verify returned data against raw results + let node5_results = res.measurements[0] + .measurements_per_kind + .get(&measurement_kind) + .unwrap(); + let expected_results = + test.read_raw_scores(epoch_id, nodes[5], measurement_kind.clone())?; + assert_eq!(node5_results.inner(), expected_results.inner()); + + let node6_results = res.measurements[1] + .measurements_per_kind + .get(&measurement_kind) + .unwrap(); + let expected_results = + test.read_raw_scores(epoch_id, nodes[6], measurement_kind.clone())?; + assert_eq!(node6_results.inner(), expected_results.inner()); + + // query starting after nodes[3] + // should skip nodes[3] entirely and start from nodes[5] (nodes[4] doesn't exist) + let res = query_epoch_measurements_paged(deps, epoch_id, Some(nodes[3]), None)?; + assert_eq!(res.start_next_after, Some(nodes[6])); + // Verify only nodes[5] and nodes[6] are present (nodes[3] is skipped) + assert_eq!(res.measurements.len(), 2); + assert_eq!(res.measurements[0].node_id, nodes[5]); + assert_eq!(res.measurements[1].node_id, nodes[6]); + + // query with start_after = nodes[2] + let res = query_epoch_measurements_paged(deps, epoch_id, Some(nodes[2]), None)?; + assert_eq!(res.start_next_after, Some(nodes[6])); + assert_eq!(res.measurements.len(), 3); + // only nodes[3], nodes[5], nodes[6] are present (nodes[2] is skipped) + assert_eq!(res.measurements[0].node_id, nodes[3]); + assert_eq!(res.measurements[1].node_id, nodes[5]); + assert_eq!(res.measurements[2].node_id, nodes[6]); + + // measurements HashMap structure for all nodes + for measurement in &res.measurements { + assert!( + measurement + .measurements_per_kind + .contains_key(&measurement_kind) + ); + } + + // query from beginning (no start_after) - should return all nodes + let res = query_epoch_measurements_paged(deps, epoch_id, None, None)?; + assert_eq!(res.start_next_after, Some(nodes[6])); + assert_eq!(res.measurements.len(), 5); + // verify all expected nodes are present IN SORTED ORDER + assert_eq!(res.measurements[0].node_id, nodes[1]); + assert_eq!(res.measurements[1].node_id, nodes[2]); + assert_eq!(res.measurements[2].node_id, nodes[3]); + assert_eq!(res.measurements[3].node_id, nodes[5]); + assert_eq!(res.measurements[4].node_id, nodes[6]); + + // query with custom limit + // With limit=1, we fetch 1 storage item starting from nodes[3] (nodes[2] + 1) + let res = query_epoch_measurements_paged(deps, epoch_id, Some(nodes[2]), Some(1))?; + assert_eq!(res.start_next_after, Some(nodes[3])); + assert_eq!(res.measurements.len(), 1); + assert_eq!(res.measurements[0].node_id, nodes[3]); + + Ok(()) + } + + #[test] + fn querying_epoch_measurements_paged_multiple_kinds() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + + // Use two different network monitors for different measurement kinds + let nm1 = test.generate_account(); + let nm2 = test.generate_account(); + test.authorise_network_monitor(&nm1)?; + test.authorise_network_monitor(&nm2)?; + + // define two different measurement kinds + let admin = test.admin_unchecked(); + let kind1 = String::from("mixnet"); + let kind2 = String::from("wireguard"); + + test.execute_raw( + admin.clone(), + ExecuteMsg::DefineMeasurementKind { + measurement_kind: kind1.clone(), + }, + )?; + test.execute_raw( + admin, + ExecuteMsg::DefineMeasurementKind { + measurement_kind: kind2.clone(), + }, + )?; + + // bond some nodes + let node1 = test.bond_dummy_nymnode()?; + let node2 = test.bond_dummy_nymnode()?; + let node3 = test.bond_dummy_nymnode()?; + + let epoch_id = 10; + test.set_mixnet_epoch(epoch_id)?; + + // both measurement kinds (different network monitors) + test.insert_raw_performance(&nm1, node1, kind1.clone(), "0.11")?; + test.insert_raw_performance(&nm2, node1, kind2.clone(), "0.12")?; + + // only first kind + test.insert_raw_performance(&nm1, node2, kind1.clone(), "0.21")?; + + // both kinds (different network monitors) + test.insert_raw_performance(&nm1, node3, kind1.clone(), "0.31")?; + test.insert_raw_performance(&nm2, node3, kind2.clone(), "0.32")?; + + let deps = test.deps(); + + // query all measurements for this epoch + let res = query_epoch_measurements_paged(deps, epoch_id, None, None)?; + assert_eq!(res.epoch_id, epoch_id); + assert_eq!(res.measurements.len(), 3); + + assert_eq!(res.measurements[0].node_id, node1); + assert_eq!(res.measurements[1].node_id, node2); + assert_eq!(res.measurements[2].node_id, node3); + + let node1_measurements = &res.measurements[0]; + let node2_measurements = &res.measurements[1]; + let node3_measurements = &res.measurements[2]; + + // verify node 1 has 2 measurement kinds + assert_eq!(node1_measurements.measurements_per_kind.len(), 2); + assert!( + node1_measurements + .measurements_per_kind + .contains_key(&kind1) + ); + assert!( + node1_measurements + .measurements_per_kind + .contains_key(&kind2) + ); + + // raw data for node 1 + let node1_kind1_results = node1_measurements + .measurements_per_kind + .get(&kind1) + .unwrap(); + let expected = test.read_raw_scores(epoch_id, node1, kind1.clone())?; + assert_eq!(node1_kind1_results.inner(), expected.inner()); + + let node1_kind2_results = node1_measurements + .measurements_per_kind + .get(&kind2) + .unwrap(); + let expected = test.read_raw_scores(epoch_id, node1, kind2.clone())?; + assert_eq!(node1_kind2_results.inner(), expected.inner()); + + // node 2 has only 1 measurement kind + assert_eq!(node2_measurements.measurements_per_kind.len(), 1); + assert!( + node2_measurements + .measurements_per_kind + .contains_key(&kind1) + ); + assert!( + !node2_measurements + .measurements_per_kind + .contains_key(&kind2) + ); + + // raw data for node 2 + let node2_kind1_results = node2_measurements + .measurements_per_kind + .get(&kind1) + .unwrap(); + let expected = test.read_raw_scores(epoch_id, node2, kind1.clone())?; + assert_eq!(node2_kind1_results.inner(), expected.inner()); + + // node 3 has 2 measurement kinds + assert_eq!(node3_measurements.measurements_per_kind.len(), 2); + assert!( + node3_measurements + .measurements_per_kind + .contains_key(&kind1) + ); + assert!( + node3_measurements + .measurements_per_kind + .contains_key(&kind2) + ); + + // raw data for node 3 + let node3_kind1_results = node3_measurements + .measurements_per_kind + .get(&kind1) + .unwrap(); + let expected = test.read_raw_scores(epoch_id, node3, kind1.clone())?; + assert_eq!(node3_kind1_results.inner(), expected.inner()); + + let node3_kind2_results = node3_measurements + .measurements_per_kind + .get(&kind2) + .unwrap(); + let expected = test.read_raw_scores(epoch_id, node3, kind2.clone())?; + assert_eq!(node3_kind2_results.inner(), expected.inner()); + + // pagination with multiple kinds - query after node1 + let res = query_epoch_measurements_paged(deps, epoch_id, Some(node1), None)?; + assert_eq!(res.measurements.len(), 2); // node2 and node3 only (node1 is skipped) + assert_eq!(res.measurements[0].node_id, node2); + assert_eq!(res.measurements[1].node_id, node3); + assert_eq!(res.measurements[0].measurements_per_kind.len(), 1); // only latency + assert_eq!(res.measurements[1].measurements_per_kind.len(), 2); // both kinds + + // pagination after node2 - should only return node3 + let res = query_epoch_measurements_paged(deps, epoch_id, Some(node2), None)?; + assert_eq!(res.measurements.len(), 1); // only node3 + assert_eq!(res.measurements[0].node_id, node3); + assert_eq!(res.measurements[0].measurements_per_kind.len(), 2); // both kinds + + Ok(()) + } + #[test] fn last_submission_query() -> anyhow::Result<()> { let mut test = init_contract_tester(); @@ -619,8 +1124,9 @@ mod tests { test.authorise_network_monitor(&nm1)?; test.authorise_network_monitor(&nm2)?; test.set_mixnet_epoch(10)?; + let measurement_kind = test.define_dummy_measurement_kind().unwrap(); - test.insert_raw_performance(&nm1, id1, "0.2")?; + test.insert_raw_performance(&nm1, id1, measurement_kind.clone(), "0.2")?; let data = query_last_submission(test.deps())?; assert_eq!( @@ -633,7 +1139,8 @@ mod tests { epoch_id: 10, data: NodePerformance { node_id: id1, - performance: "0.2".parse()? + performance: "0.2".parse()?, + measurement_kind: measurement_kind.clone(), }, }), } @@ -642,7 +1149,7 @@ mod tests { test.next_block(); let env = test.env(); - test.insert_epoch_performance(&nm2, 5, id2, "0.3".parse()?)?; + test.insert_epoch_performance(&nm2, 5, id2, measurement_kind.clone(), "0.3".parse()?)?; // note that even though it's "earlier" data, last submission is still updated accordingly let data = query_last_submission(test.deps())?; @@ -656,12 +1163,466 @@ mod tests { epoch_id: 5, data: NodePerformance { node_id: id2, - performance: "0.3".parse()? + performance: "0.3".parse()?, + measurement_kind: measurement_kind.clone(), + }, + }), + } + ); + + Ok(()) + } + + #[test] + #[ignore] + // TODO uncomment test: + // currently logic for stale submission doesn't work well with different measurement kinds + fn last_submission_query_multiple_kinds() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + let env = test.env(); + + // Bond one node and authorize one monitor + let id1 = test.bond_dummy_nymnode()?; + let nm1 = test.generate_account(); + test.authorise_network_monitor(&nm1)?; + test.set_mixnet_epoch(10)?; + + // Define TWO measurement kinds + let measurement_mixnet = MeasurementKind::from("mixnet"); + let measurement_dvpn = MeasurementKind::from("dvpn"); + let admin = test.admin_unchecked(); + test.execute_raw( + admin.clone(), + ExecuteMsg::DefineMeasurementKind { + measurement_kind: measurement_dvpn.clone(), + }, + )?; + test.execute_raw( + admin, + ExecuteMsg::DefineMeasurementKind { + measurement_kind: measurement_mixnet.clone(), + }, + )?; + + // no submissions yet + let data = query_last_submission(test.deps())?; + assert_eq!( + data, + LastSubmission { + block_height: env.block.height, + block_time: env.block.time, + data: None, + } + ); + + // Submit first measurement kind in epoch 10 + test.insert_raw_performance(&nm1, id1, measurement_dvpn.clone(), "0.75")?; + + let data = query_last_submission(test.deps())?; + assert_eq!( + data, + LastSubmission { + block_height: env.block.height, + block_time: env.block.time, + data: Some(LastSubmittedData { + sender: nm1.clone(), + epoch_id: 10, + data: NodePerformance { + node_id: id1, + performance: "0.75".parse()?, + measurement_kind: measurement_dvpn.clone(), }, }), } ); + let env = test.env(); + + // submit second measurement kind: same monitor, same node, same epoch + test.insert_raw_performance(&nm1, id1, measurement_mixnet.clone(), "0.85")?; + + // verify that last submission is updated with the new measurement kind + let data = query_last_submission(test.deps())?; + assert_eq!( + data, + LastSubmission { + block_height: env.block.height, + block_time: env.block.time, + data: Some(LastSubmittedData { + sender: nm1.clone(), + epoch_id: 10, + data: NodePerformance { + node_id: id1, + performance: "0.85".parse()?, + measurement_kind: measurement_mixnet.clone(), + }, + }), + } + ); + + // verify both measurements are stored independently in the same epoch + let bandwidth_results = test.read_raw_scores(10, id1, measurement_dvpn.clone())?; + assert_eq!(bandwidth_results.inner().len(), 1); + assert_eq!(bandwidth_results.inner()[0], "0.75".parse()?); + + let latency_results = test.read_raw_scores(10, id1, measurement_mixnet.clone())?; + assert_eq!(latency_results.inner().len(), 1); + assert_eq!(latency_results.inner()[0], "0.85".parse()?); + + Ok(()) + } + + #[test] + fn querying_full_historical_performance_paged() -> anyhow::Result<()> { + use std::collections::HashSet; + + let mut test = init_contract_tester(); + + // create & authorize NMs + let nm1 = test.generate_account(); + let nm2 = test.generate_account(); + let nm3 = test.generate_account(); + test.authorise_network_monitor(&nm1)?; + test.authorise_network_monitor(&nm2)?; + test.authorise_network_monitor(&nm3)?; + + // define measurement kinds + let admin = test.admin_unchecked(); + let kind_mixnet = String::from("mixnet"); + let kind_wireguard = String::from("wireguard"); + let kind_third = String::from("third"); + + test.execute_raw( + admin.clone(), + ExecuteMsg::DefineMeasurementKind { + measurement_kind: kind_mixnet.clone(), + }, + )?; + test.execute_raw( + admin.clone(), + ExecuteMsg::DefineMeasurementKind { + measurement_kind: kind_wireguard.clone(), + }, + )?; + test.execute_raw( + admin, + ExecuteMsg::DefineMeasurementKind { + measurement_kind: kind_third.clone(), + }, + )?; + + // prepare nodes + let node1 = test.bond_dummy_nymnode()?; + let node2 = test.bond_dummy_nymnode()?; + let node3 = test.bond_dummy_nymnode()?; + let node4 = test.bond_dummy_nymnode()?; + + test.set_mixnet_epoch(1)?; + + // epoch 1 + test.insert_raw_performance(&nm1, node1, kind_mixnet.clone(), "0.101")?; + test.insert_raw_performance(&nm2, node1, kind_wireguard.clone(), "0.102")?; + test.insert_raw_performance(&nm3, node1, kind_third.clone(), "0.103")?; + + test.insert_raw_performance(&nm1, node2, kind_mixnet.clone(), "0.201")?; + test.insert_raw_performance(&nm2, node2, kind_wireguard.clone(), "0.202")?; + + test.insert_raw_performance(&nm1, node3, kind_mixnet.clone(), "0.301")?; + + test.insert_raw_performance(&nm1, node4, kind_mixnet.clone(), "0.401")?; + test.insert_raw_performance(&nm2, node4, kind_third.clone(), "0.403")?; + + // epoch 2 + test.advance_mixnet_epoch()?; + + test.insert_raw_performance(&nm1, node1, kind_mixnet.clone(), "0.111")?; + + test.insert_raw_performance(&nm1, node2, kind_mixnet.clone(), "0.211")?; + test.insert_raw_performance(&nm2, node2, kind_wireguard.clone(), "0.212")?; + test.insert_raw_performance(&nm3, node2, kind_third.clone(), "0.213")?; + + test.insert_raw_performance(&nm1, node3, kind_wireguard.clone(), "0.312")?; + test.insert_raw_performance(&nm2, node3, kind_third.clone(), "0.313")?; + + test.insert_raw_performance(&nm1, node4, kind_mixnet.clone(), "0.411")?; + + // epoch 3 + test.advance_mixnet_epoch()?; + + test.insert_raw_performance(&nm1, node1, kind_mixnet.clone(), "0.121")?; + test.insert_raw_performance(&nm2, node1, kind_wireguard.clone(), "0.122")?; + + test.insert_raw_performance(&nm1, node2, kind_mixnet.clone(), "0.221")?; + + test.insert_raw_performance(&nm1, node3, kind_mixnet.clone(), "0.321")?; + test.insert_raw_performance(&nm2, node3, kind_wireguard.clone(), "0.322")?; + test.insert_raw_performance(&nm3, node3, kind_third.clone(), "0.323")?; + + let deps = test.deps(); + + // Helper function to validate right measurement kinds are present + // depending on (epoch_id, node_id) combination + let validate_completeness = |item: &HistoricalPerformance| { + let expected_kinds = match (item.epoch_id, item.node_id) { + (1, n) if n == node1 => vec![&kind_mixnet, &kind_wireguard, &kind_third], + (1, n) if n == node2 => vec![&kind_mixnet, &kind_wireguard], + (1, n) if n == node3 => vec![&kind_mixnet], + (1, n) if n == node4 => vec![&kind_mixnet, &kind_third], + (2, n) if n == node1 => vec![&kind_mixnet], + (2, n) if n == node2 => vec![&kind_mixnet, &kind_wireguard, &kind_third], + (2, n) if n == node3 => vec![&kind_wireguard, &kind_third], + (2, n) if n == node4 => vec![&kind_mixnet], + (3, n) if n == node1 => vec![&kind_mixnet, &kind_wireguard], + (3, n) if n == node2 => vec![&kind_mixnet], + (3, n) if n == node3 => vec![&kind_mixnet, &kind_wireguard, &kind_third], + _ => panic!( + "Unexpected epoch/node combination: {}/{}", + item.epoch_id, item.node_id + ), + }; + + assert_eq!( + item.performance.len(), + expected_kinds.len(), + "Node {}-{} has incomplete measurement kinds. Expected {}, got {}", + item.epoch_id, + item.node_id, + expected_kinds.len(), + item.performance.len() + ); + + for kind in expected_kinds { + assert!( + item.performance.contains_key(kind), + "Missing kind {} for node {}-{}", + kind, + item.epoch_id, + item.node_id + ); + } + }; + + // ===== full query (no pagination) ===== + let res = query_full_historical_performance_paged(deps, None, None)?; + + // total count (11 aggregated items: 4+4+3) + assert_eq!( + res.performance.len(), + 11, + "Expected 11 total items, got {}", + res.performance.len() + ); + + // ordering by (epoch, node) + for i in 1..res.performance.len() { + let prev = &res.performance[i - 1]; + let curr = &res.performance[i]; + assert!( + (prev.epoch_id, prev.node_id) < (curr.epoch_id, curr.node_id), + "Results not properly sorted: ({}, {}) should be before ({}, {})", + prev.epoch_id, + prev.node_id, + curr.epoch_id, + curr.node_id + ); + } + + // no duplicates + let keys: HashSet<_> = res + .performance + .iter() + .map(|p| (p.epoch_id, p.node_id)) + .collect(); + assert_eq!( + keys.len(), + 11, + "Found duplicate (epoch, node) pairs in results" + ); + + // Verify completeness for all items + for item in &res.performance { + validate_completeness(item); + } + + // Verify start_next_after is the last item + assert_eq!( + res.start_next_after, + Some((3, node3)), + "start_next_after should be the last item" + ); + + // ===== Pagination with small limit ===== + let mut all_pages = Vec::new(); + let mut start_after = None; + let mut page_count = 0; + // safety limit to prevent an infinite loop + let max_pages = 20; + + // test page by page if pagination works + loop { + let page = query_full_historical_performance_paged(deps, start_after, Some(1))?; + + if page.performance.is_empty() { + break; + } + + assert_eq!( + page.performance.len(), + 1, + "Page {} should have exactly 1 item", + page_count + 1 + ); + + // in each step, validate_completeness ensures correct data is present + // for that (epoch, node) combination + validate_completeness(&page.performance[0]); + + all_pages.extend(page.performance); + start_after = page.start_next_after; + page_count += 1; + + if start_after.is_none() { + break; + } + + assert!( + page_count < max_pages, + "Too many pages ({}), possible infinite loop!", + page_count + ); + } + + // verify totals + assert_eq!(all_pages.len(), 11,); + assert_eq!(page_count, 11,); + + // verify no duplicates across pages + let keys: HashSet<_> = all_pages.iter().map(|p| (p.epoch_id, p.node_id)).collect(); + assert_eq!( + keys.len(), + 11, + "Found duplicate (epoch, node) pairs across paginated results" + ); + + // ===== pagination with larger limit ===== + // Page 1: should get first 3 items from epoch 1 (nodes 1, 2, 3) + let page1 = query_full_historical_performance_paged(deps, None, Some(3))?; + assert_eq!(page1.performance.len(), 3, "Page 1 should have 3 items"); + assert_eq!(page1.performance[0].epoch_id, 1); + assert_eq!(page1.performance[0].node_id, node1); + assert_eq!(page1.performance[1].epoch_id, 1); + assert_eq!(page1.performance[1].node_id, node2); + assert_eq!(page1.performance[2].epoch_id, 1); + assert_eq!(page1.performance[2].node_id, node3); + assert_eq!(page1.start_next_after, Some((1, node3))); + + for item in &page1.performance { + validate_completeness(item); + } + + // Page 2: Should get node4 from epoch 1, then nodes 1,2 from epoch 2 + let page2 = query_full_historical_performance_paged(deps, page1.start_next_after, Some(3))?; + assert_eq!(page2.performance.len(), 3, "Page 2 should have 3 items"); + assert_eq!(page2.performance[0].epoch_id, 1); + assert_eq!(page2.performance[0].node_id, node4); + assert_eq!(page2.performance[1].epoch_id, 2); + assert_eq!(page2.performance[1].node_id, node1); + assert_eq!(page2.performance[2].epoch_id, 2); + assert_eq!(page2.performance[2].node_id, node2); + + // Verify no duplication of (10, node3) + assert!( + page2 + .performance + .iter() + .all(|p| (p.epoch_id, p.node_id) != (1, node3)), + ); + + for item in &page2.performance { + validate_completeness(item); + } + + // ===== SECTION 4: Pagination with start_after in Middle ===== + // Start from middle of epoch 11 (after node 2) + let res = query_full_historical_performance_paged(deps, Some((2, node2)), None)?; + assert_eq!(res.performance.len(), 5,); + + // Verify (2, node2) NOT included (exclusive bound) + assert!( + res.performance + .iter() + .all(|p| (p.epoch_id, p.node_id) != (11, node2)), + ); + + // Verify we get nodes 3,4 from epoch 2 and all from epoch 3 + assert_eq!(res.performance[0].epoch_id, 2); + assert_eq!(res.performance[0].node_id, node3); + assert_eq!(res.performance[1].epoch_id, 2); + assert_eq!(res.performance[1].node_id, node4); + assert_eq!(res.performance[2].epoch_id, 3); + assert_eq!(res.performance[2].node_id, node1); + assert_eq!(res.performance[3].epoch_id, 3); + assert_eq!(res.performance[3].node_id, node2); + assert_eq!(res.performance[4].epoch_id, 3); + assert_eq!(res.performance[4].node_id, node3); + + for item in &res.performance { + validate_completeness(item); + } + + // Start from middle with limit + let res = query_full_historical_performance_paged(deps, Some((2, node2)), Some(2))?; + assert_eq!(res.performance.len(), 2,); + + assert_eq!(res.performance[0].epoch_id, 2); + assert_eq!(res.performance[0].node_id, node3); + assert_eq!(res.performance[1].epoch_id, 2); + assert_eq!(res.performance[1].node_id, node4); + assert_eq!(res.start_next_after, Some((2, node4))); + + for item in &res.performance { + validate_completeness(item); + } + + // ===== edge Cases ===== + // start_after beyond last item + let res = query_full_historical_performance_paged(deps, Some((12, node3)), None)?; + assert!(res.performance.is_empty(),); + assert_eq!(res.start_next_after, None,); + + // start_after at nonexistent node (should jump to next epoch/node combo) + let res = query_full_historical_performance_paged(deps, Some((2, 9999)), None)?; + assert_eq!(res.performance.len(), 3,); + assert_eq!(res.performance[0].epoch_id, 3); + assert_eq!(res.performance[0].node_id, node1); + + // limit exceeding available data should return all items + let res = query_full_historical_performance_paged(deps, None, Some(1000))?; + assert_eq!(res.performance.len(), 11,); + assert_eq!(res.start_next_after, Some((3, node3))); + + // limit=0 should return empty + let res = query_full_historical_performance_paged(deps, None, Some(0))?; + assert!(res.performance.is_empty(),); + assert_eq!(res.start_next_after, None,); + + // ===== Collect all data again and verify against raw storage ===== + let all_data = query_full_historical_performance_paged(deps, None, None)?; + for item in &all_data.performance { + for (kind, percent) in &item.performance { + // verify the performance value matches the median from raw storage + let raw = test.read_raw_scores(item.epoch_id, item.node_id, kind.clone())?; + assert_eq!( + *percent, + raw.median(), + "Performance value mismatch for epoch {} node {} kind {}", + item.epoch_id, + item.node_id, + kind + ); + } + } + Ok(()) } } diff --git a/contracts/performance/src/storage.rs b/contracts/performance/src/storage.rs index e392fb78ba7..79ee4329060 100644 --- a/contracts/performance/src/storage.rs +++ b/contracts/performance/src/storage.rs @@ -1,6 +1,8 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use std::collections::HashMap; + use crate::helpers::MixnetContractQuerier; use cosmwasm_std::{Addr, Deps, DepsMut, Env, StdError, Storage}; use cw_controllers::Admin; @@ -192,12 +194,21 @@ impl NymPerformanceContractStorage { let mut accepted_scores = 0; let mut non_existent_nodes = Vec::new(); + let mut non_existent_measurement_kind = Vec::new(); // 3. submit it if self.node_bonded(deps.as_ref(), first.node_id)? { - self.performance_results - .insert_performance_data(deps.storage, epoch_id, first)?; - accepted_scores += 1; + match self + .performance_results + .insert_performance_data(deps.storage, epoch_id, first) + { + Ok(_) => accepted_scores += 1, + Err(NymPerformanceContractError::UnsupportedMeasurementKind { kind }) => { + non_existent_measurement_kind.push(kind); + } + // propagate other errors + Err(e) => return Err(e), + }; } else { non_existent_nodes.push(first.node_id); } @@ -245,7 +256,7 @@ impl NymPerformanceContractStorage { data: Some(LastSubmittedData { sender: sender.clone(), epoch_id, - data: *last, + data: last.clone(), }), }, )?; @@ -253,6 +264,7 @@ impl NymPerformanceContractStorage { Ok(BatchSubmissionResult { accepted_scores, non_existent_nodes, + non_existent_measurement_kind, }) } @@ -318,17 +330,51 @@ impl NymPerformanceContractStorage { .retire(deps, &env, sender, &network_monitor) } - pub fn try_load_performance( + pub fn try_load_measurement_kind( &self, storage: &dyn Storage, epoch_id: EpochId, node_id: NodeId, - ) -> Result, NymPerformanceContractError> { - Ok(self - .performance_results + measurement_kind: MeasurementKind, + ) -> Result, NymPerformanceContractError> { + self.performance_results + .assert_measurement_defined(storage, measurement_kind.clone())?; + + let key = (epoch_id, node_id, measurement_kind); + self.performance_results .results - .may_load(storage, (epoch_id, node_id))? - .map(|r| r.median())) + .may_load(storage, key) + .map_err(From::from) + } + + pub fn try_load_performance( + &self, + storage: &dyn Storage, + epoch_id: EpochId, + node_id: NodeId, + ) -> Result, NymPerformanceContractError> { + // Get all defined measurement kinds + let measurement_kinds = self.performance_results.defined_measurements(storage)?; + + // short-circuit if no measurement kinds are defined + if measurement_kinds.is_empty() { + return Ok(HashMap::new()); + } + + let mut performance_per_kind: HashMap = HashMap::new(); + + // collect median values per measurement kind + for kind in measurement_kinds { + if let Some(results) = self + .performance_results + .results + .may_load(storage, (epoch_id, node_id, kind.clone()))? + { + performance_per_kind.insert(kind, results.median()); + } + } + + Ok(performance_per_kind) } pub fn remove_node_measurements( @@ -340,9 +386,16 @@ impl NymPerformanceContractStorage { ) -> Result<(), NymPerformanceContractError> { self.ensure_is_admin(deps.as_ref(), sender)?; - self.performance_results - .results - .remove(deps.storage, (epoch_id, node_id)); + // Remove all measurements for this (epoch_id, node_id) pair + let measurement_kinds = self + .performance_results + .defined_measurements(deps.storage)?; + for kind in measurement_kinds { + self.performance_results + .results + .remove(deps.storage, (epoch_id, node_id, kind)); + } + Ok(()) } @@ -355,7 +408,7 @@ impl NymPerformanceContractStorage { self.ensure_is_admin(deps.as_ref(), sender)?; // 1. purge the entries according to the limit - self.performance_results.results.prefix(epoch_id).clear( + self.performance_results.results.sub_prefix(epoch_id).clear( deps.storage, Some(retrieval_limits::EPOCH_PERFORMANCE_PURGE_LIMIT), ); @@ -364,7 +417,7 @@ impl NymPerformanceContractStorage { let additional_entries_to_remove_remaining = !self .performance_results .results - .prefix(epoch_id) + .sub_prefix(epoch_id) .is_empty(deps.storage); Ok(RemoveEpochMeasurementsResponse { @@ -457,7 +510,10 @@ impl NetworkMonitorsStorage { } pub(crate) struct PerformanceResultsStorage { - pub(crate) results: Map<(EpochId, NodeId), NodeResults>, + pub(crate) results: Map<(EpochId, NodeId, MeasurementKind), NodeResults>, + + /// only measurements defined here can be submitted, as defined by contract admin + defined_measurements: Map, // in order to ensure NM does not resubmit results, we keep metadata // of the latest submitted information @@ -465,11 +521,15 @@ pub(crate) struct PerformanceResultsStorage { pub(crate) submission_metadata: Map<&'static Addr, NetworkMonitorSubmissionMetadata>, } +// stringly typed because admin can set new measurements after contract is deployed +pub type MeasurementKind = String; + impl PerformanceResultsStorage { #[allow(clippy::new_without_default)] const fn new() -> Self { PerformanceResultsStorage { - results: Map::new(storage_keys::PERFORMANCE_RESULTS), + results: Map::new(storage_keys::PERFORMANCE_RESULTS_PER_KIND), + defined_measurements: Map::new(storage_keys::PERFORMANCE_DEFINED_KINDS), submission_metadata: Map::new(storage_keys::SUBMISSION_METADATA), } } @@ -482,10 +542,12 @@ impl PerformanceResultsStorage { epoch_id: EpochId, data: &NodePerformance, ) -> Result<(), NymPerformanceContractError> { + self.assert_measurement_defined(storage, data.measurement_kind.clone())?; + let performance = data.performance; - let key = (epoch_id, data.node_id); - let updated = match self.results.may_load(storage, key)? { + let key = (epoch_id, data.node_id, data.measurement_kind.clone()); + let updated = match self.results.may_load(storage, key.clone())? { None => NodeResults::new(performance), Some(mut existing) => { existing.insert_new(performance); @@ -497,6 +559,76 @@ impl PerformanceResultsStorage { Ok(()) } + pub(crate) fn defined_measurements( + &self, + storage: &dyn Storage, + ) -> Result, cosmwasm_std::StdError> { + self.defined_measurements + .keys(storage, None, None, cosmwasm_std::Order::Ascending) + // turn a vec of Results into a single Result + .collect::, cosmwasm_std::StdError>>() + } + + fn is_measurement_defined( + &self, + storage: &dyn Storage, + measurement_kind: MeasurementKind, + ) -> Result { + self.defined_measurements + .may_load(storage, measurement_kind) + .map(|entry| entry.is_some()) + } + + fn assert_measurement_defined( + &self, + storage: &dyn Storage, + measurement_kind: MeasurementKind, + ) -> Result<(), NymPerformanceContractError> { + if !self.is_measurement_defined(storage, measurement_kind.clone())? { + Err(NymPerformanceContractError::UnsupportedMeasurementKind { + kind: measurement_kind, + }) + } else { + Ok(()) + } + } + + /// assumes authorization had been performed + pub fn define_new_measurement_kind( + &self, + storage: &mut dyn Storage, + measurement_kind: MeasurementKind, + ) -> Result<(), NymPerformanceContractError> { + if self.is_measurement_defined(storage, measurement_kind.clone())? { + return Err(NymPerformanceContractError::InvalidInput(format!( + "Measurement {} already defined", + &measurement_kind + ))); + } + + self.defined_measurements + .save(storage, measurement_kind, &()) + .map_err(From::from) + } + + /// assumes authorization had been performed + pub fn retire_measurement_kind( + &self, + storage: &mut dyn Storage, + measurement_kind: MeasurementKind, + ) -> Result<(), NymPerformanceContractError> { + if !self.is_measurement_defined(storage, measurement_kind.clone())? { + return Err(NymPerformanceContractError::InvalidInput(format!( + "Invalid input: measurement {} not defined", + &measurement_kind + ))); + } + + self.defined_measurements.remove(storage, measurement_kind); + + Ok(()) + } + fn update_submission_metadata( &self, storage: &mut dyn Storage, @@ -515,6 +647,11 @@ impl PerformanceResultsStorage { Ok(()) } + // TODO: this logic was written with ONE measurement kind in mind. + // Whether different measurement kinds are submitted sequentially or not, + // batched by node_id or not etc. is an open question. Until that is decided, + // this implementation is flawed and needs to be FIXED. + // There is a unit test (currently ignored) showing desired behaviour fn ensure_non_stale_submission( &self, storage: &dyn Storage, @@ -579,7 +716,7 @@ mod tests { #[cfg(test)] mod performance_contract_storage { use super::*; - use crate::testing::{init_contract_tester, PerformanceContractTesterExt, PreInitContract}; + use crate::testing::{PerformanceContractTesterExt, PreInitContract, init_contract_tester}; use nym_contracts_common_testing::{AdminExt, ContractOpts}; #[cfg(test)] @@ -786,15 +923,26 @@ mod tests { tester.authorise_network_monitor(&nm1)?; + // required for dummy node performance to be submittable + tester.define_dummy_measurement_kind().unwrap(); + // authorised network monitor can submit the results just fine let perf = tester.dummy_node_performance(); - assert!(storage - .submit_performance_data(tester.deps_mut(), env.clone(), &nm1, 0, perf) - .is_ok()); + assert!( + storage + .submit_performance_data( + tester.deps_mut(), + env.clone(), + &nm1, + 0, + perf.clone() + ) + .is_ok() + ); // unauthorised address is rejected let res = storage - .submit_performance_data(tester.deps_mut(), env.clone(), &nm2, 0, perf) + .submit_performance_data(tester.deps_mut(), env.clone(), &nm2, 0, perf.clone()) .unwrap_err(); assert_eq!( res, @@ -805,13 +953,27 @@ mod tests { // it is fine after explicit authorisation though tester.authorise_network_monitor(&nm2)?; - assert!(storage - .submit_performance_data(tester.deps_mut(), env.clone(), &nm2, 0, perf) - .is_ok()); + assert!( + storage + .submit_performance_data( + tester.deps_mut(), + env.clone(), + &nm2, + 0, + perf.clone() + ) + .is_ok() + ); // and address that was never authorised still fails let res = storage - .submit_performance_data(tester.deps_mut(), env.clone(), &unauthorised, 0, perf) + .submit_performance_data( + tester.deps_mut(), + env.clone(), + &unauthorised, + 0, + perf.clone(), + ) .unwrap_err(); assert_eq!( res, @@ -830,26 +992,38 @@ mod tests { let nm = tester.addr_make("network-monitor"); tester.authorise_network_monitor(&nm)?; + let measurement_kind = tester.define_dummy_measurement_kind().unwrap(); + let id1 = tester.bond_dummy_nymnode()?; let id2 = tester.bond_dummy_nymnode()?; let data = NodePerformance { node_id: id1, performance: Percent::hundred(), + measurement_kind: measurement_kind.clone(), }; let another_data = NodePerformance { node_id: id2, performance: Percent::hundred(), + measurement_kind, }; // first submission - assert!(storage - .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 0, data) - .is_ok()); + assert!( + storage + .submit_performance_data( + tester.deps_mut(), + env.clone(), + &nm, + 0, + data.clone() + ) + .is_ok() + ); // second submission let res = storage - .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 0, data) + .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 0, data.clone()) .unwrap_err(); assert_eq!( @@ -863,14 +1037,30 @@ mod tests { ); // another submission works fine - assert!(storage - .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 0, another_data) - .is_ok()); + assert!( + storage + .submit_performance_data( + tester.deps_mut(), + env.clone(), + &nm, + 0, + another_data + ) + .is_ok() + ); // original one works IF it's for next epoch - assert!(storage - .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 1, data) - .is_ok()); + assert!( + storage + .submit_performance_data( + tester.deps_mut(), + env.clone(), + &nm, + 1, + data.clone() + ) + .is_ok() + ); let res = storage .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 0, data) @@ -899,21 +1089,34 @@ mod tests { let id1 = tester.bond_dummy_nymnode()?; let id2 = tester.bond_dummy_nymnode()?; + + let measurement_kind = tester.define_dummy_measurement_kind().unwrap(); + let data = NodePerformance { node_id: id1, performance: Percent::hundred(), + measurement_kind: measurement_kind.clone(), }; let another_data = NodePerformance { node_id: id2, performance: Percent::hundred(), + measurement_kind, }; - assert!(storage - .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 0, another_data) - .is_ok()); + assert!( + storage + .submit_performance_data( + tester.deps_mut(), + env.clone(), + &nm, + 0, + another_data + ) + .is_ok() + ); let res = storage - .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 0, data) + .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 0, data.clone()) .unwrap_err(); assert_eq!( @@ -927,9 +1130,17 @@ mod tests { ); // check across epochs - assert!(storage - .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 10, data) - .is_ok()); + assert!( + storage + .submit_performance_data( + tester.deps_mut(), + env.clone(), + &nm, + 10, + data.clone() + ) + .is_ok() + ); let res = storage .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 9, data) @@ -955,12 +1166,13 @@ mod tests { let nm = tester.addr_make("network-monitor"); tester.authorise_network_monitor(&nm)?; + tester.define_dummy_measurement_kind().unwrap(); let env = tester.env(); // if NM got authorised at epoch 10, it can only submit data for epochs >=10 let perf = tester.dummy_node_performance(); let res = storage - .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 0, perf) + .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 0, perf.clone()) .unwrap_err(); assert_eq!( @@ -974,7 +1186,7 @@ mod tests { ); let res = storage - .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 9, perf) + .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 9, perf.clone()) .unwrap_err(); assert_eq!( @@ -987,12 +1199,28 @@ mod tests { } ); - assert!(storage - .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 10, perf) - .is_ok()); - assert!(storage - .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 11, perf) - .is_ok()); + assert!( + storage + .submit_performance_data( + tester.deps_mut(), + env.clone(), + &nm, + 10, + perf.clone() + ) + .is_ok() + ); + assert!( + storage + .submit_performance_data( + tester.deps_mut(), + env.clone(), + &nm, + 11, + perf.clone() + ) + .is_ok() + ); Ok(()) } @@ -1017,6 +1245,8 @@ mod tests { assert_eq!(metadata.last_submitted_epoch_id, 0); assert_eq!(metadata.last_submitted_node_id, 0); + let measurement_kind = tester.define_dummy_measurement_kind().unwrap(); + storage.submit_performance_data( tester.deps_mut(), env.clone(), @@ -1025,6 +1255,7 @@ mod tests { NodePerformance { node_id: nodes[0], performance: Default::default(), + measurement_kind: measurement_kind.clone(), }, )?; let metadata = storage @@ -1042,6 +1273,7 @@ mod tests { NodePerformance { node_id: nodes[3], performance: Default::default(), + measurement_kind: measurement_kind.clone(), }, )?; let metadata = storage @@ -1059,6 +1291,7 @@ mod tests { NodePerformance { node_id: nodes[1], performance: Default::default(), + measurement_kind: measurement_kind.clone(), }, )?; let metadata = storage @@ -1076,6 +1309,7 @@ mod tests { NodePerformance { node_id: nodes[8], performance: Default::default(), + measurement_kind, }, )?; let metadata = storage @@ -1101,7 +1335,7 @@ mod tests { for _ in 0..10 { nodes.push(tester.bond_dummy_nymnode()?); } - + let measurement_kind = tester.define_dummy_measurement_kind().unwrap(); storage.submit_performance_data( tester.deps_mut(), env.clone(), @@ -1110,6 +1344,7 @@ mod tests { NodePerformance { node_id: nodes[0], performance: Default::default(), + measurement_kind: measurement_kind.clone(), }, )?; let data = storage.last_performance_submission.load(&tester)?; @@ -1124,6 +1359,7 @@ mod tests { data: NodePerformance { node_id: nodes[0], performance: Default::default(), + measurement_kind: measurement_kind.clone() }, }), } @@ -1137,6 +1373,7 @@ mod tests { NodePerformance { node_id: nodes[6], performance: Default::default(), + measurement_kind: measurement_kind.clone(), }, )?; let data = storage.last_performance_submission.load(&tester)?; @@ -1151,6 +1388,7 @@ mod tests { data: NodePerformance { node_id: nodes[6], performance: Default::default(), + measurement_kind: measurement_kind.clone() }, }), } @@ -1164,6 +1402,7 @@ mod tests { NodePerformance { node_id: nodes[2], performance: Default::default(), + measurement_kind: measurement_kind.clone(), }, )?; let data = storage.last_performance_submission.load(&tester)?; @@ -1178,6 +1417,7 @@ mod tests { data: NodePerformance { node_id: nodes[2], performance: Default::default(), + measurement_kind: measurement_kind.clone() }, }), } @@ -1191,6 +1431,7 @@ mod tests { NodePerformance { node_id: nodes[9], performance: Default::default(), + measurement_kind: measurement_kind.clone(), }, )?; let data = storage.last_performance_submission.load(&tester)?; @@ -1205,6 +1446,7 @@ mod tests { data: NodePerformance { node_id: nodes[9], performance: Default::default(), + measurement_kind: measurement_kind.clone() }, }), } @@ -1221,15 +1463,23 @@ mod tests { let nm = tester.addr_make("network-monitor"); tester.authorise_network_monitor(&nm)?; + let measurement_kind = tester.define_dummy_measurement_kind().unwrap(); let dummy_perf = NodePerformance { node_id: 12345, performance: Percent::from_percentage_value(69)?, + measurement_kind: measurement_kind.clone(), }; // no node bonded at this point let res = storage - .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 0, dummy_perf) + .submit_performance_data( + tester.deps_mut(), + env.clone(), + &nm, + 0, + dummy_perf.clone(), + ) .unwrap_err(); assert_eq!( res, @@ -1243,6 +1493,7 @@ mod tests { let perf = NodePerformance { node_id, performance: Default::default(), + measurement_kind: measurement_kind.clone(), }; let res = storage.submit_performance_data(tester.deps_mut(), env.clone(), &nm, 0, perf); @@ -1252,7 +1503,13 @@ mod tests { tester.unbond_nymnode(node_id)?; let res = storage - .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 0, dummy_perf) + .submit_performance_data( + tester.deps_mut(), + env.clone(), + &nm, + 0, + dummy_perf.clone(), + ) .unwrap_err(); assert_eq!( res, @@ -1282,15 +1539,17 @@ mod tests { let perf = tester.dummy_node_performance(); // authorised network monitor can submit the results just fine - assert!(storage - .batch_submit_performance_results( - tester.deps_mut(), - env.clone(), - &nm1, - 0, - vec![perf] - ) - .is_ok()); + assert!( + storage + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm1, + 0, + vec![perf.clone()] + ) + .is_ok() + ); // unauthorised address is rejected let res = storage @@ -1299,7 +1558,7 @@ mod tests { env.clone(), &nm2, 0, - vec![perf], + vec![perf.clone()], ) .unwrap_err(); assert_eq!( @@ -1311,15 +1570,17 @@ mod tests { // it is fine after explicit authorisation though tester.authorise_network_monitor(&nm2)?; - assert!(storage - .batch_submit_performance_results( - tester.deps_mut(), - env.clone(), - &nm2, - 0, - vec![perf] - ) - .is_ok()); + assert!( + storage + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm2, + 0, + vec![perf.clone()] + ) + .is_ok() + ); // and address that was never authorised still fails let res = storage @@ -1351,24 +1612,28 @@ mod tests { let id1 = tester.bond_dummy_nymnode()?; let id2 = tester.bond_dummy_nymnode()?; let id3 = tester.bond_dummy_nymnode()?; + let measurement_kind = tester.define_dummy_measurement_kind().unwrap(); let data = NodePerformance { node_id: id1, performance: Percent::hundred(), + measurement_kind: measurement_kind.clone(), }; let another_data = NodePerformance { node_id: id2, performance: Percent::hundred(), + measurement_kind: measurement_kind.clone(), }; let more_data = NodePerformance { node_id: id3, performance: Percent::hundred(), + measurement_kind: measurement_kind.clone(), }; - let duplicates = vec![data, data]; - let another_dups = vec![another_data, another_data]; - let unsorted = vec![another_data, data]; - let semi_sorted = vec![data, more_data, another_data]; - let sorted = vec![data, another_data, more_data]; + let duplicates = vec![data.clone(), data.clone()]; + let another_dups = vec![another_data.clone(), another_data.clone()]; + let unsorted = vec![another_data.clone(), data.clone()]; + let semi_sorted = vec![data.clone(), more_data.clone(), another_data.clone()]; + let sorted = vec![data, another_data.clone(), more_data]; let res = storage .batch_submit_performance_results( @@ -1414,15 +1679,17 @@ mod tests { .unwrap_err(); assert_eq!(res, NymPerformanceContractError::UnsortedBatchSubmission); - assert!(storage - .batch_submit_performance_results( - tester.deps_mut(), - env.clone(), - &nm, - 0, - sorted - ) - .is_ok()); + assert!( + storage + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 0, + sorted + ) + .is_ok() + ); Ok(()) } @@ -1436,25 +1703,32 @@ mod tests { let id1 = tester.bond_dummy_nymnode()?; let id2 = tester.bond_dummy_nymnode()?; + + let measurement_kind = tester.define_dummy_measurement_kind().unwrap(); + let data = NodePerformance { node_id: id1, performance: Percent::hundred(), + measurement_kind: measurement_kind.clone(), }; let another_data = NodePerformance { node_id: id2, performance: Percent::hundred(), + measurement_kind: measurement_kind.clone(), }; // first submission - assert!(storage - .batch_submit_performance_results( - tester.deps_mut(), - env.clone(), - &nm, - 0, - vec![data] - ) - .is_ok()); + assert!( + storage + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 0, + vec![data.clone()] + ) + .is_ok() + ); // second submission let res = storage @@ -1463,7 +1737,7 @@ mod tests { env.clone(), &nm, 0, - vec![data], + vec![data.clone()], ) .unwrap_err(); @@ -1478,26 +1752,30 @@ mod tests { ); // another submission works fine - assert!(storage - .batch_submit_performance_results( - tester.deps_mut(), - env.clone(), - &nm, - 0, - vec![another_data] - ) - .is_ok()); + assert!( + storage + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 0, + vec![another_data] + ) + .is_ok() + ); // original one works IF it's for next epoch - assert!(storage - .batch_submit_performance_results( - tester.deps_mut(), - env.clone(), - &nm, - 1, - vec![data] - ) - .is_ok()); + assert!( + storage + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 1, + vec![data.clone()] + ) + .is_ok() + ); let res = storage .batch_submit_performance_results( @@ -1532,24 +1810,31 @@ mod tests { let id1 = tester.bond_dummy_nymnode()?; let id2 = tester.bond_dummy_nymnode()?; + + let measurement_kind = tester.define_dummy_measurement_kind().unwrap(); + let data = NodePerformance { node_id: id1, performance: Percent::hundred(), + measurement_kind: measurement_kind.clone(), }; let another_data = NodePerformance { node_id: id2, performance: Percent::hundred(), + measurement_kind: measurement_kind.clone(), }; - assert!(storage - .batch_submit_performance_results( - tester.deps_mut(), - env.clone(), - &nm, - 0, - vec![another_data] - ) - .is_ok()); + assert!( + storage + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 0, + vec![another_data] + ) + .is_ok() + ); let res = storage .batch_submit_performance_results( @@ -1557,7 +1842,7 @@ mod tests { env.clone(), &nm, 0, - vec![data], + vec![data.clone()], ) .unwrap_err(); @@ -1572,15 +1857,17 @@ mod tests { ); // check across epochs - assert!(storage - .batch_submit_performance_results( - tester.deps_mut(), - env.clone(), - &nm, - 10, - vec![data] - ) - .is_ok()); + assert!( + storage + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 10, + vec![data.clone()] + ) + .is_ok() + ); let res = storage .batch_submit_performance_results( @@ -1623,7 +1910,7 @@ mod tests { env.clone(), &nm, 0, - vec![perf], + vec![perf.clone()], ) .unwrap_err(); @@ -1643,7 +1930,7 @@ mod tests { env.clone(), &nm, 9, - vec![perf], + vec![perf.clone()], ) .unwrap_err(); @@ -1657,24 +1944,28 @@ mod tests { } ); - assert!(storage - .batch_submit_performance_results( - tester.deps_mut(), - env.clone(), - &nm, - 10, - vec![perf] - ) - .is_ok()); - assert!(storage - .batch_submit_performance_results( - tester.deps_mut(), - env.clone(), - &nm, - 11, - vec![perf] - ) - .is_ok()); + assert!( + storage + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 10, + vec![perf.clone()] + ) + .is_ok() + ); + assert!( + storage + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 11, + vec![perf] + ) + .is_ok() + ); Ok(()) } @@ -1699,6 +1990,8 @@ mod tests { nodes.push(tester.bond_dummy_nymnode()?); } + let measurement_kind = tester.define_dummy_measurement_kind().unwrap(); + // single submission storage.batch_submit_performance_results( tester.deps_mut(), @@ -1708,6 +2001,7 @@ mod tests { vec![NodePerformance { node_id: nodes[0], performance: Default::default(), + measurement_kind: measurement_kind.clone(), }], )?; let metadata = storage @@ -1726,6 +2020,7 @@ mod tests { vec![NodePerformance { node_id: nodes[1], performance: Default::default(), + measurement_kind: measurement_kind.clone(), }], )?; let metadata = storage @@ -1745,14 +2040,17 @@ mod tests { NodePerformance { node_id: nodes[2], performance: Default::default(), + measurement_kind: measurement_kind.clone(), }, NodePerformance { node_id: nodes[3], performance: Default::default(), + measurement_kind: measurement_kind.clone(), }, NodePerformance { node_id: nodes[4], performance: Default::default(), + measurement_kind: measurement_kind.clone(), }, ], )?; @@ -1773,14 +2071,17 @@ mod tests { NodePerformance { node_id: nodes[1], performance: Default::default(), + measurement_kind: measurement_kind.clone(), }, NodePerformance { node_id: nodes[6], performance: Default::default(), + measurement_kind: measurement_kind.clone(), }, NodePerformance { node_id: nodes[8], performance: Default::default(), + measurement_kind: measurement_kind.clone(), }, ], )?; @@ -1808,6 +2109,7 @@ mod tests { nodes.push(tester.bond_dummy_nymnode()?); } + let measurement_kind = tester.define_dummy_measurement_kind().unwrap(); // single submission storage.batch_submit_performance_results( tester.deps_mut(), @@ -1817,6 +2119,7 @@ mod tests { vec![NodePerformance { node_id: nodes[0], performance: Default::default(), + measurement_kind: measurement_kind.clone(), }], )?; let data = storage.last_performance_submission.load(&tester)?; @@ -1831,6 +2134,7 @@ mod tests { data: NodePerformance { node_id: nodes[0], performance: Default::default(), + measurement_kind: measurement_kind.clone() }, }), } @@ -1845,6 +2149,7 @@ mod tests { vec![NodePerformance { node_id: nodes[1], performance: Default::default(), + measurement_kind: measurement_kind.clone(), }], )?; let data = storage.last_performance_submission.load(&tester)?; @@ -1859,6 +2164,7 @@ mod tests { data: NodePerformance { node_id: nodes[1], performance: Default::default(), + measurement_kind: measurement_kind.clone() }, }), } @@ -1874,14 +2180,17 @@ mod tests { NodePerformance { node_id: nodes[2], performance: Default::default(), + measurement_kind: measurement_kind.clone(), }, NodePerformance { node_id: nodes[3], performance: Default::default(), + measurement_kind: measurement_kind.clone(), }, NodePerformance { node_id: nodes[4], performance: Default::default(), + measurement_kind: measurement_kind.clone(), }, ], )?; @@ -1897,6 +2206,7 @@ mod tests { data: NodePerformance { node_id: nodes[4], performance: Default::default(), + measurement_kind: measurement_kind.clone() }, }), } @@ -1912,14 +2222,17 @@ mod tests { NodePerformance { node_id: nodes[1], performance: Default::default(), + measurement_kind: measurement_kind.clone(), }, NodePerformance { node_id: nodes[7], performance: Default::default(), + measurement_kind: measurement_kind.clone(), }, NodePerformance { node_id: nodes[8], performance: Default::default(), + measurement_kind: measurement_kind.clone(), }, ], )?; @@ -1935,6 +2248,7 @@ mod tests { data: NodePerformance { node_id: nodes[8], performance: Default::default(), + measurement_kind: measurement_kind.clone() }, }), } @@ -1964,6 +2278,8 @@ mod tests { let env = tester.env(); + let measurement_kind = tester.define_dummy_measurement_kind().unwrap(); + // single id - nothing bonded let res = storage.batch_submit_performance_results( tester.deps_mut(), @@ -1973,6 +2289,7 @@ mod tests { vec![NodePerformance { node_id: 999999, performance: Default::default(), + measurement_kind: measurement_kind.clone(), }], )?; assert_eq!(res.accepted_scores, 0); @@ -1988,10 +2305,12 @@ mod tests { NodePerformance { node_id: nym_node1, performance: Default::default(), + measurement_kind: measurement_kind.clone(), }, NodePerformance { node_id: 999999, performance: Default::default(), + measurement_kind: measurement_kind.clone(), }, ], )?; @@ -2008,18 +2327,22 @@ mod tests { NodePerformance { node_id: 2, performance: Default::default(), + measurement_kind: measurement_kind.clone(), }, NodePerformance { node_id: nym_node1, performance: Default::default(), + measurement_kind: measurement_kind.clone(), }, NodePerformance { node_id: nym_node_between, performance: Default::default(), + measurement_kind: measurement_kind.clone(), }, NodePerformance { node_id: nym_node2, performance: Default::default(), + measurement_kind: measurement_kind.clone(), }, ], )?; @@ -2091,9 +2414,11 @@ mod tests { .unwrap_err(); assert_eq!(res, NymPerformanceContractError::Admin(NotAdmin {})); - assert!(storage - .authorise_network_monitor(tester.deps_mut(), &env, &admin, nm) - .is_ok()); + assert!( + storage + .authorise_network_monitor(tester.deps_mut(), &env, &admin, nm) + .is_ok() + ); // change admin let new_admin = tester.addr_make("new-admin"); @@ -2107,9 +2432,11 @@ mod tests { .unwrap_err(); assert_eq!(res, NymPerformanceContractError::Admin(NotAdmin {})); - assert!(storage - .authorise_network_monitor(tester.deps_mut(), &env, &new_admin, another_nm) - .is_ok()); + assert!( + storage + .authorise_network_monitor(tester.deps_mut(), &env, &new_admin, another_nm) + .is_ok() + ); Ok(()) } @@ -2234,9 +2561,11 @@ mod tests { .unwrap_err(); assert_eq!(res, NymPerformanceContractError::Admin(NotAdmin {})); - assert!(storage - .retire_network_monitor(tester.deps_mut(), env.clone(), &admin, nm) - .is_ok()); + assert!( + storage + .retire_network_monitor(tester.deps_mut(), env.clone(), &admin, nm) + .is_ok() + ); // change admin let new_admin = tester.addr_make("new-admin"); @@ -2253,9 +2582,11 @@ mod tests { .unwrap_err(); assert_eq!(res, NymPerformanceContractError::Admin(NotAdmin {})); - assert!(storage - .retire_network_monitor(tester.deps_mut(), env, &new_admin, another_nm) - .is_ok()); + assert!( + storage + .retire_network_monitor(tester.deps_mut(), env, &new_admin, another_nm) + .is_ok() + ); Ok(()) } @@ -2302,7 +2633,10 @@ mod tests { // no results let node_id = tester.bond_dummy_nymnode()?; - assert_eq!(storage.try_load_performance(&tester, 0, node_id)?, None); + assert_eq!( + storage.try_load_performance(&tester, 0, node_id)?, + HashMap::new() + ); // // always returns median value with 2decimal places precision @@ -2310,10 +2644,12 @@ mod tests { // single result let node_id = tester.bond_dummy_nymnode()?; - tester.insert_raw_performance(&nms[0], node_id, "0.42")?; + let measurement_kind = tester.define_dummy_measurement_kind().unwrap(); + tester.insert_raw_performance(&nms[0], node_id, measurement_kind.clone(), "0.42")?; assert_eq!( storage .try_load_performance(&tester, 0, node_id)? + .get(&measurement_kind) .unwrap() .value() .to_string(), @@ -2322,11 +2658,12 @@ mod tests { // two results (median doesn't require changing decimal places) let node_id = tester.bond_dummy_nymnode()?; - tester.insert_raw_performance(&nms[0], node_id, "0.50")?; - tester.insert_raw_performance(&nms[1], node_id, "0.40")?; + tester.insert_raw_performance(&nms[0], node_id, measurement_kind.clone(), "0.50")?; + tester.insert_raw_performance(&nms[1], node_id, measurement_kind.clone(), "0.40")?; assert_eq!( storage .try_load_performance(&tester, 0, node_id)? + .get(&measurement_kind) .unwrap() .value() .to_string(), @@ -2335,11 +2672,12 @@ mod tests { // two results (median requires changing decimal places) let node_id = tester.bond_dummy_nymnode()?; - tester.insert_raw_performance(&nms[0], node_id, "0.58")?; - tester.insert_raw_performance(&nms[1], node_id, "0.45")?; + tester.insert_raw_performance(&nms[0], node_id, measurement_kind.clone(), "0.58")?; + tester.insert_raw_performance(&nms[1], node_id, measurement_kind.clone(), "0.45")?; assert_eq!( storage .try_load_performance(&tester, 0, node_id)? + .get(&measurement_kind) .unwrap() .value() .to_string(), @@ -2348,12 +2686,13 @@ mod tests { // three results (median is the middle value rather than the average) let node_id = tester.bond_dummy_nymnode()?; - tester.insert_raw_performance(&nms[0], node_id, "0.12")?; - tester.insert_raw_performance(&nms[1], node_id, "0.34")?; - tester.insert_raw_performance(&nms[2], node_id, "0.56")?; + tester.insert_raw_performance(&nms[0], node_id, measurement_kind.clone(), "0.12")?; + tester.insert_raw_performance(&nms[1], node_id, measurement_kind.clone(), "0.34")?; + tester.insert_raw_performance(&nms[2], node_id, measurement_kind.clone(), "0.56")?; assert_eq!( storage .try_load_performance(&tester, 0, node_id)? + .get(&measurement_kind) .unwrap() .value() .to_string(), @@ -2362,14 +2701,15 @@ mod tests { // five results (notice how they're not inserted sorted) let node_id = tester.bond_dummy_nymnode()?; - tester.insert_raw_performance(&nms[0], node_id, "0.9")?; - tester.insert_raw_performance(&nms[1], node_id, "0.9")?; - tester.insert_raw_performance(&nms[2], node_id, "0.1")?; - tester.insert_raw_performance(&nms[4], node_id, "0.1")?; - tester.insert_raw_performance(&nms[5], node_id, "0.7")?; + tester.insert_raw_performance(&nms[0], node_id, measurement_kind.clone(), "0.9")?; + tester.insert_raw_performance(&nms[1], node_id, measurement_kind.clone(), "0.9")?; + tester.insert_raw_performance(&nms[2], node_id, measurement_kind.clone(), "0.1")?; + tester.insert_raw_performance(&nms[4], node_id, measurement_kind.clone(), "0.1")?; + tester.insert_raw_performance(&nms[5], node_id, measurement_kind.clone(), "0.7")?; assert_eq!( storage .try_load_performance(&tester, 0, node_id)? + .get(&measurement_kind) .unwrap() .value() .to_string(), @@ -2378,15 +2718,16 @@ mod tests { // six results (same as above, but average of middle values) let node_id = tester.bond_dummy_nymnode()?; - tester.insert_raw_performance(&nms[0], node_id, "0.9")?; - tester.insert_raw_performance(&nms[1], node_id, "0.9")?; - tester.insert_raw_performance(&nms[2], node_id, "0.1")?; - tester.insert_raw_performance(&nms[3], node_id, "0.1")?; - tester.insert_raw_performance(&nms[4], node_id, "0.2")?; - tester.insert_raw_performance(&nms[5], node_id, "0.3")?; + tester.insert_raw_performance(&nms[0], node_id, measurement_kind.clone(), "0.9")?; + tester.insert_raw_performance(&nms[1], node_id, measurement_kind.clone(), "0.9")?; + tester.insert_raw_performance(&nms[2], node_id, measurement_kind.clone(), "0.1")?; + tester.insert_raw_performance(&nms[3], node_id, measurement_kind.clone(), "0.1")?; + tester.insert_raw_performance(&nms[4], node_id, measurement_kind.clone(), "0.2")?; + tester.insert_raw_performance(&nms[5], node_id, measurement_kind.clone(), "0.3")?; assert_eq!( storage .try_load_performance(&tester, 0, node_id)? + .get(&measurement_kind) .unwrap() .value() .to_string(), @@ -2417,17 +2758,21 @@ mod tests { let id1 = tester.bond_dummy_nymnode()?; let id2 = tester.bond_dummy_nymnode()?; - tester.insert_raw_performance(&nm, id1, "0.42")?; - tester.insert_raw_performance(&nm, id2, "0.42")?; + let measurement_kind = tester.define_dummy_measurement_kind().unwrap(); + + tester.insert_raw_performance(&nm, id1, measurement_kind.clone(), "0.42")?; + tester.insert_raw_performance(&nm, id2, measurement_kind, "0.42")?; let res = storage .remove_node_measurements(tester.deps_mut(), ¬_admin, epoch_id, id1) .unwrap_err(); assert_eq!(res, NymPerformanceContractError::Admin(NotAdmin {})); - assert!(storage - .remove_node_measurements(tester.deps_mut(), &admin, epoch_id, id1) - .is_ok()); + assert!( + storage + .remove_node_measurements(tester.deps_mut(), &admin, epoch_id, id1) + .is_ok() + ); // change admin let new_admin = tester.addr_make("new-admin"); @@ -2439,9 +2784,11 @@ mod tests { .unwrap_err(); assert_eq!(res, NymPerformanceContractError::Admin(NotAdmin {})); - assert!(storage - .remove_node_measurements(tester.deps_mut(), &new_admin, epoch_id, id2) - .is_ok()); + assert!( + storage + .remove_node_measurements(tester.deps_mut(), &new_admin, epoch_id, id2) + .is_ok() + ); Ok(()) } @@ -2486,41 +2833,50 @@ mod tests { storage.authorise_network_monitor(tester.deps_mut(), &env, &admin, nm2.clone())?; storage.authorise_network_monitor(tester.deps_mut(), &env, &admin, nm3.clone())?; + let measurement_kind = tester.define_dummy_measurement_kind().unwrap(); // single measurement - tester.insert_raw_performance(&nm1, id1, "0.42")?; + tester.insert_raw_performance(&nm1, id1, measurement_kind.clone(), "0.42")?; let before = storage .performance_results .results - .may_load(&tester, (epoch_id, id1))?; - assert!(before.is_some()); + .prefix((epoch_id, id1)) + .all_values(tester.storage()) + .unwrap(); + assert!(!before.is_empty()); storage.remove_node_measurements(tester.deps_mut(), &admin, epoch_id, id1)?; let after = storage .performance_results .results - .may_load(&tester, (epoch_id, id1))?; - assert!(after.is_none()); + .prefix((epoch_id, id1)) + .all_values(tester.storage()) + .unwrap(); + assert!(after.is_empty()); // multiple measurements - tester.insert_raw_performance(&nm1, id2, "0.42")?; - tester.insert_raw_performance(&nm2, id2, "0.69")?; - tester.insert_raw_performance(&nm3, id2, "1")?; + tester.insert_raw_performance(&nm1, id2, measurement_kind.clone(), "0.42")?; + tester.insert_raw_performance(&nm2, id2, measurement_kind.clone(), "0.69")?; + tester.insert_raw_performance(&nm3, id2, measurement_kind, "1")?; let before = storage .performance_results .results - .may_load(&tester, (epoch_id, id2))?; - assert!(before.is_some()); + .prefix((epoch_id, id2)) + .all_values(tester.storage()) + .unwrap(); + assert!(!before.is_empty()); storage.remove_node_measurements(tester.deps_mut(), &admin, epoch_id, id2)?; let after = storage .performance_results .results - .may_load(&tester, (epoch_id, id2))?; - assert!(after.is_none()); + .prefix((epoch_id, id2)) + .all_values(tester.storage()) + .unwrap(); + assert!(after.is_empty()); Ok(()) } @@ -2547,23 +2903,27 @@ mod tests { let id1 = tester.bond_dummy_nymnode()?; let id2 = tester.bond_dummy_nymnode()?; + let measurement_kind = tester.define_dummy_measurement_kind().unwrap(); + // epoch 0 - tester.insert_raw_performance(&nm, id1, "0.42")?; - tester.insert_raw_performance(&nm, id2, "0.42")?; + tester.insert_raw_performance(&nm, id1, measurement_kind.clone(), "0.42")?; + tester.insert_raw_performance(&nm, id2, measurement_kind.clone(), "0.42")?; // epoch 1 tester.advance_mixnet_epoch()?; - tester.insert_raw_performance(&nm, id1, "0.42")?; - tester.insert_raw_performance(&nm, id2, "0.42")?; + tester.insert_raw_performance(&nm, id1, measurement_kind.clone(), "0.42")?; + tester.insert_raw_performance(&nm, id2, measurement_kind, "0.42")?; let res = storage .remove_epoch_measurements(tester.deps_mut(), ¬_admin, 0) .unwrap_err(); assert_eq!(res, NymPerformanceContractError::Admin(NotAdmin {})); - assert!(storage - .remove_epoch_measurements(tester.deps_mut(), &admin, 0) - .is_ok()); + assert!( + storage + .remove_epoch_measurements(tester.deps_mut(), &admin, 0) + .is_ok() + ); // change admin let new_admin = tester.addr_make("new-admin"); @@ -2575,9 +2935,11 @@ mod tests { .unwrap_err(); assert_eq!(res, NymPerformanceContractError::Admin(NotAdmin {})); - assert!(storage - .remove_epoch_measurements(tester.deps_mut(), &new_admin, 1) - .is_ok()); + assert!( + storage + .remove_epoch_measurements(tester.deps_mut(), &new_admin, 1) + .is_ok() + ); Ok(()) } @@ -2614,15 +2976,21 @@ mod tests { // just few entries let epoch_id = 0; + let measurement_kind = tester.define_dummy_measurement_kind().unwrap(); for _ in 0..10 { let node_id = tester.bond_dummy_nymnode()?; - tester.insert_raw_performance(&nm, node_id, "0.42")?; + tester.insert_raw_performance( + &nm, + node_id, + measurement_kind.clone(), + "0.42", + )?; } let before = storage .performance_results .results - .prefix(epoch_id) + .sub_prefix(epoch_id) .all_values(&tester)?; assert_eq!(before.len(), 10); @@ -2631,7 +2999,7 @@ mod tests { let after = storage .performance_results .results - .prefix(epoch_id) + .sub_prefix(epoch_id) .all_values(&tester)?; assert!(after.is_empty()); @@ -2641,7 +3009,12 @@ mod tests { tester.advance_mixnet_epoch()?; for _ in 0..retrieval_limits::EPOCH_PERFORMANCE_PURGE_LIMIT { let node_id = tester.bond_dummy_nymnode()?; - tester.insert_raw_performance(&nm, node_id, "0.42")?; + tester.insert_raw_performance( + &nm, + node_id, + measurement_kind.clone(), + "0.42", + )?; } let res = storage.remove_epoch_measurements(tester.deps_mut(), &admin, epoch_id)?; @@ -2649,7 +3022,7 @@ mod tests { let after = storage .performance_results .results - .prefix(epoch_id) + .sub_prefix(epoch_id) .all_values(&tester)?; assert!(after.is_empty()); @@ -2670,15 +3043,21 @@ mod tests { // just few entries let epoch_id = 0; + let measurement_kind = tester.define_dummy_measurement_kind().unwrap(); for _ in 0..2 * retrieval_limits::EPOCH_PERFORMANCE_PURGE_LIMIT + 50 { let node_id = tester.bond_dummy_nymnode()?; - tester.insert_raw_performance(&nm, node_id, "0.42")?; + tester.insert_raw_performance( + &nm, + node_id, + measurement_kind.clone(), + "0.42", + )?; } let before = storage .performance_results .results - .prefix(epoch_id) + .sub_prefix(epoch_id) .all_values(&tester)?; assert_eq!( before.len(), @@ -2690,7 +3069,7 @@ mod tests { let after = storage .performance_results .results - .prefix(epoch_id) + .sub_prefix(epoch_id) .all_values(&tester)?; assert_eq!( @@ -2703,7 +3082,7 @@ mod tests { let after = storage .performance_results .results - .prefix(epoch_id) + .sub_prefix(epoch_id) .all_values(&tester)?; assert_eq!(after.len(), 50); @@ -2713,7 +3092,7 @@ mod tests { let after = storage .performance_results .results - .prefix(epoch_id) + .sub_prefix(epoch_id) .all_values(&tester)?; assert!(after.is_empty()); @@ -2726,7 +3105,7 @@ mod tests { #[cfg(test)] mod network_monitors_storage { use super::*; - use crate::testing::{init_contract_tester, PerformanceContractTesterExt}; + use crate::testing::{PerformanceContractTesterExt, init_contract_tester}; use nym_contracts_common_testing::{AdminExt, ContractOpts}; #[test] @@ -2741,9 +3120,11 @@ mod tests { let nm1 = tester.addr_make("network-monitor1"); let nm2 = tester.addr_make("network-monitor2"); - assert!(storage - .insert_new(tester.deps_mut(), &env, &admin, &nm1) - .is_ok()); + assert!( + storage + .insert_new(tester.deps_mut(), &env, &admin, &nm1) + .is_ok() + ); // total authorised count is incremented assert_eq!(storage.authorised_count.load(&tester)?, 1); @@ -2758,9 +3139,11 @@ mod tests { } ); - assert!(storage - .insert_new(tester.deps_mut(), &env, &admin, &nm2) - .is_ok()); + assert!( + storage + .insert_new(tester.deps_mut(), &env, &admin, &nm2) + .is_ok() + ); assert_eq!(storage.authorised_count.load(&tester)?, 2); assert_eq!( @@ -2781,9 +3164,11 @@ mod tests { assert!(storage.retired.may_load(&tester, &nm1)?.is_some()); // if it was previously retired, that information is purged - assert!(storage - .insert_new(tester.deps_mut(), &env, &admin, &nm1) - .is_ok()); + assert!( + storage + .insert_new(tester.deps_mut(), &env, &admin, &nm1) + .is_ok() + ); assert!(storage.retired.may_load(&tester, &nm1)?.is_none()); @@ -2805,9 +3190,11 @@ mod tests { tester.authorise_network_monitor(&nm2)?; // fails on unauthorised NMs - assert!(storage - .retire(tester.deps_mut(), &env, &admin, &nm3) - .is_err()); + assert!( + storage + .retire(tester.deps_mut(), &env, &admin, &nm3) + .is_err() + ); assert_eq!(storage.authorised_count.load(&tester)?, 2); @@ -2855,7 +3242,7 @@ mod tests { #[cfg(test)] mod performance_storage { use super::*; - use crate::testing::{init_contract_tester, PerformanceContractTesterExt}; + use crate::testing::{PerformanceContractTesterExt, init_contract_tester}; use nym_contracts_common_testing::ContractOpts; use std::str::FromStr; @@ -2868,66 +3255,90 @@ mod tests { let node_id1 = 123; let node_id2 = 456; + let measurement_kind = tester.define_dummy_measurement_kind().unwrap(); let data1 = NodePerformance { node_id: node_id1, performance: Percent::from_str("0.23")?, + measurement_kind: measurement_kind.clone(), }; let data2 = NodePerformance { node_id: node_id1, performance: Percent::hundred(), + measurement_kind: measurement_kind.clone(), }; let data3 = NodePerformance { node_id: node_id2, performance: Percent::from_str("0.23643634")?, + measurement_kind: measurement_kind.clone(), }; let data4 = NodePerformance { node_id: node_id2, performance: Percent::hundred(), + measurement_kind: measurement_kind.clone(), }; - assert!(storage.results.may_load(&tester, (1, node_id1))?.is_none()); - assert!(storage.results.may_load(&tester, (1, node_id2))?.is_none()); - + assert!( + storage + .results + .may_load(&tester, (1, node_id1, measurement_kind.clone()))? + .is_none() + ); + assert!( + storage + .results + .may_load(&tester, (1, node_id2, measurement_kind.clone()))? + .is_none() + ); storage.insert_performance_data(&mut tester, 1, &data1)?; assert_eq!( - tester.read_raw_scores(1, node_id1)?.inner(), + tester + .read_raw_scores(1, node_id1, measurement_kind.clone())? + .inner(), &[data1.performance] ); storage.insert_performance_data(&mut tester, 1, &data2)?; assert_eq!( - tester.read_raw_scores(1, node_id1)?.inner(), + tester + .read_raw_scores(1, node_id1, measurement_kind.clone())? + .inner(), &[data1.performance, data2.performance] ); - storage.insert_performance_data(&mut tester, 1, &data3)?; assert_eq!( - tester.read_raw_scores(1, node_id2)?.inner(), + tester + .read_raw_scores(1, node_id2, measurement_kind.clone())? + .inner(), &[data3.performance.round_to_two_decimal_places()] ); storage.insert_performance_data(&mut tester, 1, &data4)?; assert_eq!( - tester.read_raw_scores(1, node_id2)?.inner(), + tester + .read_raw_scores(1, node_id2, measurement_kind.clone())? + .inner(), &[ data3.performance.round_to_two_decimal_places(), data4.performance ] ); - storage.insert_performance_data(&mut tester, 2, &data2)?; storage.insert_performance_data(&mut tester, 2, &data2)?; assert_eq!( - tester.read_raw_scores(2, node_id1)?.inner(), + tester + .read_raw_scores(2, node_id1, measurement_kind.clone())? + .inner(), &[data2.performance, data2.performance] ); storage.insert_performance_data(&mut tester, 2, &data4)?; storage.insert_performance_data(&mut tester, 2, &data4)?; assert_eq!( - tester.read_raw_scores(2, node_id2)?.inner(), + tester + .read_raw_scores(2, node_id2, measurement_kind.clone())? + .inner(), &[data4.performance, data4.performance] ); @@ -2944,41 +3355,60 @@ mod tests { let id3 = tester.bond_dummy_nymnode()?; let nm = tester.addr_make("network-monitor"); + let measurement_kind = tester.define_dummy_measurement_kind().unwrap(); tester.authorise_network_monitor(&nm)?; - tester.insert_epoch_performance(&nm, 2, id2, Percent::hundred())?; + tester.insert_epoch_performance(&nm, 2, id2, measurement_kind, Percent::hundred())?; // illegal to submit anything < than last used epoch - assert!(storage - .ensure_non_stale_submission(&tester, &nm, 0, id2) - .is_err()); - assert!(storage - .ensure_non_stale_submission(&tester, &nm, 1, id2) - .is_err()); - assert!(storage - .ensure_non_stale_submission(&tester, &nm, 1, id3) - .is_err()); + assert!( + storage + .ensure_non_stale_submission(&tester, &nm, 0, id2) + .is_err() + ); + assert!( + storage + .ensure_non_stale_submission(&tester, &nm, 1, id2) + .is_err() + ); + assert!( + storage + .ensure_non_stale_submission(&tester, &nm, 1, id3) + .is_err() + ); // for the current epoch, node id has to be greater than what has already been submitted - assert!(storage - .ensure_non_stale_submission(&tester, &nm, 2, id1) - .is_err()); - assert!(storage - .ensure_non_stale_submission(&tester, &nm, 2, id2) - .is_err()); - assert!(storage - .ensure_non_stale_submission(&tester, &nm, 2, id3) - .is_ok()); + assert!( + storage + .ensure_non_stale_submission(&tester, &nm, 2, id1) + .is_err() + ); + assert!( + storage + .ensure_non_stale_submission(&tester, &nm, 2, id2) + .is_err() + ); + assert!( + storage + .ensure_non_stale_submission(&tester, &nm, 2, id3) + .is_ok() + ); // and anything for future epochs is fine (as long as it's the first entry) - assert!(storage - .ensure_non_stale_submission(&tester, &nm, 3, id1) - .is_ok()); - assert!(storage - .ensure_non_stale_submission(&tester, &nm, 3, id2) - .is_ok()); - assert!(storage - .ensure_non_stale_submission(&tester, &nm, 1111, id3) - .is_ok()); + assert!( + storage + .ensure_non_stale_submission(&tester, &nm, 3, id1) + .is_ok() + ); + assert!( + storage + .ensure_non_stale_submission(&tester, &nm, 3, id2) + .is_ok() + ); + assert!( + storage + .ensure_non_stale_submission(&tester, &nm, 1111, id3) + .is_ok() + ); Ok(()) } diff --git a/contracts/performance/src/testing/mod.rs b/contracts/performance/src/testing/mod.rs index 33e90beaf3d..8bc495dfacb 100644 --- a/contracts/performance/src/testing/mod.rs +++ b/contracts/performance/src/testing/mod.rs @@ -3,35 +3,35 @@ use crate::contract::{execute, instantiate, migrate, query}; use crate::helpers::MixnetContractQuerier; -use crate::storage::NYM_PERFORMANCE_CONTRACT_STORAGE; -use cosmwasm_std::testing::{message_info, mock_env, MockApi}; +use crate::storage::{MeasurementKind, NYM_PERFORMANCE_CONTRACT_STORAGE}; +use cosmwasm_std::testing::{MockApi, message_info, mock_env}; use cosmwasm_std::{ - coin, coins, Addr, ContractInfo, Deps, DepsMut, Env, MessageInfo, QuerierWrapper, StdError, - StdResult, + Addr, ContractInfo, Deps, DepsMut, Env, MessageInfo, QuerierWrapper, StdError, StdResult, coin, + coins, }; use mixnet_contract::testable_mixnet_contract::MixnetContract; -use nym_contracts_common::signing::{ContractMessageContent, MessageSignature}; use nym_contracts_common::Percent; +use nym_contracts_common::signing::{ContractMessageContent, MessageSignature}; use nym_contracts_common_testing::{ - addr, AdminExt, ArbitraryContractStorageReader, ArbitraryContractStorageWriter, BankExt, - ChainOpts, CommonStorageKeys, ContractFn, ContractOpts, ContractStorageWrapper, ContractTester, - ContractTesterBuilder, DenomExt, PermissionedFn, QueryFn, RandExt, TestableNymContract, - TEST_DENOM, + AdminExt, ArbitraryContractStorageReader, ArbitraryContractStorageWriter, BankExt, ChainOpts, + CommonStorageKeys, ContractFn, ContractOpts, ContractStorageWrapper, ContractTester, + ContractTesterBuilder, DenomExt, PermissionedFn, QueryFn, RandExt, TEST_DENOM, + TestableNymContract, addr, }; use nym_crypto::asymmetric::ed25519; use nym_mixnet_contract_common::nym_node::{NodeDetailsResponse, NodeOwnershipResponse, Role}; use nym_mixnet_contract_common::{ - CurrentIntervalResponse, EpochId, Interval, NodeCostParams, NymNode, NymNodeBondingPayload, - RoleAssignment, SignableNymNodeBondingMsg, DEFAULT_INTERVAL_OPERATING_COST_AMOUNT, - DEFAULT_PROFIT_MARGIN_PERCENT, + CurrentIntervalResponse, DEFAULT_INTERVAL_OPERATING_COST_AMOUNT, DEFAULT_PROFIT_MARGIN_PERCENT, + EpochId, Interval, NodeCostParams, NymNode, NymNodeBondingPayload, RoleAssignment, + SignableNymNodeBondingMsg, }; use nym_performance_contract_common::constants::storage_keys; use nym_performance_contract_common::{ - ExecuteMsg, InstantiateMsg, MigrateMsg, NodeId, NodePerformance, NodeResults, - NymPerformanceContractError, QueryMsg, + EpochNodePerformance, ExecuteMsg, InstantiateMsg, MigrateMsg, NodeId, NodePerformance, + NodeResults, NymPerformanceContractError, QueryMsg, }; -use serde::de::DeserializeOwned; use serde::Serialize; +use serde::de::DeserializeOwned; use std::str::FromStr; pub struct PerformanceContract; @@ -94,6 +94,20 @@ pub fn init_contract_tester() -> ContractTester { .with_common_storage_key(CommonStorageKeys::Admin, storage_keys::CONTRACT_ADMIN) } +#[cfg(test)] +// shorthand factory to avoid verbosity in tests +pub(crate) fn epoch_node_performance_unchecked( + epoch: EpochId, + measurement_kind: MeasurementKind, + performance: &str, +) -> EpochNodePerformance { + let performance = performance.parse().unwrap(); + EpochNodePerformance { + epoch, + performance: [(measurement_kind, performance)].into_iter().collect(), + } +} + // we need to be able to test instantiation, but for that we require // deps in a state that already includes instantiated mixnet contract pub(crate) struct PreInitContract { @@ -358,11 +372,33 @@ pub(crate) trait PerformanceContractTesterExt: Ok(()) } + fn dummy_measurement_kind(&mut self) -> MeasurementKind { + String::from("dummy") + } + + fn define_dummy_measurement_kind( + &mut self, + ) -> Result { + let admin = self.admin_unchecked(); + let measurement_kind = self.dummy_measurement_kind(); + + self.execute_raw( + admin, + ExecuteMsg::DefineMeasurementKind { + measurement_kind: measurement_kind.clone(), + }, + )?; + + Ok(measurement_kind) + } + fn dummy_node_performance(&mut self) -> NodePerformance { let node_id = self.bond_dummy_nymnode().unwrap(); + let measurement_kind = self.dummy_measurement_kind(); NodePerformance { node_id, performance: Percent::from_percentage_value(69).unwrap(), + measurement_kind, } } @@ -382,6 +418,7 @@ pub(crate) trait PerformanceContractTesterExt: addr: &Addr, epoch_id: EpochId, node_id: NodeId, + measurement_kind: MeasurementKind, performance: Percent, ) -> Result<(), NymPerformanceContractError> { let env = self.env(); @@ -393,6 +430,7 @@ pub(crate) trait PerformanceContractTesterExt: NodePerformance { node_id, performance, + measurement_kind, }, ) } @@ -401,11 +439,12 @@ pub(crate) trait PerformanceContractTesterExt: &mut self, addr: &Addr, node_id: NodeId, + measurement_kind: MeasurementKind, performance: Percent, ) -> Result<(), NymPerformanceContractError> { let epoch_id = self.current_mixnet_epoch()?; - self.insert_epoch_performance(addr, epoch_id, node_id, performance) + self.insert_epoch_performance(addr, epoch_id, node_id, measurement_kind, performance) } // makes testing easier @@ -413,11 +452,13 @@ pub(crate) trait PerformanceContractTesterExt: &mut self, addr: &Addr, node_id: NodeId, + measurement_kind: MeasurementKind, raw: &str, ) -> Result<(), NymPerformanceContractError> { self.insert_performance( addr, node_id, + measurement_kind, Percent::from_str(raw).map_err(|err| { NymPerformanceContractError::StdErr(StdError::parse_err("Percent", err.to_string())) })?, @@ -428,11 +469,12 @@ pub(crate) trait PerformanceContractTesterExt: &self, epoch_id: EpochId, node_id: NodeId, + measurement_kind: MeasurementKind, ) -> Result { let scores = NYM_PERFORMANCE_CONTRACT_STORAGE .performance_results .results - .load(self.deps().storage, (epoch_id, node_id))?; + .load(self.deps().storage, (epoch_id, node_id, measurement_kind))?; Ok(scores) } diff --git a/contracts/performance/src/transactions.rs b/contracts/performance/src/transactions.rs index 86a2d997721..d81d335c7ff 100644 --- a/contracts/performance/src/transactions.rs +++ b/contracts/performance/src/transactions.rs @@ -1,8 +1,8 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::storage::NYM_PERFORMANCE_CONTRACT_STORAGE; -use cosmwasm_std::{to_json_binary, DepsMut, Env, Event, MessageInfo, Response}; +use crate::storage::{MeasurementKind, NYM_PERFORMANCE_CONTRACT_STORAGE}; +use cosmwasm_std::{Addr, DepsMut, Env, Event, MessageInfo, Response, to_json_binary}; use nym_performance_contract_common::{ EpochId, NodeId, NodePerformance, NymPerformanceContractError, }; @@ -21,6 +21,50 @@ pub fn try_update_contract_admin( Ok(res) } +pub fn try_define_measurement_kind( + deps: DepsMut<'_>, + sender: &Addr, + measurement_kind: MeasurementKind, +) -> Result { + NYM_PERFORMANCE_CONTRACT_STORAGE + .contract_admin + .assert_admin(deps.as_ref(), sender)?; + + // validation + if measurement_kind.len() < 2 + || measurement_kind.len() > 20 + || !measurement_kind.is_ascii() + || measurement_kind.contains(char::is_whitespace) + { + return Err(NymPerformanceContractError::InvalidInput(format!( + "Cannot define {} as measurement kind", + measurement_kind + ))); + } + + NYM_PERFORMANCE_CONTRACT_STORAGE + .performance_results + .define_new_measurement_kind(deps.storage, measurement_kind)?; + + Ok(Response::new()) +} + +pub fn try_retire_measurement_kind( + deps: DepsMut<'_>, + sender_addr: &Addr, + measurement_kind: MeasurementKind, +) -> Result { + NYM_PERFORMANCE_CONTRACT_STORAGE + .contract_admin + .assert_admin(deps.as_ref(), sender_addr)?; + + NYM_PERFORMANCE_CONTRACT_STORAGE + .performance_results + .retire_measurement_kind(deps.storage, measurement_kind)?; + + Ok(Response::new()) +} + pub fn try_submit_performance_results( deps: DepsMut<'_>, env: Env, @@ -61,6 +105,10 @@ pub fn try_batch_submit_performance_results( .add_attribute( "non_existent_nodes", format!("{:?}", res.non_existent_nodes), + ) + .add_attribute( + "non_existent_measurement_kinds", + format!("{:?}", res.non_existent_measurement_kind), ), ); Ok(response) @@ -130,7 +178,7 @@ pub fn try_remove_epoch_measurements( mod tests { use super::*; use crate::storage::retrieval_limits; - use crate::testing::{init_contract_tester, PerformanceContractTesterExt}; + use crate::testing::{PerformanceContractTesterExt, init_contract_tester}; use cosmwasm_std::from_json; use nym_contracts_common_testing::{AdminExt, ContractOpts}; use nym_performance_contract_common::RemoveEpochMeasurementsResponse; @@ -222,20 +270,24 @@ mod tests { let env = test.env(); let admin = test.admin_msg(); - assert!(try_authorise_network_monitor( - test.deps_mut(), - env.clone(), - admin.clone(), - bad_address - ) - .is_err()); - assert!(try_authorise_network_monitor( - test.deps_mut(), - env, - admin, - good_address.to_string() - ) - .is_ok()); + assert!( + try_authorise_network_monitor( + test.deps_mut(), + env.clone(), + admin.clone(), + bad_address + ) + .is_err() + ); + assert!( + try_authorise_network_monitor( + test.deps_mut(), + env, + admin, + good_address.to_string() + ) + .is_ok() + ); Ok(()) } @@ -244,7 +296,7 @@ mod tests { #[cfg(test)] mod retiring_network_monitor { use super::*; - use crate::testing::{init_contract_tester, PerformanceContractTesterExt}; + use crate::testing::{PerformanceContractTesterExt, init_contract_tester}; use nym_contracts_common_testing::{AdminExt, ContractOpts, RandExt}; #[test] @@ -258,20 +310,19 @@ mod tests { let env = test.env(); let admin = test.admin_msg(); - assert!(try_retire_network_monitor( - test.deps_mut(), - env.clone(), - admin.clone(), - bad_address - ) - .is_err()); - assert!(try_retire_network_monitor( - test.deps_mut(), - env, - admin, - good_address.to_string() - ) - .is_ok()); + assert!( + try_retire_network_monitor( + test.deps_mut(), + env.clone(), + admin.clone(), + bad_address + ) + .is_err() + ); + assert!( + try_retire_network_monitor(test.deps_mut(), env, admin, good_address.to_string()) + .is_ok() + ); Ok(()) } @@ -285,11 +336,14 @@ mod tests { let nm = tester.addr_make("network-monitor"); tester.authorise_network_monitor(&nm)?; + tester.define_dummy_measurement_kind()?; tester.advance_mixnet_epoch()?; + + let measurement_kind = tester.dummy_measurement_kind(); for _ in 0..2 * retrieval_limits::EPOCH_PERFORMANCE_PURGE_LIMIT { let node_id = tester.bond_dummy_nymnode()?; - tester.insert_raw_performance(&nm, node_id, "0.42")?; + tester.insert_raw_performance(&nm, node_id, measurement_kind.clone(), "0.42")?; } let admin = tester.admin_msg(); @@ -311,4 +365,200 @@ mod tests { Ok(()) } + + mod measurement_kind_authorization { + use cosmwasm_std::testing::message_info; + use nym_contracts_common_testing::{AdminExt, ContractOpts}; + use nym_performance_contract_common::NymPerformanceContractError; + + use crate::{ + storage::MeasurementKind, + testing::{PerformanceContractTesterExt, init_contract_tester}, + transactions::{ + try_define_measurement_kind, try_retire_measurement_kind, + try_submit_performance_results, + }, + }; + + #[allow(clippy::panic)] + #[test] + fn add_requires_admin() { + let mut tester = init_contract_tester(); + let admin = tester.admin_msg(); + let new_measurement = MeasurementKind::from("new-measurement"); + + assert!( + try_define_measurement_kind( + tester.deps_mut(), + &admin.sender, + new_measurement.clone() + ) + .is_ok() + ); + } + + #[allow(clippy::panic)] + #[test] + fn retire_requires_admin() { + let mut tester = init_contract_tester(); + let admin = tester.admin_msg(); + let new_measurement = MeasurementKind::from("new-measurement"); + + try_define_measurement_kind(tester.deps_mut(), &admin.sender, new_measurement.clone()) + .unwrap(); + + let unauthorized_addr = tester.addr_make("unauthorized-addr"); + let unauthorized = try_retire_measurement_kind( + tester.deps_mut(), + &unauthorized_addr, + new_measurement.clone(), + ); + assert!(matches!( + unauthorized, + Err(NymPerformanceContractError::Admin { .. }) + )); + + let authorized = try_retire_measurement_kind( + tester.deps_mut(), + &admin.sender, + new_measurement.clone(), + ); + assert!(matches!(authorized, Ok(..))); + } + + #[allow(clippy::panic)] + #[test] + fn cannot_add_existing() { + let mut tester = init_contract_tester(); + let admin = tester.admin_msg(); + let new_measurement = MeasurementKind::from("new-measurement"); + + let first_attempt = try_define_measurement_kind( + tester.deps_mut(), + &admin.sender, + new_measurement.clone(), + ); + assert!(matches!(first_attempt, Ok(..))); + + let second_attempt = + try_define_measurement_kind(tester.deps_mut(), &admin.sender, new_measurement); + assert!(matches!( + second_attempt, + Err(NymPerformanceContractError::InvalidInput(_)) + )); + } + + #[allow(clippy::panic)] + #[test] + fn cannot_retire_nonexistent() { + let mut tester = init_contract_tester(); + let admin = tester.admin_msg(); + let nonexistent = MeasurementKind::from("nonexistent"); + + let err = try_retire_measurement_kind(tester.deps_mut(), &admin.sender, nonexistent); + + assert!(matches!( + err, + Err(NymPerformanceContractError::InvalidInput(_)) + )); + } + + #[allow(clippy::panic)] + #[test] + fn cannot_submit_undefined() { + let mut tester = init_contract_tester(); + let env = tester.env(); + let admin = tester.admin_msg(); + let dummy_perf = tester.dummy_node_performance(); + let nm = tester.addr_make("network-monitor"); + tester.authorise_network_monitor(&nm).unwrap(); + + let dummy_measurement = dummy_perf.measurement_kind.clone(); + + let first_attempt = try_submit_performance_results( + tester.deps_mut(), + env.clone(), + // network monitor submits + message_info(&nm, &[]), + 0, + dummy_perf.clone(), + ); + assert!(matches!( + first_attempt, + Err(NymPerformanceContractError::UnsupportedMeasurementKind { .. }) + )); + + try_define_measurement_kind( + tester.deps_mut(), + // admin defines + &admin.sender, + dummy_measurement.clone(), + ) + .unwrap(); + let second_attempt = try_submit_performance_results( + tester.deps_mut(), + env, + // network monitor submits + message_info(&nm, &[]), + 0, + dummy_perf, + ); + assert!(matches!(second_attempt, Ok(..))); + } + + #[allow(clippy::panic)] + #[test] + fn cannot_submit_retired() { + let mut tester = init_contract_tester(); + let env = tester.env(); + let admin = tester.admin_msg(); + let dummy_perf = tester.dummy_node_performance(); + let nm = tester.addr_make("network-monitor"); + tester.authorise_network_monitor(&nm).unwrap(); + + let dummy_measurement = dummy_perf.measurement_kind.clone(); + + try_define_measurement_kind( + tester.deps_mut(), + // admin defines + &admin.sender, + dummy_measurement.clone(), + ) + .unwrap(); + let defined_ok = try_submit_performance_results( + tester.deps_mut(), + env.clone(), + // network monitor submits + message_info(&nm, &[]), + 0, + dummy_perf.clone(), + ); + assert!(matches!(defined_ok, Ok(..))); + + // can't submit for the same node in the same epoch again + tester.advance_mixnet_epoch().unwrap(); + + try_retire_measurement_kind( + tester.deps_mut(), + // admin defines + &admin.sender, + dummy_measurement.clone(), + ) + .unwrap(); + + let retired_err = try_submit_performance_results( + tester.deps_mut(), + env, + // network monitor submits + message_info(&nm, &[]), + 1, + dummy_perf, + ); + println!("{:#?}", retired_err); + assert!(matches!( + retired_err, + Err(NymPerformanceContractError::UnsupportedMeasurementKind { .. }) + )); + } + } }