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 7d85e7a668..268e591be8 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,8 +14,8 @@ pub use nym_performance_contract_common::{ EpochMeasurementsPagedResponse, EpochNodePerformance, EpochPerformancePagedResponse, FullHistoricalPerformancePagedResponse, HistoricalPerformance, LastSubmission, NetworkMonitorInformation, NetworkMonitorsPagedResponse, NodeId, NodeMeasurement, - NodeMeasurementsResponse, NodePerformancePagedResponse, NodePerformanceResponse, - NodePerformanceSpecific, RetiredNetworkMonitor, RetiredNetworkMonitorsPagedResponse, + NodeMeasurementsPerKindResponse, NodePerformance, NodePerformancePagedResponse, + NodePerformanceResponse, RetiredNetworkMonitor, RetiredNetworkMonitorsPagedResponse, }; #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] @@ -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 } @@ -167,7 +167,7 @@ pub trait PagedPerformanceQueryClient: PerformanceQueryClient { async fn get_all_epoch_performance( &self, epoch_id: EpochId, - ) -> Result, NyxdError> { + ) -> Result, NyxdError> { collect_paged!(self, get_epoch_performance_paged, performance, epoch_id) } diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/performance_signing_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/performance_signing_client.rs index cfc7f1f2cc..d0380c0452 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/performance_signing_client.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/performance_signing_client.rs @@ -10,7 +10,7 @@ use crate::nyxd::{Fee, SigningCosmWasmClient}; use crate::signing::signer::OfflineSigner; use async_trait::async_trait; use nym_performance_contract_common::{ - EpochId, ExecuteMsg as PerformanceExecuteMsg, NodeId, NodePerformanceSpecific, + EpochId, ExecuteMsg as PerformanceExecuteMsg, NodeId, NodePerformance, RemoveEpochMeasurementsResponse, }; @@ -42,7 +42,7 @@ pub trait PerformanceSigningClient { async fn submit_performance( &self, epoch: EpochId, - data: NodePerformanceSpecific, + data: NodePerformance, fee: Option, ) -> Result { self.execute_performance_contract( @@ -57,7 +57,7 @@ pub trait PerformanceSigningClient { async fn batch_submit_performance( &self, epoch: EpochId, - data: Vec, + data: Vec, fee: Option, ) -> Result { self.execute_performance_contract( 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 7165252c44..bbff2a5b21 100644 --- a/common/cosmwasm-smart-contracts/nym-performance-contract/src/constants.rs +++ b/common/cosmwasm-smart-contracts/nym-performance-contract/src/constants.rs @@ -9,7 +9,6 @@ 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"; 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 3b773b817f..ae85c1fe5b 100644 --- a/common/cosmwasm-smart-contracts/nym-performance-contract/src/msg.rs +++ b/common/cosmwasm-smart-contracts/nym-performance-contract/src/msg.rs @@ -1,14 +1,14 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::{EpochId, NodeId, NodePerformanceSpecific}; +use crate::{EpochId, NodeId, NodePerformance}; use cosmwasm_schema::cw_serde; #[cfg(feature = "schema")] use crate::types::{ EpochMeasurementsPagedResponse, EpochPerformancePagedResponse, FullHistoricalPerformancePagedResponse, LastSubmission, NetworkMonitorResponse, - NetworkMonitorsPagedResponse, NodeMeasurementsResponse, NodePerformancePagedResponse, + NetworkMonitorsPagedResponse, NodeMeasurementsPerKindResponse, NodePerformancePagedResponse, NodePerformanceResponse, RetiredNetworkMonitorsPagedResponse, }; @@ -23,17 +23,16 @@ pub enum ExecuteMsg { /// Change the admin UpdateAdmin { admin: String }, - // TODO dz rename to remove speicifc suffix /// Attempt to submit performance data of a particular node for given epoch - SubmitSpecific { + Submit { epoch: EpochId, - data: NodePerformanceSpecific, + data: NodePerformance, }, /// Attempt to submit performance data of a batch of nodes for given epoch BatchSubmit { epoch: EpochId, - data: Vec, + data: Vec, }, /// Measurement kind needs to be defined by the admin before measurements of @@ -78,11 +77,9 @@ 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 }, - - NodeMeasurementsSpecific { + NodeMeasurements { epoch_id: EpochId, node_id: NodeId, kind: String, 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 45b133ffc5..769dcc1506 100644 --- a/common/cosmwasm-smart-contracts/nym-performance-contract/src/types.rs +++ b/common/cosmwasm-smart-contracts/nym-performance-contract/src/types.rs @@ -23,7 +23,7 @@ pub struct LastSubmission { pub struct LastSubmittedData { pub sender: Addr, pub epoch_id: EpochId, - pub data: NodePerformanceSpecific, + pub data: NodePerformance, } #[cw_serde] @@ -50,14 +50,13 @@ pub struct RetiredNetworkMonitor { pub retired_at_height: u64, } -// TODO dz remove specific from name #[cw_serde] -pub struct NodePerformanceSpecific { +pub struct NodePerformance { #[serde(rename = "n")] pub node_id: NodeId, #[serde(rename = "m")] - pub measurement_kind: String, + pub measurement_kind: MeasurementKind, // note: value is rounded to 2 decimal places. #[serde(rename = "p")] @@ -106,13 +105,17 @@ impl NodeResults { } } +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, } @@ -120,14 +123,13 @@ pub struct NodeMeasurementsResponse { 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>, + pub measurements: HashMap>, } #[cw_serde] -#[derive(Copy)] pub struct EpochNodePerformance { pub epoch: EpochId, - pub performance: Option, + pub performance: HashMap, } #[cw_serde] @@ -140,20 +142,20 @@ pub struct NodePerformancePagedResponse { #[cw_serde] pub struct EpochPerformancePagedResponse { pub epoch_id: EpochId, - pub performance: Vec, + pub performance: Vec, pub start_next_after: Option, } #[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, } diff --git a/contracts/performance/src/contract.rs b/contracts/performance/src/contract.rs index 762464d8e0..e7fdccf774 100644 --- a/contracts/performance/src/contract.rs +++ b/contracts/performance/src/contract.rs @@ -4,9 +4,8 @@ use crate::queries::{ 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, - query_node_measurements_specific, query_node_performance, query_node_performance_paged, - query_retired_network_monitors_paged, + 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::{ @@ -58,7 +57,7 @@ pub fn execute( ) -> Result { match msg { ExecuteMsg::UpdateAdmin { admin } => try_update_contract_admin(deps, info, admin), - ExecuteMsg::SubmitSpecific { epoch, data } => { + ExecuteMsg::Submit { epoch, data } => { try_submit_performance_results(deps, env, info, epoch, data) } ExecuteMsg::BatchSubmit { epoch, data } => { @@ -126,18 +125,12 @@ 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::NodeMeasurementsSpecific { + QueryMsg::NodeMeasurements { epoch_id, node_id, - kind: measurement_kind, - } => Ok(to_json_binary(&query_node_measurements_specific( - deps, - epoch_id, - node_id, - measurement_kind, + 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)?, diff --git a/contracts/performance/src/queries.rs b/contracts/performance/src/queries.rs index 431e266585..e2f3280f20 100644 --- a/contracts/performance/src/queries.rs +++ b/contracts/performance/src/queries.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; -use crate::storage::{NYM_PERFORMANCE_CONTRACT_STORAGE, retrieval_limits}; +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; @@ -11,8 +11,8 @@ use nym_performance_contract_common::{ AllNodeMeasurementsResponse, EpochId, EpochMeasurementsPagedResponse, EpochNodePerformance, EpochPerformancePagedResponse, FullHistoricalPerformancePagedResponse, HistoricalPerformance, LastSubmission, NetworkMonitorInformation, NetworkMonitorResponse, - NetworkMonitorsPagedResponse, NodeId, NodeMeasurement, NodeMeasurementsResponse, - NodePerformancePagedResponse, NodePerformanceResponse, NodePerformanceSpecific, + NetworkMonitorsPagedResponse, NodeId, NodeMeasurements, NodeMeasurementsPerKindResponse, + NodePerformance, NodePerformancePagedResponse, NodePerformanceResponse, NodeResults, NymPerformanceContractError, RetiredNetworkMonitorsPagedResponse, }; @@ -23,7 +23,6 @@ pub fn query_admin(deps: Deps) -> Result Result { - let measurements = NYM_PERFORMANCE_CONTRACT_STORAGE - .performance_results - .results - .may_load(deps.storage, (epoch_id, node_id))?; - Ok(NodeMeasurementsResponse { measurements }) -} - -// TODO dz unit test -pub fn query_node_measurements_specific( +pub fn query_node_measurements_for_kind( deps: Deps, epoch_id: EpochId, node_id: NodeId, measurement_kind: String, -) -> Result { - let measurements = NYM_PERFORMANCE_CONTRACT_STORAGE - .performance_results - .results_specific; +) -> Result { + let measurements = NYM_PERFORMANCE_CONTRACT_STORAGE.try_load_measurement_kind( + deps.storage, + epoch_id, + node_id, + measurement_kind, + )?; - let key = (epoch_id, node_id, measurement_kind); - let measurements = measurements.may_load(deps.storage, key)?; - - Ok(NodeMeasurementsResponse { measurements }) + Ok(NodeMeasurementsPerKindResponse { measurements }) } -// TODO dz unit test pub fn query_all_node_measurements( deps: Deps, epoch_id: EpochId, node_id: NodeId, ) -> Result { - let measurements = NYM_PERFORMANCE_CONTRACT_STORAGE - .performance_results - .results_specific; + 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 - @@ -146,7 +129,6 @@ pub fn query_node_performance_paged( }) } -// TODO dz unit test changes pub fn query_epoch_performance_paged( deps: Deps, epoch_id: EpochId, @@ -161,18 +143,16 @@ pub fn query_epoch_performance_paged( let performance = NYM_PERFORMANCE_CONTRACT_STORAGE .performance_results - .results_specific + .results .sub_prefix(epoch_id) .range(deps.storage, start, None, Order::Ascending) .take(limit) .map(|record| { - record.map( - |((node_id, measurement_kind), results)| NodePerformanceSpecific { - node_id, - performance: results.median(), - measurement_kind, - }, - ) + record.map(|((node_id, measurement_kind), results)| NodePerformance { + node_id, + performance: results.median(), + measurement_kind, + }) }) .collect::>>()?; @@ -195,27 +175,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, }) } @@ -229,7 +233,7 @@ 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); + let start = start_after.map(|(n, e)| Bound::exclusive((n, e, String::new()))); let performance = NYM_PERFORMANCE_CONTRACT_STORAGE .performance_results @@ -237,7 +241,8 @@ pub fn query_full_historical_performance_paged( .range(deps.storage, start, None, Order::Ascending) .take(limit) .map(|record| { - record.map(|((epoch_id, node_id), results)| HistoricalPerformance { + record.map(|((epoch_id, node_id, _), results)| HistoricalPerformance { + // TODO dz map kind as well epoch_id, node_id, performance: results.median(), @@ -368,9 +373,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_id = 10; + test.set_mixnet_epoch(epoch_id)?; + + let deps = test.deps(); + + // ===== Test: undefined measurement kind ===== + let undefined_kind = String::from("undefined"); + let res = query_node_measurements_for_kind(deps, epoch_id, 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_id, 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_id, 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_id, 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_id, 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_11 = 11; + test.set_mixnet_epoch(epoch_11)?; + + // 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_11, 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_11, 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_id, 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_12 = 12; + test.set_mixnet_epoch(epoch_12)?; + test.insert_raw_performance(&nm1, node1, kind_mixnet.clone(), "0.25")?; + + // epoch 12 should have new data + let res_epoch12 = + query_node_measurements_for_kind(test.deps(), epoch_12, 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 10 should still have old data + let res_epoch10 = + query_node_measurements_for_kind(test.deps(), epoch_id, node1, kind_mixnet.clone())?; + assert_eq!(res_epoch10.measurements.unwrap().inner().len(), 2); + + // epoch 11 with mixnet (no data) should return None + let res_epoch11_mixnet = + query_node_measurements_for_kind(test.deps(), epoch_11, node1, kind_mixnet.clone())?; + assert!(res_epoch11_mixnet.measurements.is_none()); + + // ===== Test: different nodes are independent ===== + // nm1 can now submit for node2 in epoch 12 since node2 > node1 + test.insert_raw_performance(&nm1, node2, kind_mixnet.clone(), "0.42")?; + + // Query node1 in epoch 12 - should have nm1's data + let res_node1_e12 = + query_node_measurements_for_kind(test.deps(), epoch_12, node1, kind_mixnet.clone())?; + assert_eq!(res_node1_e12.measurements.unwrap().inner().len(), 1); + + // Query node2 in epoch 12 - should have different data + let res_node2_e12 = + query_node_measurements_for_kind(test.deps(), epoch_12, 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 10 (no data) - should return None + let res_node2_e10 = + query_node_measurements_for_kind(test.deps(), epoch_id, node2, kind_mixnet.clone())?; + assert!(res_node2_e10.measurements.is_none()); + + // verify against raw data + let raw_scores = test.read_raw_scores(epoch_id, node1, kind_mixnet.clone())?; + let query_result = + query_node_measurements_for_kind(test.deps(), epoch_id, 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(); @@ -454,10 +615,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)?; @@ -465,18 +627,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"), ] ); @@ -485,30 +638,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"), ] ); @@ -516,10 +651,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(()) @@ -562,12 +698,12 @@ mod tests { assert_eq!( res.performance, vec![ - NodePerformanceSpecific { + NodePerformance { node_id: nodes[5], performance: "0.5".parse()?, measurement_kind: measurement_kind.clone() }, - NodePerformanceSpecific { + NodePerformance { node_id: nodes[6], performance: "0.6".parse()?, measurement_kind: measurement_kind.clone() @@ -579,12 +715,12 @@ mod tests { assert_eq!( res.performance, vec![ - NodePerformanceSpecific { + NodePerformance { node_id: nodes[5], performance: "0.5".parse()?, measurement_kind: measurement_kind.clone() }, - NodePerformanceSpecific { + NodePerformance { node_id: nodes[6], performance: "0.6".parse()?, measurement_kind: measurement_kind.clone() @@ -597,17 +733,17 @@ mod tests { assert_eq!( res.performance, vec![ - NodePerformanceSpecific { + NodePerformance { node_id: nodes[3], performance: "0.3".parse()?, measurement_kind: measurement_kind.clone() }, - NodePerformanceSpecific { + NodePerformance { node_id: nodes[5], performance: "0.5".parse()?, measurement_kind: measurement_kind.clone() }, - NodePerformanceSpecific { + NodePerformance { node_id: nodes[6], performance: "0.6".parse()?, measurement_kind: measurement_kind.clone() @@ -620,27 +756,27 @@ mod tests { assert_eq!( res.performance, vec![ - NodePerformanceSpecific { + NodePerformance { node_id: nodes[1], performance: "0.1".parse()?, measurement_kind: measurement_kind.clone() }, - NodePerformanceSpecific { + NodePerformance { node_id: nodes[2], performance: "0.2".parse()?, measurement_kind: measurement_kind.clone() }, - NodePerformanceSpecific { + NodePerformance { node_id: nodes[3], performance: "0.3".parse()?, measurement_kind: measurement_kind.clone() }, - NodePerformanceSpecific { + NodePerformance { node_id: nodes[5], performance: "0.5".parse()?, measurement_kind: measurement_kind.clone() }, - NodePerformanceSpecific { + NodePerformance { node_id: nodes[6], performance: "0.6".parse()?, measurement_kind: measurement_kind.clone() @@ -652,7 +788,7 @@ mod tests { assert_eq!(res.start_next_after, Some(nodes[3])); assert_eq!( res.performance, - vec![NodePerformanceSpecific { + vec![NodePerformance { node_id: nodes[3], performance: "0.3".parse()?, measurement_kind: measurement_kind.clone() @@ -662,6 +798,270 @@ mod tests { 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(); @@ -700,7 +1100,7 @@ mod tests { data: Some(LastSubmittedData { sender: nm1.clone(), epoch_id: 10, - data: NodePerformanceSpecific { + data: NodePerformance { node_id: id1, performance: "0.2".parse()?, measurement_kind: measurement_kind.clone(), @@ -724,7 +1124,7 @@ mod tests { data: Some(LastSubmittedData { sender: nm2.clone(), epoch_id: 5, - data: NodePerformanceSpecific { + data: NodePerformance { node_id: id2, performance: "0.3".parse()?, measurement_kind: measurement_kind.clone(), @@ -735,4 +1135,103 @@ mod tests { 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(()) + } } diff --git a/contracts/performance/src/storage.rs b/contracts/performance/src/storage.rs index a0cbb3a8bb..79ee432906 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; @@ -9,7 +11,7 @@ use nym_contracts_common::Percent; use nym_performance_contract_common::constants::storage_keys; use nym_performance_contract_common::{ BatchSubmissionResult, EpochId, LastSubmission, LastSubmittedData, NetworkMonitorDetails, - NetworkMonitorSubmissionMetadata, NodeId, NodePerformanceSpecific, NodeResults, + NetworkMonitorSubmissionMetadata, NodeId, NodePerformance, NodeResults, NymPerformanceContractError, RemoveEpochMeasurementsResponse, RetiredNetworkMonitor, }; @@ -115,7 +117,7 @@ impl NymPerformanceContractStorage { env: Env, sender: &Addr, epoch_id: EpochId, - data: NodePerformanceSpecific, + data: NodePerformance, ) -> Result<(), NymPerformanceContractError> { // 1. check if the sender is authorised to submit performance data self.network_monitors @@ -138,7 +140,7 @@ impl NymPerformanceContractStorage { // 4 insert performance data into the storage self.performance_results - .insert_performance_data_specific(deps.storage, epoch_id, &data)?; + .insert_performance_data(deps.storage, epoch_id, &data)?; // 5. update submission metadata based on the last result we submitted self.performance_results.update_submission_metadata( @@ -171,7 +173,7 @@ impl NymPerformanceContractStorage { env: Env, sender: &Addr, epoch_id: EpochId, - data: Vec, + data: Vec, ) -> Result { // 1. check if the sender is authorised to submit performance data self.network_monitors @@ -196,11 +198,10 @@ impl NymPerformanceContractStorage { // 3. submit it if self.node_bonded(deps.as_ref(), first.node_id)? { - match self.performance_results.insert_performance_data_specific( - deps.storage, - epoch_id, - first, - ) { + 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); @@ -226,11 +227,8 @@ impl NymPerformanceContractStorage { // 5. insert performance data into the storage if self.node_bonded(deps.as_ref(), perf.node_id)? { - self.performance_results.insert_performance_data_specific( - deps.storage, - epoch_id, - perf, - )?; + self.performance_results + .insert_performance_data(deps.storage, epoch_id, perf)?; accepted_scores += 1; } else { non_existent_nodes.push(perf.node_id); @@ -332,45 +330,51 @@ impl NymPerformanceContractStorage { .retire(deps, &env, sender, &network_monitor) } - // TODO dz needs to support measurement_kind + pub fn try_load_measurement_kind( + &self, + storage: &dyn Storage, + epoch_id: EpochId, + node_id: NodeId, + 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, key) + .map_err(From::from) + } + pub fn try_load_performance( &self, storage: &dyn Storage, epoch_id: EpochId, node_id: NodeId, - ) -> Result, NymPerformanceContractError> { + ) -> 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(None); + return Ok(HashMap::new()); } - // Collect median values from all measurement kinds - let mut medians = Vec::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_specific - .may_load(storage, (epoch_id, node_id, kind))? + .results + .may_load(storage, (epoch_id, node_id, kind.clone()))? { - medians.push(results.median()); + performance_per_kind.insert(kind, results.median()); } } - // If no data found for any measurement kind, return None - if medians.is_empty() { - return Ok(None); - } - - // Build NodeResults from collected medians and return the median of medians - let mut aggregated = NodeResults::new(medians[0]); - for &median in &medians[1..] { - aggregated.insert_new(median); - } - - Ok(Some(aggregated.median())) + Ok(performance_per_kind) } pub fn remove_node_measurements( @@ -388,7 +392,7 @@ impl NymPerformanceContractStorage { .defined_measurements(deps.storage)?; for kind in measurement_kinds { self.performance_results - .results_specific + .results .remove(deps.storage, (epoch_id, node_id, kind)); } @@ -404,18 +408,15 @@ impl NymPerformanceContractStorage { self.ensure_is_admin(deps.as_ref(), sender)?; // 1. purge the entries according to the limit - self.performance_results - .results_specific - .sub_prefix(epoch_id) - .clear( - deps.storage, - Some(retrieval_limits::EPOCH_PERFORMANCE_PURGE_LIMIT), - ); + self.performance_results.results.sub_prefix(epoch_id).clear( + deps.storage, + Some(retrieval_limits::EPOCH_PERFORMANCE_PURGE_LIMIT), + ); // 2. see if there's anything left let additional_entries_to_remove_remaining = !self .performance_results - .results_specific + .results .sub_prefix(epoch_id) .is_empty(deps.storage); @@ -509,9 +510,7 @@ impl NetworkMonitorsStorage { } pub(crate) struct PerformanceResultsStorage { - pub(crate) results: Map<(EpochId, NodeId), NodeResults>, - - pub(crate) results_specific: Map<(EpochId, NodeId, MeasurementKind), NodeResults>, + pub(crate) results: Map<(EpochId, NodeId, MeasurementKind), NodeResults>, /// only measurements defined here can be submitted, as defined by contract admin defined_measurements: Map, @@ -529,8 +528,7 @@ impl PerformanceResultsStorage { #[allow(clippy::new_without_default)] const fn new() -> Self { PerformanceResultsStorage { - results: Map::new(storage_keys::PERFORMANCE_RESULTS), - results_specific: Map::new(storage_keys::PERFORMANCE_RESULTS_PER_KIND), + 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), } @@ -542,12 +540,14 @@ impl PerformanceResultsStorage { &self, storage: &mut dyn Storage, epoch_id: EpochId, - data: &NodePerformanceSpecific, + 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); @@ -559,29 +559,6 @@ impl PerformanceResultsStorage { Ok(()) } - fn insert_performance_data_specific( - &self, - storage: &mut dyn Storage, - epoch_id: EpochId, - data: &NodePerformanceSpecific, - ) -> Result<(), NymPerformanceContractError> { - self.assert_measurement_defined(storage, data.measurement_kind.clone())?; - - let performance = data.performance; - - let key = (epoch_id, data.node_id, data.measurement_kind.clone()); - let updated = match self.results_specific.may_load(storage, key.clone())? { - None => NodeResults::new(performance), - Some(mut existing) => { - existing.insert_new(performance); - existing - } - }; - - self.results_specific.save(storage, key, &updated)?; - Ok(()) - } - pub(crate) fn defined_measurements( &self, storage: &dyn Storage, @@ -594,7 +571,7 @@ impl PerformanceResultsStorage { fn is_measurement_defined( &self, - storage: &mut dyn Storage, + storage: &dyn Storage, measurement_kind: MeasurementKind, ) -> Result { self.defined_measurements @@ -604,7 +581,7 @@ impl PerformanceResultsStorage { fn assert_measurement_defined( &self, - storage: &mut dyn Storage, + storage: &dyn Storage, measurement_kind: MeasurementKind, ) -> Result<(), NymPerformanceContractError> { if !self.is_measurement_defined(storage, measurement_kind.clone())? { @@ -670,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, @@ -1015,12 +997,12 @@ mod tests { let id1 = tester.bond_dummy_nymnode()?; let id2 = tester.bond_dummy_nymnode()?; - let data = NodePerformanceSpecific { + let data = NodePerformance { node_id: id1, performance: Percent::hundred(), measurement_kind: measurement_kind.clone(), }; - let another_data = NodePerformanceSpecific { + let another_data = NodePerformance { node_id: id2, performance: Percent::hundred(), measurement_kind, @@ -1110,12 +1092,12 @@ mod tests { let measurement_kind = tester.define_dummy_measurement_kind().unwrap(); - let data = NodePerformanceSpecific { + let data = NodePerformance { node_id: id1, performance: Percent::hundred(), measurement_kind: measurement_kind.clone(), }; - let another_data = NodePerformanceSpecific { + let another_data = NodePerformance { node_id: id2, performance: Percent::hundred(), measurement_kind, @@ -1270,7 +1252,7 @@ mod tests { env.clone(), &nm, 0, - NodePerformanceSpecific { + NodePerformance { node_id: nodes[0], performance: Default::default(), measurement_kind: measurement_kind.clone(), @@ -1288,7 +1270,7 @@ mod tests { env.clone(), &nm, 0, - NodePerformanceSpecific { + NodePerformance { node_id: nodes[3], performance: Default::default(), measurement_kind: measurement_kind.clone(), @@ -1306,7 +1288,7 @@ mod tests { env.clone(), &nm, 1, - NodePerformanceSpecific { + NodePerformance { node_id: nodes[1], performance: Default::default(), measurement_kind: measurement_kind.clone(), @@ -1324,7 +1306,7 @@ mod tests { env.clone(), &nm, 12345, - NodePerformanceSpecific { + NodePerformance { node_id: nodes[8], performance: Default::default(), measurement_kind, @@ -1359,7 +1341,7 @@ mod tests { env.clone(), &nm, 0, - NodePerformanceSpecific { + NodePerformance { node_id: nodes[0], performance: Default::default(), measurement_kind: measurement_kind.clone(), @@ -1374,7 +1356,7 @@ mod tests { data: Some(LastSubmittedData { sender: nm.clone(), epoch_id: 0, - data: NodePerformanceSpecific { + data: NodePerformance { node_id: nodes[0], performance: Default::default(), measurement_kind: measurement_kind.clone() @@ -1388,7 +1370,7 @@ mod tests { env.clone(), &nm, 0, - NodePerformanceSpecific { + NodePerformance { node_id: nodes[6], performance: Default::default(), measurement_kind: measurement_kind.clone(), @@ -1403,7 +1385,7 @@ mod tests { data: Some(LastSubmittedData { sender: nm.clone(), epoch_id: 0, - data: NodePerformanceSpecific { + data: NodePerformance { node_id: nodes[6], performance: Default::default(), measurement_kind: measurement_kind.clone() @@ -1417,7 +1399,7 @@ mod tests { env.clone(), &nm, 1, - NodePerformanceSpecific { + NodePerformance { node_id: nodes[2], performance: Default::default(), measurement_kind: measurement_kind.clone(), @@ -1432,7 +1414,7 @@ mod tests { data: Some(LastSubmittedData { sender: nm.clone(), epoch_id: 1, - data: NodePerformanceSpecific { + data: NodePerformance { node_id: nodes[2], performance: Default::default(), measurement_kind: measurement_kind.clone() @@ -1446,7 +1428,7 @@ mod tests { env.clone(), &nm, 12345, - NodePerformanceSpecific { + NodePerformance { node_id: nodes[9], performance: Default::default(), measurement_kind: measurement_kind.clone(), @@ -1461,7 +1443,7 @@ mod tests { data: Some(LastSubmittedData { sender: nm.clone(), epoch_id: 12345, - data: NodePerformanceSpecific { + data: NodePerformance { node_id: nodes[9], performance: Default::default(), measurement_kind: measurement_kind.clone() @@ -1483,7 +1465,7 @@ mod tests { tester.authorise_network_monitor(&nm)?; let measurement_kind = tester.define_dummy_measurement_kind().unwrap(); - let dummy_perf = NodePerformanceSpecific { + let dummy_perf = NodePerformance { node_id: 12345, performance: Percent::from_percentage_value(69)?, measurement_kind: measurement_kind.clone(), @@ -1508,7 +1490,7 @@ mod tests { // bonded nym-node let node_id = tester.bond_dummy_nymnode()?; - let perf = NodePerformanceSpecific { + let perf = NodePerformance { node_id, performance: Default::default(), measurement_kind: measurement_kind.clone(), @@ -1631,17 +1613,17 @@ mod tests { let id2 = tester.bond_dummy_nymnode()?; let id3 = tester.bond_dummy_nymnode()?; let measurement_kind = tester.define_dummy_measurement_kind().unwrap(); - let data = NodePerformanceSpecific { + let data = NodePerformance { node_id: id1, performance: Percent::hundred(), measurement_kind: measurement_kind.clone(), }; - let another_data = NodePerformanceSpecific { + let another_data = NodePerformance { node_id: id2, performance: Percent::hundred(), measurement_kind: measurement_kind.clone(), }; - let more_data = NodePerformanceSpecific { + let more_data = NodePerformance { node_id: id3, performance: Percent::hundred(), measurement_kind: measurement_kind.clone(), @@ -1724,12 +1706,12 @@ mod tests { let measurement_kind = tester.define_dummy_measurement_kind().unwrap(); - let data = NodePerformanceSpecific { + let data = NodePerformance { node_id: id1, performance: Percent::hundred(), measurement_kind: measurement_kind.clone(), }; - let another_data = NodePerformanceSpecific { + let another_data = NodePerformance { node_id: id2, performance: Percent::hundred(), measurement_kind: measurement_kind.clone(), @@ -1831,12 +1813,12 @@ mod tests { let measurement_kind = tester.define_dummy_measurement_kind().unwrap(); - let data = NodePerformanceSpecific { + let data = NodePerformance { node_id: id1, performance: Percent::hundred(), measurement_kind: measurement_kind.clone(), }; - let another_data = NodePerformanceSpecific { + let another_data = NodePerformance { node_id: id2, performance: Percent::hundred(), measurement_kind: measurement_kind.clone(), @@ -2016,7 +1998,7 @@ mod tests { env.clone(), &nm, 0, - vec![NodePerformanceSpecific { + vec![NodePerformance { node_id: nodes[0], performance: Default::default(), measurement_kind: measurement_kind.clone(), @@ -2035,7 +2017,7 @@ mod tests { env.clone(), &nm, 1, - vec![NodePerformanceSpecific { + vec![NodePerformance { node_id: nodes[1], performance: Default::default(), measurement_kind: measurement_kind.clone(), @@ -2055,17 +2037,17 @@ mod tests { &nm, 1, vec![ - NodePerformanceSpecific { + NodePerformance { node_id: nodes[2], performance: Default::default(), measurement_kind: measurement_kind.clone(), }, - NodePerformanceSpecific { + NodePerformance { node_id: nodes[3], performance: Default::default(), measurement_kind: measurement_kind.clone(), }, - NodePerformanceSpecific { + NodePerformance { node_id: nodes[4], performance: Default::default(), measurement_kind: measurement_kind.clone(), @@ -2086,17 +2068,17 @@ mod tests { &nm, 2, vec![ - NodePerformanceSpecific { + NodePerformance { node_id: nodes[1], performance: Default::default(), measurement_kind: measurement_kind.clone(), }, - NodePerformanceSpecific { + NodePerformance { node_id: nodes[6], performance: Default::default(), measurement_kind: measurement_kind.clone(), }, - NodePerformanceSpecific { + NodePerformance { node_id: nodes[8], performance: Default::default(), measurement_kind: measurement_kind.clone(), @@ -2134,7 +2116,7 @@ mod tests { env.clone(), &nm, 0, - vec![NodePerformanceSpecific { + vec![NodePerformance { node_id: nodes[0], performance: Default::default(), measurement_kind: measurement_kind.clone(), @@ -2149,7 +2131,7 @@ mod tests { data: Some(LastSubmittedData { sender: nm.clone(), epoch_id: 0, - data: NodePerformanceSpecific { + data: NodePerformance { node_id: nodes[0], performance: Default::default(), measurement_kind: measurement_kind.clone() @@ -2164,7 +2146,7 @@ mod tests { env.clone(), &nm, 1, - vec![NodePerformanceSpecific { + vec![NodePerformance { node_id: nodes[1], performance: Default::default(), measurement_kind: measurement_kind.clone(), @@ -2179,7 +2161,7 @@ mod tests { data: Some(LastSubmittedData { sender: nm.clone(), epoch_id: 1, - data: NodePerformanceSpecific { + data: NodePerformance { node_id: nodes[1], performance: Default::default(), measurement_kind: measurement_kind.clone() @@ -2195,17 +2177,17 @@ mod tests { &nm, 1, vec![ - NodePerformanceSpecific { + NodePerformance { node_id: nodes[2], performance: Default::default(), measurement_kind: measurement_kind.clone(), }, - NodePerformanceSpecific { + NodePerformance { node_id: nodes[3], performance: Default::default(), measurement_kind: measurement_kind.clone(), }, - NodePerformanceSpecific { + NodePerformance { node_id: nodes[4], performance: Default::default(), measurement_kind: measurement_kind.clone(), @@ -2221,7 +2203,7 @@ mod tests { data: Some(LastSubmittedData { sender: nm.clone(), epoch_id: 1, - data: NodePerformanceSpecific { + data: NodePerformance { node_id: nodes[4], performance: Default::default(), measurement_kind: measurement_kind.clone() @@ -2237,17 +2219,17 @@ mod tests { &nm, 2, vec![ - NodePerformanceSpecific { + NodePerformance { node_id: nodes[1], performance: Default::default(), measurement_kind: measurement_kind.clone(), }, - NodePerformanceSpecific { + NodePerformance { node_id: nodes[7], performance: Default::default(), measurement_kind: measurement_kind.clone(), }, - NodePerformanceSpecific { + NodePerformance { node_id: nodes[8], performance: Default::default(), measurement_kind: measurement_kind.clone(), @@ -2263,7 +2245,7 @@ mod tests { data: Some(LastSubmittedData { sender: nm.clone(), epoch_id: 2, - data: NodePerformanceSpecific { + data: NodePerformance { node_id: nodes[8], performance: Default::default(), measurement_kind: measurement_kind.clone() @@ -2304,7 +2286,7 @@ mod tests { env.clone(), &nm, 0, - vec![NodePerformanceSpecific { + vec![NodePerformance { node_id: 999999, performance: Default::default(), measurement_kind: measurement_kind.clone(), @@ -2320,12 +2302,12 @@ mod tests { &nm, 1, vec![ - NodePerformanceSpecific { + NodePerformance { node_id: nym_node1, performance: Default::default(), measurement_kind: measurement_kind.clone(), }, - NodePerformanceSpecific { + NodePerformance { node_id: 999999, performance: Default::default(), measurement_kind: measurement_kind.clone(), @@ -2342,22 +2324,22 @@ mod tests { &nm, 2, vec![ - NodePerformanceSpecific { + NodePerformance { node_id: 2, performance: Default::default(), measurement_kind: measurement_kind.clone(), }, - NodePerformanceSpecific { + NodePerformance { node_id: nym_node1, performance: Default::default(), measurement_kind: measurement_kind.clone(), }, - NodePerformanceSpecific { + NodePerformance { node_id: nym_node_between, performance: Default::default(), measurement_kind: measurement_kind.clone(), }, - NodePerformanceSpecific { + NodePerformance { node_id: nym_node2, performance: Default::default(), measurement_kind: measurement_kind.clone(), @@ -2651,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 @@ -2664,6 +2649,7 @@ mod tests { assert_eq!( storage .try_load_performance(&tester, 0, node_id)? + .get(&measurement_kind) .unwrap() .value() .to_string(), @@ -2677,6 +2663,7 @@ mod tests { assert_eq!( storage .try_load_performance(&tester, 0, node_id)? + .get(&measurement_kind) .unwrap() .value() .to_string(), @@ -2690,6 +2677,7 @@ mod tests { assert_eq!( storage .try_load_performance(&tester, 0, node_id)? + .get(&measurement_kind) .unwrap() .value() .to_string(), @@ -2704,6 +2692,7 @@ mod tests { assert_eq!( storage .try_load_performance(&tester, 0, node_id)? + .get(&measurement_kind) .unwrap() .value() .to_string(), @@ -2720,6 +2709,7 @@ mod tests { assert_eq!( storage .try_load_performance(&tester, 0, node_id)? + .get(&measurement_kind) .unwrap() .value() .to_string(), @@ -2737,6 +2727,7 @@ mod tests { assert_eq!( storage .try_load_performance(&tester, 0, node_id)? + .get(&measurement_kind) .unwrap() .value() .to_string(), @@ -2848,7 +2839,7 @@ mod tests { let before = storage .performance_results - .results_specific + .results .prefix((epoch_id, id1)) .all_values(tester.storage()) .unwrap(); @@ -2858,7 +2849,7 @@ mod tests { let after = storage .performance_results - .results_specific + .results .prefix((epoch_id, id1)) .all_values(tester.storage()) .unwrap(); @@ -2871,7 +2862,7 @@ mod tests { let before = storage .performance_results - .results_specific + .results .prefix((epoch_id, id2)) .all_values(tester.storage()) .unwrap(); @@ -2881,7 +2872,7 @@ mod tests { let after = storage .performance_results - .results_specific + .results .prefix((epoch_id, id2)) .all_values(tester.storage()) .unwrap(); @@ -2998,7 +2989,7 @@ mod tests { let before = storage .performance_results - .results_specific + .results .sub_prefix(epoch_id) .all_values(&tester)?; assert_eq!(before.len(), 10); @@ -3008,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()); @@ -3031,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()); @@ -3065,7 +3056,7 @@ mod tests { let before = storage .performance_results - .results_specific + .results .sub_prefix(epoch_id) .all_values(&tester)?; assert_eq!( @@ -3077,7 +3068,7 @@ mod tests { assert!(res.additional_entries_to_remove_remaining); let after = storage .performance_results - .results_specific + .results .sub_prefix(epoch_id) .all_values(&tester)?; @@ -3090,7 +3081,7 @@ mod tests { assert!(res.additional_entries_to_remove_remaining); let after = storage .performance_results - .results_specific + .results .sub_prefix(epoch_id) .all_values(&tester)?; @@ -3100,7 +3091,7 @@ mod tests { assert!(!res.additional_entries_to_remove_remaining); let after = storage .performance_results - .results_specific + .results .sub_prefix(epoch_id) .all_values(&tester)?; @@ -3266,69 +3257,88 @@ mod tests { let node_id2 = 456; let measurement_kind = tester.define_dummy_measurement_kind().unwrap(); - let data1 = NodePerformanceSpecific { + let data1 = NodePerformance { node_id: node_id1, performance: Percent::from_str("0.23")?, measurement_kind: measurement_kind.clone(), }; - let data2 = NodePerformanceSpecific { + let data2 = NodePerformance { node_id: node_id1, performance: Percent::hundred(), measurement_kind: measurement_kind.clone(), }; - let data3 = NodePerformanceSpecific { + let data3 = NodePerformance { node_id: node_id2, performance: Percent::from_str("0.23643634")?, measurement_kind: measurement_kind.clone(), }; - let data4 = NodePerformanceSpecific { + 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] ); diff --git a/contracts/performance/src/testing/mod.rs b/contracts/performance/src/testing/mod.rs index 7f394ba7ae..8bc495dfac 100644 --- a/contracts/performance/src/testing/mod.rs +++ b/contracts/performance/src/testing/mod.rs @@ -27,8 +27,8 @@ use nym_mixnet_contract_common::{ }; use nym_performance_contract_common::constants::storage_keys; use nym_performance_contract_common::{ - ExecuteMsg, InstantiateMsg, MigrateMsg, NodeId, NodePerformanceSpecific, NodeResults, - NymPerformanceContractError, QueryMsg, + EpochNodePerformance, ExecuteMsg, InstantiateMsg, MigrateMsg, NodeId, NodePerformance, + NodeResults, NymPerformanceContractError, QueryMsg, }; use serde::Serialize; use serde::de::DeserializeOwned; @@ -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 { @@ -378,10 +392,10 @@ pub(crate) trait PerformanceContractTesterExt: Ok(measurement_kind) } - fn dummy_node_performance(&mut self) -> NodePerformanceSpecific { + fn dummy_node_performance(&mut self) -> NodePerformance { let node_id = self.bond_dummy_nymnode().unwrap(); let measurement_kind = self.dummy_measurement_kind(); - NodePerformanceSpecific { + NodePerformance { node_id, performance: Percent::from_percentage_value(69).unwrap(), measurement_kind, @@ -413,7 +427,7 @@ pub(crate) trait PerformanceContractTesterExt: env, addr, epoch_id, - NodePerformanceSpecific { + NodePerformance { node_id, performance, measurement_kind, @@ -455,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 833545720f..d81d335c7f 100644 --- a/contracts/performance/src/transactions.rs +++ b/contracts/performance/src/transactions.rs @@ -4,7 +4,7 @@ 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, NodePerformanceSpecific, NymPerformanceContractError, + EpochId, NodeId, NodePerformance, NymPerformanceContractError, }; pub fn try_update_contract_admin( @@ -30,6 +30,18 @@ pub fn try_define_measurement_kind( .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)?; @@ -58,7 +70,7 @@ pub fn try_submit_performance_results( env: Env, info: MessageInfo, epoch_id: EpochId, - data: NodePerformanceSpecific, + data: NodePerformance, ) -> Result { NYM_PERFORMANCE_CONTRACT_STORAGE.submit_performance_data( deps, @@ -77,7 +89,7 @@ pub fn try_batch_submit_performance_results( env: Env, info: MessageInfo, epoch_id: EpochId, - data: Vec, + data: Vec, ) -> Result { let res = NYM_PERFORMANCE_CONTRACT_STORAGE.batch_submit_performance_results( deps, diff --git a/nym-api/src/node_performance/contract_cache/data.rs b/nym-api/src/node_performance/contract_cache/data.rs index 81ee024ef1..985ee0a4d7 100644 --- a/nym-api/src/node_performance/contract_cache/data.rs +++ b/nym-api/src/node_performance/contract_cache/data.rs @@ -6,7 +6,7 @@ use nym_api_requests::models::RoutingScore; use nym_contracts_common::NaiveFloat; use nym_mixnet_contract_common::reward_params::Performance; use nym_mixnet_contract_common::{EpochId, NodeId}; -use nym_validator_client::nyxd::contract_traits::performance_query_client::NodePerformanceSpecific; +use nym_validator_client::nyxd::contract_traits::performance_query_client::NodePerformance; use std::collections::{BTreeMap, HashMap}; pub(crate) struct PerformanceContractEpochCacheData { @@ -16,7 +16,7 @@ pub(crate) struct PerformanceContractEpochCacheData { impl PerformanceContractEpochCacheData { pub(crate) fn from_node_performance( - performance: Vec, + performance: Vec, epoch_id: EpochId, ) -> Self { let median_performance = performance diff --git a/nym-api/src/support/nyxd/mod.rs b/nym-api/src/support/nyxd/mod.rs index c6a8949a05..6de33c4738 100644 --- a/nym-api/src/support/nyxd/mod.rs +++ b/nym-api/src/support/nyxd/mod.rs @@ -36,7 +36,7 @@ use nym_mixnet_contract_common::{ use nym_validator_client::coconut::EcashApiError; use nym_validator_client::nyxd::contract_traits::mixnet_query_client::MixnetQueryClientExt; use nym_validator_client::nyxd::contract_traits::performance_query_client::{ - LastSubmission, NodePerformanceSpecific, + LastSubmission, NodePerformance, }; use nym_validator_client::nyxd::contract_traits::{ PagedDkgQueryClient, PagedPerformanceQueryClient, PerformanceQueryClient, @@ -414,7 +414,7 @@ impl Client { pub(crate) async fn get_full_epoch_performance( &self, epoch_id: nym_mixnet_contract_common::EpochId, - ) -> Result, NyxdError> { + ) -> Result, NyxdError> { nyxd_query!(self, get_all_epoch_performance(epoch_id).await) } }