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 722a2d002c0..470a412e8d3 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 @@ -7,17 +7,16 @@ use crate::nyxd::error::NyxdError; use crate::nyxd::CosmWasmClient; use async_trait::async_trait; use cosmrs::AccountId; +use serde::Deserialize; + pub use nym_performance_contract_common::{ - msg::QueryMsg as PerformanceQueryMsg, types::NetworkMonitorResponse, + msg::QueryMsg as PerformanceQueryMsg, types::NetworkMonitorResponse, EpochId, + EpochMeasurementsPagedResponse, EpochNodePerformance, EpochPerformancePagedResponse, + FullHistoricalPerformancePagedResponse, HistoricalPerformance, LastSubmission, + NetworkMonitorInformation, NetworkMonitorsPagedResponse, NodeId, NodeMeasurement, + NodeMeasurementsResponse, NodePerformance, NodePerformancePagedResponse, + NodePerformanceResponse, RetiredNetworkMonitor, RetiredNetworkMonitorsPagedResponse, }; -use nym_performance_contract_common::{ - EpochId, EpochMeasurementsPagedResponse, EpochNodePerformance, EpochPerformancePagedResponse, - FullHistoricalPerformancePagedResponse, HistoricalPerformance, NetworkMonitorInformation, - NetworkMonitorsPagedResponse, NodeId, NodeMeasurement, NodeMeasurementsResponse, - NodePerformance, NodePerformancePagedResponse, NodePerformanceResponse, RetiredNetworkMonitor, - RetiredNetworkMonitorsPagedResponse, -}; -use serde::Deserialize; #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] @@ -139,6 +138,11 @@ pub trait PerformanceQueryClient { }) .await } + + async fn get_last_submission(&self) -> Result { + self.query_performance_contract(PerformanceQueryMsg::LastSubmittedMeasurement {}) + .await + } } // extension trait to the query client to deal with the paged queries @@ -212,6 +216,7 @@ where mod tests { use super::*; use crate::nyxd::contract_traits::tests::IgnoreValue; + use nym_performance_contract_common::QueryMsg; // it's enough that this compiles and clippy is happy about it #[allow(dead_code)] @@ -260,6 +265,7 @@ mod tests { PerformanceQueryMsg::RetiredNetworkMonitorsPaged { start_after, limit } => client .get_retired_network_monitors_paged(start_after, limit) .ignore(), + QueryMsg::LastSubmittedMeasurement {} => client.get_last_submission().ignore(), }; } } diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/mod.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/mod.rs index ce54caab33f..374b30bf0c4 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/mod.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/mod.rs @@ -44,6 +44,17 @@ pub struct RewardEstimate { pub operating_cost: Decimal, } +impl RewardEstimate { + pub const fn zero() -> RewardEstimate { + RewardEstimate { + total_node_reward: Decimal::zero(), + operator: Decimal::zero(), + delegates: Decimal::zero(), + operating_cost: Decimal::zero(), + } + } +} + #[cw_serde] #[derive(Copy, Default)] pub struct RewardDistribution { 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 b452f959163..5f98a8e4398 100644 --- a/common/cosmwasm-smart-contracts/nym-performance-contract/src/constants.rs +++ b/common/cosmwasm-smart-contracts/nym-performance-contract/src/constants.rs @@ -4,6 +4,7 @@ pub mod storage_keys { pub const CONTRACT_ADMIN: &str = "contract-admin"; pub const INITIAL_EPOCH_ID: &str = "initial-epoch-id"; + pub const LAST_SUBMISSION: &str = "last-submission"; pub const MIXNET_CONTRACT: &str = "mixnet-contract"; pub const AUTHORISED_COUNT: &str = "authorised-count"; pub const AUTHORISED: &str = "authorised"; 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 cf03b650b78..74f512d8915 100644 --- a/common/cosmwasm-smart-contracts/nym-performance-contract/src/msg.rs +++ b/common/cosmwasm-smart-contracts/nym-performance-contract/src/msg.rs @@ -7,9 +7,9 @@ use cosmwasm_schema::cw_serde; #[cfg(feature = "schema")] use crate::types::{ EpochMeasurementsPagedResponse, EpochPerformancePagedResponse, - FullHistoricalPerformancePagedResponse, NetworkMonitorResponse, NetworkMonitorsPagedResponse, - NodeMeasurementsResponse, NodePerformancePagedResponse, NodePerformanceResponse, - RetiredNetworkMonitorsPagedResponse, + FullHistoricalPerformancePagedResponse, LastSubmission, NetworkMonitorResponse, + NetworkMonitorsPagedResponse, NodeMeasurementsResponse, NodePerformancePagedResponse, + NodePerformanceResponse, RetiredNetworkMonitorsPagedResponse, }; #[cw_serde] @@ -113,6 +113,10 @@ pub enum QueryMsg { start_after: Option, limit: Option, }, + + /// Returns information regarding the latest submitted performance data + #[cfg_attr(feature = "schema", returns(LastSubmission))] + LastSubmittedMeasurement {}, } #[cw_serde] 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 10ef055d28b..383fb082802 100644 --- a/common/cosmwasm-smart-contracts/nym-performance-contract/src/types.rs +++ b/common/cosmwasm-smart-contracts/nym-performance-contract/src/types.rs @@ -2,12 +2,28 @@ // SPDX-License-Identifier: Apache-2.0 use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Addr, Env}; +use cosmwasm_std::{Addr, Env, Timestamp}; use nym_contracts_common::Percent; pub type EpochId = u32; pub type NodeId = u32; +#[cw_serde] +pub struct LastSubmission { + pub block_height: u64, + pub block_time: Timestamp, + + // not as relevant, but might as well store it + pub data: Option, +} + +#[cw_serde] +pub struct LastSubmittedData { + pub sender: Addr, + pub epoch_id: EpochId, + pub data: NodePerformance, +} + #[cw_serde] pub struct NetworkMonitorDetails { pub address: Addr, diff --git a/contracts/performance/schema/nym-performance-contract.json b/contracts/performance/schema/nym-performance-contract.json new file mode 100644 index 00000000000..2242aefd752 --- /dev/null +++ b/contracts/performance/schema/nym-performance-contract.json @@ -0,0 +1,1240 @@ +{ + "contract_name": "nym-performance-contract", + "contract_version": "0.1.0", + "idl_version": "1.0.0", + "instantiate": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "type": "object", + "required": [ + "authorised_network_monitors", + "mixnet_contract_address" + ], + "properties": { + "authorised_network_monitors": { + "type": "array", + "items": { + "type": "string" + } + }, + "mixnet_contract_address": { + "type": "string" + } + }, + "additionalProperties": false + }, + "execute": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "oneOf": [ + { + "description": "Change the admin", + "type": "object", + "required": [ + "update_admin" + ], + "properties": { + "update_admin": { + "type": "object", + "required": [ + "admin" + ], + "properties": { + "admin": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Attempt to submit performance data of a particular node for given epoch", + "type": "object", + "required": [ + "submit" + ], + "properties": { + "submit": { + "type": "object", + "required": [ + "data", + "epoch" + ], + "properties": { + "data": { + "$ref": "#/definitions/NodePerformance" + }, + "epoch": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Attempt to submit performance data of a batch of nodes for given epoch", + "type": "object", + "required": [ + "batch_submit" + ], + "properties": { + "batch_submit": { + "type": "object", + "required": [ + "data", + "epoch" + ], + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/NodePerformance" + } + }, + "epoch": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Attempt to authorise new network monitor for submitting performance data", + "type": "object", + "required": [ + "authorise_network_monitor" + ], + "properties": { + "authorise_network_monitor": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Attempt to retire an existing network monitor and forbid it from submitting any future performance data", + "type": "object", + "required": [ + "retire_network_monitor" + ], + "properties": { + "retire_network_monitor": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ], + "definitions": { + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "NodePerformance": { + "type": "object", + "required": [ + "n", + "p" + ], + "properties": { + "n": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "p": { + "$ref": "#/definitions/Percent" + } + }, + "additionalProperties": false + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + } + }, + "query": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "oneOf": [ + { + "type": "object", + "required": [ + "admin" + ], + "properties": { + "admin": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns performance of particular node for the provided epoch", + "type": "object", + "required": [ + "node_performance" + ], + "properties": { + "node_performance": { + "type": "object", + "required": [ + "epoch_id", + "node_id" + ], + "properties": { + "epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "node_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns historical performance for particular node", + "type": "object", + "required": [ + "node_performance_paged" + ], + "properties": { + "node_performance_paged": { + "type": "object", + "required": [ + "node_id" + ], + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "node_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns all submitted measurements for the particular node", + "type": "object", + "required": [ + "node_measurements" + ], + "properties": { + "node_measurements": { + "type": "object", + "required": [ + "epoch_id", + "node_id" + ], + "properties": { + "epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "node_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns (paged) measurements for particular epoch", + "type": "object", + "required": [ + "epoch_measurements_paged" + ], + "properties": { + "epoch_measurements_paged": { + "type": "object", + "required": [ + "epoch_id" + ], + "properties": { + "epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns (paged) performance for particular epoch", + "type": "object", + "required": [ + "epoch_performance_paged" + ], + "properties": { + "epoch_performance_paged": { + "type": "object", + "required": [ + "epoch_id" + ], + "properties": { + "epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns full (paged) historical performance of the whole network", + "type": "object", + "required": [ + "full_historical_performance_paged" + ], + "properties": { + "full_historical_performance_paged": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "array", + "null" + ], + "items": [ + { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + ], + "maxItems": 2, + "minItems": 2 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns information about particular network monitor", + "type": "object", + "required": [ + "network_monitor" + ], + "properties": { + "network_monitor": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns information about all network monitors", + "type": "object", + "required": [ + "network_monitors_paged" + ], + "properties": { + "network_monitors_paged": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns information about all retired network monitors", + "type": "object", + "required": [ + "retired_network_monitors_paged" + ], + "properties": { + "retired_network_monitors_paged": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns information regarding the latest submitted performance data", + "type": "object", + "required": [ + "last_submitted_measurement" + ], + "properties": { + "last_submitted_measurement": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "migrate": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MigrateMsg", + "type": "object", + "additionalProperties": false + }, + "sudo": null, + "responses": { + "admin": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AdminResponse", + "description": "Returned from Admin.query_admin()", + "type": "object", + "properties": { + "admin": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + }, + "epoch_measurements_paged": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "EpochMeasurementsPagedResponse", + "type": "object", + "required": [ + "epoch_id", + "measurements" + ], + "properties": { + "epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "measurements": { + "type": "array", + "items": { + "$ref": "#/definitions/NodeMeasurement" + } + }, + "start_next_after": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "NodeMeasurement": { + "type": "object", + "required": [ + "measurements", + "node_id" + ], + "properties": { + "measurements": { + "$ref": "#/definitions/NodeResults" + }, + "node_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "NodeResults": { + "type": "array", + "items": { + "$ref": "#/definitions/Percent" + } + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + } + }, + "epoch_performance_paged": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "EpochPerformancePagedResponse", + "type": "object", + "required": [ + "epoch_id", + "performance" + ], + "properties": { + "epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "performance": { + "type": "array", + "items": { + "$ref": "#/definitions/NodePerformance" + } + }, + "start_next_after": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "NodePerformance": { + "type": "object", + "required": [ + "n", + "p" + ], + "properties": { + "n": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "p": { + "$ref": "#/definitions/Percent" + } + }, + "additionalProperties": false + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + } + }, + "full_historical_performance_paged": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "FullHistoricalPerformancePagedResponse", + "type": "object", + "required": [ + "performance" + ], + "properties": { + "performance": { + "type": "array", + "items": { + "$ref": "#/definitions/HistoricalPerformance" + } + }, + "start_next_after": { + "type": [ + "array", + "null" + ], + "items": [ + { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + ], + "maxItems": 2, + "minItems": 2 + } + }, + "additionalProperties": false, + "definitions": { + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "HistoricalPerformance": { + "type": "object", + "required": [ + "epoch_id", + "node_id", + "performance" + ], + "properties": { + "epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "node_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "performance": { + "$ref": "#/definitions/Percent" + } + }, + "additionalProperties": false + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + } + }, + "last_submitted_measurement": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "LastSubmission", + "type": "object", + "required": [ + "block_height", + "block_time" + ], + "properties": { + "block_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "block_time": { + "$ref": "#/definitions/Timestamp" + }, + "data": { + "anyOf": [ + { + "$ref": "#/definitions/LastSubmittedData" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "LastSubmittedData": { + "type": "object", + "required": [ + "data", + "epoch_id", + "sender" + ], + "properties": { + "data": { + "$ref": "#/definitions/NodePerformance" + }, + "epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "sender": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + }, + "NodePerformance": { + "type": "object", + "required": [ + "n", + "p" + ], + "properties": { + "n": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "p": { + "$ref": "#/definitions/Percent" + } + }, + "additionalProperties": false + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } + }, + "network_monitor": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NetworkMonitorResponse", + "type": "object", + "properties": { + "info": { + "anyOf": [ + { + "$ref": "#/definitions/NetworkMonitorInformation" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "NetworkMonitorDetails": { + "type": "object", + "required": [ + "address", + "authorised_at_height", + "authorised_by" + ], + "properties": { + "address": { + "$ref": "#/definitions/Addr" + }, + "authorised_at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "authorised_by": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + }, + "NetworkMonitorInformation": { + "type": "object", + "required": [ + "current_submission_metadata", + "details" + ], + "properties": { + "current_submission_metadata": { + "$ref": "#/definitions/NetworkMonitorSubmissionMetadata" + }, + "details": { + "$ref": "#/definitions/NetworkMonitorDetails" + } + }, + "additionalProperties": false + }, + "NetworkMonitorSubmissionMetadata": { + "type": "object", + "required": [ + "last_submitted_epoch_id", + "last_submitted_node_id" + ], + "properties": { + "last_submitted_epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "last_submitted_node_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } + }, + "network_monitors_paged": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NetworkMonitorsPagedResponse", + "type": "object", + "required": [ + "info" + ], + "properties": { + "info": { + "type": "array", + "items": { + "$ref": "#/definitions/NetworkMonitorInformation" + } + }, + "start_next_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "NetworkMonitorDetails": { + "type": "object", + "required": [ + "address", + "authorised_at_height", + "authorised_by" + ], + "properties": { + "address": { + "$ref": "#/definitions/Addr" + }, + "authorised_at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "authorised_by": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + }, + "NetworkMonitorInformation": { + "type": "object", + "required": [ + "current_submission_metadata", + "details" + ], + "properties": { + "current_submission_metadata": { + "$ref": "#/definitions/NetworkMonitorSubmissionMetadata" + }, + "details": { + "$ref": "#/definitions/NetworkMonitorDetails" + } + }, + "additionalProperties": false + }, + "NetworkMonitorSubmissionMetadata": { + "type": "object", + "required": [ + "last_submitted_epoch_id", + "last_submitted_node_id" + ], + "properties": { + "last_submitted_epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "last_submitted_node_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } + }, + "node_measurements": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NodeMeasurementsResponse", + "type": "object", + "properties": { + "measurements": { + "anyOf": [ + { + "$ref": "#/definitions/NodeResults" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "NodeResults": { + "type": "array", + "items": { + "$ref": "#/definitions/Percent" + } + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + } + }, + "node_performance": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NodePerformanceResponse", + "type": "object", + "properties": { + "performance": { + "anyOf": [ + { + "$ref": "#/definitions/Percent" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + } + }, + "node_performance_paged": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NodePerformancePagedResponse", + "type": "object", + "required": [ + "node_id", + "performance" + ], + "properties": { + "node_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "performance": { + "type": "array", + "items": { + "$ref": "#/definitions/EpochNodePerformance" + } + }, + "start_next_after": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "EpochNodePerformance": { + "type": "object", + "required": [ + "epoch" + ], + "properties": { + "epoch": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "performance": { + "anyOf": [ + { + "$ref": "#/definitions/Percent" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + } + }, + "retired_network_monitors_paged": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "RetiredNetworkMonitorsPagedResponse", + "type": "object", + "required": [ + "info" + ], + "properties": { + "info": { + "type": "array", + "items": { + "$ref": "#/definitions/RetiredNetworkMonitor" + } + }, + "start_next_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "NetworkMonitorDetails": { + "type": "object", + "required": [ + "address", + "authorised_at_height", + "authorised_by" + ], + "properties": { + "address": { + "$ref": "#/definitions/Addr" + }, + "authorised_at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "authorised_by": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + }, + "RetiredNetworkMonitor": { + "type": "object", + "required": [ + "details", + "retired_at_height", + "retired_by" + ], + "properties": { + "details": { + "$ref": "#/definitions/NetworkMonitorDetails" + }, + "retired_at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "retired_by": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + } + } + } +} diff --git a/contracts/performance/schema/raw/execute.json b/contracts/performance/schema/raw/execute.json new file mode 100644 index 00000000000..c4b5c8be041 --- /dev/null +++ b/contracts/performance/schema/raw/execute.json @@ -0,0 +1,163 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "oneOf": [ + { + "description": "Change the admin", + "type": "object", + "required": [ + "update_admin" + ], + "properties": { + "update_admin": { + "type": "object", + "required": [ + "admin" + ], + "properties": { + "admin": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Attempt to submit performance data of a particular node for given epoch", + "type": "object", + "required": [ + "submit" + ], + "properties": { + "submit": { + "type": "object", + "required": [ + "data", + "epoch" + ], + "properties": { + "data": { + "$ref": "#/definitions/NodePerformance" + }, + "epoch": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Attempt to submit performance data of a batch of nodes for given epoch", + "type": "object", + "required": [ + "batch_submit" + ], + "properties": { + "batch_submit": { + "type": "object", + "required": [ + "data", + "epoch" + ], + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/NodePerformance" + } + }, + "epoch": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Attempt to authorise new network monitor for submitting performance data", + "type": "object", + "required": [ + "authorise_network_monitor" + ], + "properties": { + "authorise_network_monitor": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Attempt to retire an existing network monitor and forbid it from submitting any future performance data", + "type": "object", + "required": [ + "retire_network_monitor" + ], + "properties": { + "retire_network_monitor": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ], + "definitions": { + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "NodePerformance": { + "type": "object", + "required": [ + "n", + "p" + ], + "properties": { + "n": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "p": { + "$ref": "#/definitions/Percent" + } + }, + "additionalProperties": false + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + } +} diff --git a/contracts/performance/schema/raw/instantiate.json b/contracts/performance/schema/raw/instantiate.json new file mode 100644 index 00000000000..4380993e577 --- /dev/null +++ b/contracts/performance/schema/raw/instantiate.json @@ -0,0 +1,21 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "type": "object", + "required": [ + "authorised_network_monitors", + "mixnet_contract_address" + ], + "properties": { + "authorised_network_monitors": { + "type": "array", + "items": { + "type": "string" + } + }, + "mixnet_contract_address": { + "type": "string" + } + }, + "additionalProperties": false +} diff --git a/contracts/performance/schema/raw/migrate.json b/contracts/performance/schema/raw/migrate.json new file mode 100644 index 00000000000..7fbe8c5708e --- /dev/null +++ b/contracts/performance/schema/raw/migrate.json @@ -0,0 +1,6 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MigrateMsg", + "type": "object", + "additionalProperties": false +} diff --git a/contracts/performance/schema/raw/query.json b/contracts/performance/schema/raw/query.json new file mode 100644 index 00000000000..bcca4117804 --- /dev/null +++ b/contracts/performance/schema/raw/query.json @@ -0,0 +1,339 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "oneOf": [ + { + "type": "object", + "required": [ + "admin" + ], + "properties": { + "admin": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns performance of particular node for the provided epoch", + "type": "object", + "required": [ + "node_performance" + ], + "properties": { + "node_performance": { + "type": "object", + "required": [ + "epoch_id", + "node_id" + ], + "properties": { + "epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "node_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns historical performance for particular node", + "type": "object", + "required": [ + "node_performance_paged" + ], + "properties": { + "node_performance_paged": { + "type": "object", + "required": [ + "node_id" + ], + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "node_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns all submitted measurements for the particular node", + "type": "object", + "required": [ + "node_measurements" + ], + "properties": { + "node_measurements": { + "type": "object", + "required": [ + "epoch_id", + "node_id" + ], + "properties": { + "epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "node_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns (paged) measurements for particular epoch", + "type": "object", + "required": [ + "epoch_measurements_paged" + ], + "properties": { + "epoch_measurements_paged": { + "type": "object", + "required": [ + "epoch_id" + ], + "properties": { + "epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns (paged) performance for particular epoch", + "type": "object", + "required": [ + "epoch_performance_paged" + ], + "properties": { + "epoch_performance_paged": { + "type": "object", + "required": [ + "epoch_id" + ], + "properties": { + "epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns full (paged) historical performance of the whole network", + "type": "object", + "required": [ + "full_historical_performance_paged" + ], + "properties": { + "full_historical_performance_paged": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "array", + "null" + ], + "items": [ + { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + ], + "maxItems": 2, + "minItems": 2 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns information about particular network monitor", + "type": "object", + "required": [ + "network_monitor" + ], + "properties": { + "network_monitor": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns information about all network monitors", + "type": "object", + "required": [ + "network_monitors_paged" + ], + "properties": { + "network_monitors_paged": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns information about all retired network monitors", + "type": "object", + "required": [ + "retired_network_monitors_paged" + ], + "properties": { + "retired_network_monitors_paged": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns information regarding the latest submitted performance data", + "type": "object", + "required": [ + "last_submitted_measurement" + ], + "properties": { + "last_submitted_measurement": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] +} diff --git a/contracts/performance/schema/raw/response_to_admin.json b/contracts/performance/schema/raw/response_to_admin.json new file mode 100644 index 00000000000..c73969ab04b --- /dev/null +++ b/contracts/performance/schema/raw/response_to_admin.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AdminResponse", + "description": "Returned from Admin.query_admin()", + "type": "object", + "properties": { + "admin": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false +} diff --git a/contracts/performance/schema/raw/response_to_epoch_measurements_paged.json b/contracts/performance/schema/raw/response_to_epoch_measurements_paged.json new file mode 100644 index 00000000000..4c16b66bb7a --- /dev/null +++ b/contracts/performance/schema/raw/response_to_epoch_measurements_paged.json @@ -0,0 +1,69 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "EpochMeasurementsPagedResponse", + "type": "object", + "required": [ + "epoch_id", + "measurements" + ], + "properties": { + "epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "measurements": { + "type": "array", + "items": { + "$ref": "#/definitions/NodeMeasurement" + } + }, + "start_next_after": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "NodeMeasurement": { + "type": "object", + "required": [ + "measurements", + "node_id" + ], + "properties": { + "measurements": { + "$ref": "#/definitions/NodeResults" + }, + "node_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "NodeResults": { + "type": "array", + "items": { + "$ref": "#/definitions/Percent" + } + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + } +} diff --git a/contracts/performance/schema/raw/response_to_epoch_performance_paged.json b/contracts/performance/schema/raw/response_to_epoch_performance_paged.json new file mode 100644 index 00000000000..235c12b64f6 --- /dev/null +++ b/contracts/performance/schema/raw/response_to_epoch_performance_paged.json @@ -0,0 +1,63 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "EpochPerformancePagedResponse", + "type": "object", + "required": [ + "epoch_id", + "performance" + ], + "properties": { + "epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "performance": { + "type": "array", + "items": { + "$ref": "#/definitions/NodePerformance" + } + }, + "start_next_after": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "NodePerformance": { + "type": "object", + "required": [ + "n", + "p" + ], + "properties": { + "n": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "p": { + "$ref": "#/definitions/Percent" + } + }, + "additionalProperties": false + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + } +} diff --git a/contracts/performance/schema/raw/response_to_full_historical_performance_paged.json b/contracts/performance/schema/raw/response_to_full_historical_performance_paged.json new file mode 100644 index 00000000000..6f0eb6e1919 --- /dev/null +++ b/contracts/performance/schema/raw/response_to_full_historical_performance_paged.json @@ -0,0 +1,75 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "FullHistoricalPerformancePagedResponse", + "type": "object", + "required": [ + "performance" + ], + "properties": { + "performance": { + "type": "array", + "items": { + "$ref": "#/definitions/HistoricalPerformance" + } + }, + "start_next_after": { + "type": [ + "array", + "null" + ], + "items": [ + { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + ], + "maxItems": 2, + "minItems": 2 + } + }, + "additionalProperties": false, + "definitions": { + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "HistoricalPerformance": { + "type": "object", + "required": [ + "epoch_id", + "node_id", + "performance" + ], + "properties": { + "epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "node_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "performance": { + "$ref": "#/definitions/Percent" + } + }, + "additionalProperties": false + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + } +} diff --git a/contracts/performance/schema/raw/response_to_last_submitted_measurement.json b/contracts/performance/schema/raw/response_to_last_submitted_measurement.json new file mode 100644 index 00000000000..7c3779d9a66 --- /dev/null +++ b/contracts/performance/schema/raw/response_to_last_submitted_measurement.json @@ -0,0 +1,100 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "LastSubmission", + "type": "object", + "required": [ + "block_height", + "block_time" + ], + "properties": { + "block_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "block_time": { + "$ref": "#/definitions/Timestamp" + }, + "data": { + "anyOf": [ + { + "$ref": "#/definitions/LastSubmittedData" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "LastSubmittedData": { + "type": "object", + "required": [ + "data", + "epoch_id", + "sender" + ], + "properties": { + "data": { + "$ref": "#/definitions/NodePerformance" + }, + "epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "sender": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + }, + "NodePerformance": { + "type": "object", + "required": [ + "n", + "p" + ], + "properties": { + "n": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "p": { + "$ref": "#/definitions/Percent" + } + }, + "additionalProperties": false + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } +} diff --git a/contracts/performance/schema/raw/response_to_network_monitor.json b/contracts/performance/schema/raw/response_to_network_monitor.json new file mode 100644 index 00000000000..eecd26abe33 --- /dev/null +++ b/contracts/performance/schema/raw/response_to_network_monitor.json @@ -0,0 +1,82 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NetworkMonitorResponse", + "type": "object", + "properties": { + "info": { + "anyOf": [ + { + "$ref": "#/definitions/NetworkMonitorInformation" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "NetworkMonitorDetails": { + "type": "object", + "required": [ + "address", + "authorised_at_height", + "authorised_by" + ], + "properties": { + "address": { + "$ref": "#/definitions/Addr" + }, + "authorised_at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "authorised_by": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + }, + "NetworkMonitorInformation": { + "type": "object", + "required": [ + "current_submission_metadata", + "details" + ], + "properties": { + "current_submission_metadata": { + "$ref": "#/definitions/NetworkMonitorSubmissionMetadata" + }, + "details": { + "$ref": "#/definitions/NetworkMonitorDetails" + } + }, + "additionalProperties": false + }, + "NetworkMonitorSubmissionMetadata": { + "type": "object", + "required": [ + "last_submitted_epoch_id", + "last_submitted_node_id" + ], + "properties": { + "last_submitted_epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "last_submitted_node_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } +} diff --git a/contracts/performance/schema/raw/response_to_network_monitors_paged.json b/contracts/performance/schema/raw/response_to_network_monitors_paged.json new file mode 100644 index 00000000000..a8119630720 --- /dev/null +++ b/contracts/performance/schema/raw/response_to_network_monitors_paged.json @@ -0,0 +1,87 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NetworkMonitorsPagedResponse", + "type": "object", + "required": [ + "info" + ], + "properties": { + "info": { + "type": "array", + "items": { + "$ref": "#/definitions/NetworkMonitorInformation" + } + }, + "start_next_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "NetworkMonitorDetails": { + "type": "object", + "required": [ + "address", + "authorised_at_height", + "authorised_by" + ], + "properties": { + "address": { + "$ref": "#/definitions/Addr" + }, + "authorised_at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "authorised_by": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + }, + "NetworkMonitorInformation": { + "type": "object", + "required": [ + "current_submission_metadata", + "details" + ], + "properties": { + "current_submission_metadata": { + "$ref": "#/definitions/NetworkMonitorSubmissionMetadata" + }, + "details": { + "$ref": "#/definitions/NetworkMonitorDetails" + } + }, + "additionalProperties": false + }, + "NetworkMonitorSubmissionMetadata": { + "type": "object", + "required": [ + "last_submitted_epoch_id", + "last_submitted_node_id" + ], + "properties": { + "last_submitted_epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "last_submitted_node_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } +} diff --git a/contracts/performance/schema/raw/response_to_node_measurements.json b/contracts/performance/schema/raw/response_to_node_measurements.json new file mode 100644 index 00000000000..e1add3c02df --- /dev/null +++ b/contracts/performance/schema/raw/response_to_node_measurements.json @@ -0,0 +1,38 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NodeMeasurementsResponse", + "type": "object", + "properties": { + "measurements": { + "anyOf": [ + { + "$ref": "#/definitions/NodeResults" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "NodeResults": { + "type": "array", + "items": { + "$ref": "#/definitions/Percent" + } + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + } +} diff --git a/contracts/performance/schema/raw/response_to_node_performance.json b/contracts/performance/schema/raw/response_to_node_performance.json new file mode 100644 index 00000000000..d9bae2e00d2 --- /dev/null +++ b/contracts/performance/schema/raw/response_to_node_performance.json @@ -0,0 +1,32 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NodePerformanceResponse", + "type": "object", + "properties": { + "performance": { + "anyOf": [ + { + "$ref": "#/definitions/Percent" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + } +} diff --git a/contracts/performance/schema/raw/response_to_node_performance_paged.json b/contracts/performance/schema/raw/response_to_node_performance_paged.json new file mode 100644 index 00000000000..f63c28f4775 --- /dev/null +++ b/contracts/performance/schema/raw/response_to_node_performance_paged.json @@ -0,0 +1,69 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NodePerformancePagedResponse", + "type": "object", + "required": [ + "node_id", + "performance" + ], + "properties": { + "node_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "performance": { + "type": "array", + "items": { + "$ref": "#/definitions/EpochNodePerformance" + } + }, + "start_next_after": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "EpochNodePerformance": { + "type": "object", + "required": [ + "epoch" + ], + "properties": { + "epoch": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "performance": { + "anyOf": [ + { + "$ref": "#/definitions/Percent" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + } +} diff --git a/contracts/performance/schema/raw/response_to_retired_network_monitors_paged.json b/contracts/performance/schema/raw/response_to_retired_network_monitors_paged.json new file mode 100644 index 00000000000..f5310e9438e --- /dev/null +++ b/contracts/performance/schema/raw/response_to_retired_network_monitors_paged.json @@ -0,0 +1,73 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "RetiredNetworkMonitorsPagedResponse", + "type": "object", + "required": [ + "info" + ], + "properties": { + "info": { + "type": "array", + "items": { + "$ref": "#/definitions/RetiredNetworkMonitor" + } + }, + "start_next_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "NetworkMonitorDetails": { + "type": "object", + "required": [ + "address", + "authorised_at_height", + "authorised_by" + ], + "properties": { + "address": { + "$ref": "#/definitions/Addr" + }, + "authorised_at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "authorised_by": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + }, + "RetiredNetworkMonitor": { + "type": "object", + "required": [ + "details", + "retired_at_height", + "retired_by" + ], + "properties": { + "details": { + "$ref": "#/definitions/NetworkMonitorDetails" + }, + "retired_at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "retired_by": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + } +} diff --git a/contracts/performance/src/contract.rs b/contracts/performance/src/contract.rs index 235cef391fc..2f488f39618 100644 --- a/contracts/performance/src/contract.rs +++ b/contracts/performance/src/contract.rs @@ -3,7 +3,7 @@ use crate::queries::{ query_admin, query_epoch_measurements_paged, query_epoch_performance_paged, - query_full_historical_performance_paged, query_network_monitor_details, + 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, }; @@ -57,10 +57,10 @@ pub fn execute( match msg { ExecuteMsg::UpdateAdmin { admin } => try_update_contract_admin(deps, info, admin), ExecuteMsg::Submit { epoch, data } => { - try_submit_performance_results(deps, info, epoch, data) + try_submit_performance_results(deps, env, info, epoch, data) } ExecuteMsg::BatchSubmit { epoch, data } => { - try_batch_submit_performance_results(deps, info, epoch, data) + try_batch_submit_performance_results(deps, env, info, epoch, data) } ExecuteMsg::AuthoriseNetworkMonitor { address } => { try_authorise_network_monitor(deps, env, info, address) @@ -129,6 +129,7 @@ pub fn query(deps: Deps, _: Env, msg: QueryMsg) -> Result Ok(to_json_binary(&query_last_submission(deps)?)?), } } diff --git a/contracts/performance/src/queries.rs b/contracts/performance/src/queries.rs index 3cc095481ac..5fe2c3ed91d 100644 --- a/contracts/performance/src/queries.rs +++ b/contracts/performance/src/queries.rs @@ -7,9 +7,9 @@ use cw_controllers::AdminResponse; use cw_storage_plus::Bound; use nym_performance_contract_common::{ EpochId, EpochMeasurementsPagedResponse, EpochNodePerformance, EpochPerformancePagedResponse, - FullHistoricalPerformancePagedResponse, HistoricalPerformance, NetworkMonitorInformation, - NetworkMonitorResponse, NetworkMonitorsPagedResponse, NodeId, NodeMeasurement, - NodeMeasurementsResponse, NodePerformance, NodePerformancePagedResponse, + FullHistoricalPerformancePagedResponse, HistoricalPerformance, LastSubmission, + NetworkMonitorInformation, NetworkMonitorResponse, NetworkMonitorsPagedResponse, NodeId, + NodeMeasurement, NodeMeasurementsResponse, NodePerformance, NodePerformancePagedResponse, NodePerformanceResponse, NymPerformanceContractError, RetiredNetworkMonitorsPagedResponse, }; @@ -305,11 +305,19 @@ pub fn query_retired_network_monitors_paged( }) } +pub fn query_last_submission(deps: Deps) -> Result { + NYM_PERFORMANCE_CONTRACT_STORAGE + .last_performance_submission + .load(deps.storage) + .map_err(Into::into) +} + #[cfg(test)] mod tests { use super::*; use crate::testing::{init_contract_tester, PerformanceContractTesterExt}; - use nym_contracts_common_testing::{ContractOpts, RandExt}; + use nym_contracts_common_testing::{ChainOpts, ContractOpts, RandExt}; + use nym_performance_contract_common::LastSubmittedData; #[cfg(test)] mod admin_query { @@ -585,4 +593,75 @@ mod tests { Ok(()) } + + #[test] + fn last_submission_query() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + + let env = test.env(); + + let id1 = test.bond_dummy_nymnode()?; + let id2 = test.bond_dummy_nymnode()?; + + // initial + let data = query_last_submission(test.deps())?; + assert_eq!( + data, + LastSubmission { + block_height: env.block.height, + block_time: env.block.time, + data: None, + } + ); + + let nm1 = test.generate_account(); + let nm2 = test.generate_account(); + test.authorise_network_monitor(&nm1)?; + test.authorise_network_monitor(&nm2)?; + test.set_mixnet_epoch(10)?; + + test.insert_raw_performance(&nm1, id1, "0.2")?; + + 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.2".parse()? + }, + }), + } + ); + + test.next_block(); + let env = test.env(); + + test.insert_epoch_performance(&nm2, 5, id2, "0.3".parse()?)?; + + // note that even though it's "earlier" data, last submission is still updated accordingly + 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: nm2.clone(), + epoch_id: 5, + data: NodePerformance { + node_id: id2, + performance: "0.3".parse()? + }, + }), + } + ); + + Ok(()) + } } diff --git a/contracts/performance/src/storage.rs b/contracts/performance/src/storage.rs index 0559352a28e..acba90762be 100644 --- a/contracts/performance/src/storage.rs +++ b/contracts/performance/src/storage.rs @@ -8,9 +8,9 @@ use cw_storage_plus::{Item, Map}; use nym_contracts_common::Percent; use nym_performance_contract_common::constants::storage_keys; use nym_performance_contract_common::{ - BatchSubmissionResult, EpochId, NetworkMonitorDetails, NetworkMonitorSubmissionMetadata, - NodeId, NodePerformance, NodeResults, NymPerformanceContractError, - RemoveEpochMeasurementsResponse, RetiredNetworkMonitor, + BatchSubmissionResult, EpochId, LastSubmission, LastSubmittedData, NetworkMonitorDetails, + NetworkMonitorSubmissionMetadata, NodeId, NodePerformance, NodeResults, + NymPerformanceContractError, RemoveEpochMeasurementsResponse, RetiredNetworkMonitor, }; pub const NYM_PERFORMANCE_CONTRACT_STORAGE: NymPerformanceContractStorage = @@ -19,6 +19,7 @@ pub const NYM_PERFORMANCE_CONTRACT_STORAGE: NymPerformanceContractStorage = pub struct NymPerformanceContractStorage { pub(crate) contract_admin: Admin, pub(crate) mixnet_epoch_id_at_creation: Item, + pub(crate) last_performance_submission: Item, pub(crate) mixnet_contract_address: Item, @@ -33,6 +34,7 @@ impl NymPerformanceContractStorage { NymPerformanceContractStorage { contract_admin: Admin::new(storage_keys::CONTRACT_ADMIN), mixnet_epoch_id_at_creation: Item::new(storage_keys::INITIAL_EPOCH_ID), + last_performance_submission: Item::new(storage_keys::LAST_SUBMISSION), mixnet_contract_address: Item::new(storage_keys::MIXNET_CONTRACT), network_monitors: NetworkMonitorsStorage::new(), performance_results: PerformanceResultsStorage::new(), @@ -77,6 +79,16 @@ impl NymPerformanceContractStorage { let initial_epoch_id = self.current_mixnet_epoch_id(deps.as_ref())?; + // set the last submission to the initial value + self.last_performance_submission.save( + deps.storage, + &LastSubmission { + block_height: env.block.height, + block_time: env.block.time, + data: None, + }, + )?; + // set the initial epoch id self.mixnet_epoch_id_at_creation .save(deps.storage, &initial_epoch_id)?; @@ -100,6 +112,7 @@ impl NymPerformanceContractStorage { pub fn submit_performance_data( &self, deps: DepsMut, + env: Env, sender: &Addr, epoch_id: EpochId, data: NodePerformance, @@ -135,12 +148,27 @@ impl NymPerformanceContractStorage { data.node_id, )?; + // 6. update latest submitted + self.last_performance_submission.save( + deps.storage, + &LastSubmission { + block_height: env.block.height, + block_time: env.block.time, + data: Some(LastSubmittedData { + sender: sender.clone(), + epoch_id, + data, + }), + }, + )?; + Ok(()) } pub fn batch_submit_performance_results( &self, deps: DepsMut, + env: Env, sender: &Addr, epoch_id: EpochId, data: Vec, @@ -208,6 +236,20 @@ impl NymPerformanceContractStorage { last.node_id, )?; + // 6. update latest submitted + self.last_performance_submission.save( + deps.storage, + &LastSubmission { + block_height: env.block.height, + block_time: env.block.time, + data: Some(LastSubmittedData { + sender: sender.clone(), + epoch_id, + data: *last, + }), + }, + )?; + Ok(BatchSubmissionResult { accepted_scores, non_existent_nodes, @@ -592,6 +634,25 @@ mod tests { Ok(()) } + #[test] + fn sets_initial_submission_data() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut pre_init = PreInitContract::new(); + + let env = pre_init.env(); + initialise_storage(&mut pre_init, None)?; + let deps = pre_init.deps(); + + let expected = LastSubmission { + block_height: env.block.height, + block_time: env.block.time, + data: None, + }; + let data = storage.last_performance_submission.load(deps.storage)?; + assert_eq!(expected, data); + Ok(()) + } + #[test] fn retrieves_initial_epoch_id_from_mixnet_contract() -> anyhow::Result<()> { // base case @@ -721,18 +782,19 @@ mod tests { let nm1 = tester.addr_make("network-monitor-1"); let nm2 = tester.addr_make("network-monitor-2"); let unauthorised = tester.addr_make("unauthorised"); + let env = tester.env(); tester.authorise_network_monitor(&nm1)?; // authorised network monitor can submit the results just fine let perf = tester.dummy_node_performance(); assert!(storage - .submit_performance_data(tester.deps_mut(), &nm1, 0, perf) + .submit_performance_data(tester.deps_mut(), env.clone(), &nm1, 0, perf) .is_ok()); // unauthorised address is rejected let res = storage - .submit_performance_data(tester.deps_mut(), &nm2, 0, perf) + .submit_performance_data(tester.deps_mut(), env.clone(), &nm2, 0, perf) .unwrap_err(); assert_eq!( res, @@ -744,12 +806,12 @@ mod tests { // it is fine after explicit authorisation though tester.authorise_network_monitor(&nm2)?; assert!(storage - .submit_performance_data(tester.deps_mut(), &nm2, 0, perf) + .submit_performance_data(tester.deps_mut(), env.clone(), &nm2, 0, perf) .is_ok()); // and address that was never authorised still fails let res = storage - .submit_performance_data(tester.deps_mut(), &unauthorised, 0, perf) + .submit_performance_data(tester.deps_mut(), env.clone(), &unauthorised, 0, perf) .unwrap_err(); assert_eq!( res, @@ -764,6 +826,7 @@ mod tests { fn its_not_possible_to_submit_data_for_same_node_again() -> anyhow::Result<()> { let storage = NymPerformanceContractStorage::new(); let mut tester = init_contract_tester(); + let env = tester.env(); let nm = tester.addr_make("network-monitor"); tester.authorise_network_monitor(&nm)?; @@ -781,12 +844,12 @@ mod tests { // first submission assert!(storage - .submit_performance_data(tester.deps_mut(), &nm, 0, data) + .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 0, data) .is_ok()); // second submission let res = storage - .submit_performance_data(tester.deps_mut(), &nm, 0, data) + .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 0, data) .unwrap_err(); assert_eq!( @@ -801,16 +864,16 @@ mod tests { // another submission works fine assert!(storage - .submit_performance_data(tester.deps_mut(), &nm, 0, another_data) + .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(), &nm, 1, data) + .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 1, data) .is_ok()); let res = storage - .submit_performance_data(tester.deps_mut(), &nm, 0, data) + .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 0, data) .unwrap_err(); assert_eq!( @@ -832,6 +895,7 @@ mod tests { let mut tester = init_contract_tester(); let nm = tester.addr_make("network-monitor"); tester.authorise_network_monitor(&nm)?; + let env = tester.env(); let id1 = tester.bond_dummy_nymnode()?; let id2 = tester.bond_dummy_nymnode()?; @@ -845,11 +909,11 @@ mod tests { }; assert!(storage - .submit_performance_data(tester.deps_mut(), &nm, 0, another_data) + .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 0, another_data) .is_ok()); let res = storage - .submit_performance_data(tester.deps_mut(), &nm, 0, data) + .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 0, data) .unwrap_err(); assert_eq!( @@ -864,11 +928,11 @@ mod tests { // check across epochs assert!(storage - .submit_performance_data(tester.deps_mut(), &nm, 10, data) + .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 10, data) .is_ok()); let res = storage - .submit_performance_data(tester.deps_mut(), &nm, 9, data) + .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 9, data) .unwrap_err(); assert_eq!( @@ -891,11 +955,12 @@ mod tests { let nm = tester.addr_make("network-monitor"); tester.authorise_network_monitor(&nm)?; + 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(), &nm, 0, perf) + .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 0, perf) .unwrap_err(); assert_eq!( @@ -909,7 +974,7 @@ mod tests { ); let res = storage - .submit_performance_data(tester.deps_mut(), &nm, 9, perf) + .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 9, perf) .unwrap_err(); assert_eq!( @@ -923,10 +988,10 @@ mod tests { ); assert!(storage - .submit_performance_data(tester.deps_mut(), &nm, 10, perf) + .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 10, perf) .is_ok()); assert!(storage - .submit_performance_data(tester.deps_mut(), &nm, 11, perf) + .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 11, perf) .is_ok()); Ok(()) @@ -936,6 +1001,7 @@ mod tests { fn updates_submission_metadata() -> anyhow::Result<()> { let storage = NymPerformanceContractStorage::new(); let mut tester = init_contract_tester(); + let env = tester.env(); let mut nodes = Vec::new(); for _ in 0..10 { @@ -953,6 +1019,7 @@ mod tests { storage.submit_performance_data( tester.deps_mut(), + env.clone(), &nm, 0, NodePerformance { @@ -969,6 +1036,7 @@ mod tests { storage.submit_performance_data( tester.deps_mut(), + env.clone(), &nm, 0, NodePerformance { @@ -985,6 +1053,7 @@ mod tests { storage.submit_performance_data( tester.deps_mut(), + env.clone(), &nm, 1, NodePerformance { @@ -1001,6 +1070,7 @@ mod tests { storage.submit_performance_data( tester.deps_mut(), + env.clone(), &nm, 12345, NodePerformance { @@ -1018,10 +1088,136 @@ mod tests { Ok(()) } + #[test] + fn updates_latest_submitted_information() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + let env = tester.env(); + + let nm = tester.addr_make("network-monitor"); + tester.authorise_network_monitor(&nm)?; + + let mut nodes = Vec::new(); + for _ in 0..10 { + nodes.push(tester.bond_dummy_nymnode()?); + } + + storage.submit_performance_data( + tester.deps_mut(), + env.clone(), + &nm, + 0, + NodePerformance { + node_id: nodes[0], + performance: Default::default(), + }, + )?; + let data = storage.last_performance_submission.load(&tester)?; + assert_eq!( + data, + LastSubmission { + block_height: env.block.height, + block_time: env.block.time, + data: Some(LastSubmittedData { + sender: nm.clone(), + epoch_id: 0, + data: NodePerformance { + node_id: nodes[0], + performance: Default::default(), + }, + }), + } + ); + + storage.submit_performance_data( + tester.deps_mut(), + env.clone(), + &nm, + 0, + NodePerformance { + node_id: nodes[6], + performance: Default::default(), + }, + )?; + let data = storage.last_performance_submission.load(&tester)?; + assert_eq!( + data, + LastSubmission { + block_height: env.block.height, + block_time: env.block.time, + data: Some(LastSubmittedData { + sender: nm.clone(), + epoch_id: 0, + data: NodePerformance { + node_id: nodes[6], + performance: Default::default(), + }, + }), + } + ); + + storage.submit_performance_data( + tester.deps_mut(), + env.clone(), + &nm, + 1, + NodePerformance { + node_id: nodes[2], + performance: Default::default(), + }, + )?; + let data = storage.last_performance_submission.load(&tester)?; + assert_eq!( + data, + LastSubmission { + block_height: env.block.height, + block_time: env.block.time, + data: Some(LastSubmittedData { + sender: nm.clone(), + epoch_id: 1, + data: NodePerformance { + node_id: nodes[2], + performance: Default::default(), + }, + }), + } + ); + + storage.submit_performance_data( + tester.deps_mut(), + env.clone(), + &nm, + 12345, + NodePerformance { + node_id: nodes[9], + performance: Default::default(), + }, + )?; + let data = storage.last_performance_submission.load(&tester)?; + assert_eq!( + data, + LastSubmission { + block_height: env.block.height, + block_time: env.block.time, + data: Some(LastSubmittedData { + sender: nm.clone(), + epoch_id: 12345, + data: NodePerformance { + node_id: nodes[9], + performance: Default::default(), + }, + }), + } + ); + + Ok(()) + } + #[test] fn requires_associated_node_to_be_bonded() -> anyhow::Result<()> { let storage = NymPerformanceContractStorage::new(); let mut tester = init_contract_tester(); + let env = tester.env(); let nm = tester.addr_make("network-monitor"); tester.authorise_network_monitor(&nm)?; @@ -1033,7 +1229,7 @@ mod tests { // no node bonded at this point let res = storage - .submit_performance_data(tester.deps_mut(), &nm, 0, dummy_perf) + .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 0, dummy_perf) .unwrap_err(); assert_eq!( res, @@ -1048,14 +1244,15 @@ mod tests { node_id, performance: Default::default(), }; - let res = storage.submit_performance_data(tester.deps_mut(), &nm, 0, perf); + let res = + storage.submit_performance_data(tester.deps_mut(), env.clone(), &nm, 0, perf); assert!(res.is_ok()); // unbonded tester.unbond_nymnode(node_id)?; let res = storage - .submit_performance_data(tester.deps_mut(), &nm, 0, dummy_perf) + .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 0, dummy_perf) .unwrap_err(); assert_eq!( res, @@ -1070,14 +1267,15 @@ mod tests { node_id, performance: Default::default(), }; - let res = storage.submit_performance_data(tester.deps_mut(), &nm, 0, perf); + let res = + storage.submit_performance_data(tester.deps_mut(), env.clone(), &nm, 0, perf); assert!(res.is_ok()); // unbonded tester.unbond_legacy_mixnode(node_id)?; let res = storage - .submit_performance_data(tester.deps_mut(), &nm, 0, dummy_perf) + .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 0, dummy_perf) .unwrap_err(); assert_eq!( res, @@ -1100,18 +1298,31 @@ mod tests { let nm1 = tester.addr_make("network-monitor-1"); let nm2 = tester.addr_make("network-monitor-2"); let unauthorised = tester.addr_make("unauthorised"); + let env = tester.env(); tester.authorise_network_monitor(&nm1)?; let perf = tester.dummy_node_performance(); // authorised network monitor can submit the results just fine assert!(storage - .batch_submit_performance_results(tester.deps_mut(), &nm1, 0, vec![perf]) + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm1, + 0, + vec![perf] + ) .is_ok()); // unauthorised address is rejected let res = storage - .batch_submit_performance_results(tester.deps_mut(), &nm2, 0, vec![perf]) + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm2, + 0, + vec![perf], + ) .unwrap_err(); assert_eq!( res, @@ -1123,13 +1334,20 @@ mod tests { // it is fine after explicit authorisation though tester.authorise_network_monitor(&nm2)?; assert!(storage - .batch_submit_performance_results(tester.deps_mut(), &nm2, 0, vec![perf]) + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm2, + 0, + vec![perf] + ) .is_ok()); // and address that was never authorised still fails let res = storage .batch_submit_performance_results( tester.deps_mut(), + env.clone(), &unauthorised, 0, vec![perf], @@ -1150,6 +1368,7 @@ mod tests { let mut tester = init_contract_tester(); let nm = tester.addr_make("network-monitor"); tester.authorise_network_monitor(&nm)?; + let env = tester.env(); let id1 = tester.bond_dummy_nymnode()?; let id2 = tester.bond_dummy_nymnode()?; @@ -1174,27 +1393,57 @@ mod tests { let sorted = vec![data, another_data, more_data]; let res = storage - .batch_submit_performance_results(tester.deps_mut(), &nm, 0, duplicates) + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 0, + duplicates, + ) .unwrap_err(); assert_eq!(res, NymPerformanceContractError::UnsortedBatchSubmission); let res = storage - .batch_submit_performance_results(tester.deps_mut(), &nm, 0, another_dups) + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 0, + another_dups, + ) .unwrap_err(); assert_eq!(res, NymPerformanceContractError::UnsortedBatchSubmission); let res = storage - .batch_submit_performance_results(tester.deps_mut(), &nm, 0, unsorted) + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 0, + unsorted, + ) .unwrap_err(); assert_eq!(res, NymPerformanceContractError::UnsortedBatchSubmission); let res = storage - .batch_submit_performance_results(tester.deps_mut(), &nm, 0, semi_sorted) + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 0, + semi_sorted, + ) .unwrap_err(); assert_eq!(res, NymPerformanceContractError::UnsortedBatchSubmission); assert!(storage - .batch_submit_performance_results(tester.deps_mut(), &nm, 0, sorted) + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 0, + sorted + ) .is_ok()); Ok(()) } @@ -1205,6 +1454,7 @@ mod tests { let mut tester = init_contract_tester(); let nm = tester.addr_make("network-monitor"); tester.authorise_network_monitor(&nm)?; + let env = tester.env(); let id1 = tester.bond_dummy_nymnode()?; let id2 = tester.bond_dummy_nymnode()?; @@ -1219,12 +1469,24 @@ mod tests { // first submission assert!(storage - .batch_submit_performance_results(tester.deps_mut(), &nm, 0, vec![data]) + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 0, + vec![data] + ) .is_ok()); // second submission let res = storage - .batch_submit_performance_results(tester.deps_mut(), &nm, 0, vec![data]) + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 0, + vec![data], + ) .unwrap_err(); assert_eq!( @@ -1239,16 +1501,34 @@ mod tests { // another submission works fine assert!(storage - .batch_submit_performance_results(tester.deps_mut(), &nm, 0, vec![another_data]) + .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(), &nm, 1, vec![data]) + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 1, + vec![data] + ) .is_ok()); let res = storage - .batch_submit_performance_results(tester.deps_mut(), &nm, 0, vec![data]) + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 0, + vec![data], + ) .unwrap_err(); assert_eq!( @@ -1268,6 +1548,7 @@ mod tests { fn its_not_possible_to_submit_data_out_of_order() -> anyhow::Result<()> { let storage = NymPerformanceContractStorage::new(); let mut tester = init_contract_tester(); + let env = tester.env(); let nm = tester.addr_make("network-monitor"); tester.authorise_network_monitor(&nm)?; @@ -1283,11 +1564,23 @@ mod tests { }; assert!(storage - .batch_submit_performance_results(tester.deps_mut(), &nm, 0, vec![another_data]) + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 0, + vec![another_data] + ) .is_ok()); let res = storage - .batch_submit_performance_results(tester.deps_mut(), &nm, 0, vec![data]) + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 0, + vec![data], + ) .unwrap_err(); assert_eq!( @@ -1302,11 +1595,23 @@ mod tests { // check across epochs assert!(storage - .batch_submit_performance_results(tester.deps_mut(), &nm, 10, vec![data]) + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 10, + vec![data] + ) .is_ok()); let res = storage - .batch_submit_performance_results(tester.deps_mut(), &nm, 9, vec![data]) + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 9, + vec![data], + ) .unwrap_err(); assert_eq!( @@ -1325,8 +1630,9 @@ mod tests { fn its_not_possible_to_submit_data_for_past_epochs() -> anyhow::Result<()> { let storage = NymPerformanceContractStorage::new(); let mut tester = init_contract_tester(); - tester.set_mixnet_epoch(10)?; + let env = tester.env(); + tester.set_mixnet_epoch(10)?; let nm = tester.addr_make("network-monitor"); tester.authorise_network_monitor(&nm)?; @@ -1334,7 +1640,13 @@ mod tests { // if NM got authorised at epoch 10, it can only submit data for epochs >=10 let res = storage - .batch_submit_performance_results(tester.deps_mut(), &nm, 0, vec![perf]) + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 0, + vec![perf], + ) .unwrap_err(); assert_eq!( @@ -1348,7 +1660,13 @@ mod tests { ); let res = storage - .batch_submit_performance_results(tester.deps_mut(), &nm, 9, vec![perf]) + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 9, + vec![perf], + ) .unwrap_err(); assert_eq!( @@ -1362,10 +1680,22 @@ mod tests { ); assert!(storage - .batch_submit_performance_results(tester.deps_mut(), &nm, 10, vec![perf]) + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 10, + vec![perf] + ) .is_ok()); assert!(storage - .batch_submit_performance_results(tester.deps_mut(), &nm, 11, vec![perf]) + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 11, + vec![perf] + ) .is_ok()); Ok(()) @@ -1375,6 +1705,7 @@ mod tests { fn updates_submission_metadata() -> anyhow::Result<()> { let storage = NymPerformanceContractStorage::new(); let mut tester = init_contract_tester(); + let env = tester.env(); let nm = tester.addr_make("network-monitor"); tester.authorise_network_monitor(&nm)?; @@ -1393,6 +1724,7 @@ mod tests { // single submission storage.batch_submit_performance_results( tester.deps_mut(), + env.clone(), &nm, 0, vec![NodePerformance { @@ -1410,6 +1742,7 @@ mod tests { // another epoch storage.batch_submit_performance_results( tester.deps_mut(), + env.clone(), &nm, 1, vec![NodePerformance { @@ -1427,6 +1760,7 @@ mod tests { // multiple submissions storage.batch_submit_performance_results( tester.deps_mut(), + env.clone(), &nm, 1, vec![ @@ -1454,6 +1788,7 @@ mod tests { // another epoch storage.batch_submit_performance_results( tester.deps_mut(), + env.clone(), &nm, 2, vec![ @@ -1481,6 +1816,155 @@ mod tests { Ok(()) } + #[test] + fn updates_latest_submitted_information() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + let env = tester.env(); + + let nm = tester.addr_make("network-monitor"); + tester.authorise_network_monitor(&nm)?; + + let mut nodes = Vec::new(); + for _ in 0..10 { + nodes.push(tester.bond_dummy_nymnode()?); + } + + // single submission + storage.batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 0, + vec![NodePerformance { + node_id: nodes[0], + performance: Default::default(), + }], + )?; + let data = storage.last_performance_submission.load(&tester)?; + assert_eq!( + data, + LastSubmission { + block_height: env.block.height, + block_time: env.block.time, + data: Some(LastSubmittedData { + sender: nm.clone(), + epoch_id: 0, + data: NodePerformance { + node_id: nodes[0], + performance: Default::default(), + }, + }), + } + ); + + // another epoch + storage.batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 1, + vec![NodePerformance { + node_id: nodes[1], + performance: Default::default(), + }], + )?; + let data = storage.last_performance_submission.load(&tester)?; + assert_eq!( + data, + LastSubmission { + block_height: env.block.height, + block_time: env.block.time, + data: Some(LastSubmittedData { + sender: nm.clone(), + epoch_id: 1, + data: NodePerformance { + node_id: nodes[1], + performance: Default::default(), + }, + }), + } + ); + + // multiple submissions + storage.batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 1, + vec![ + NodePerformance { + node_id: nodes[2], + performance: Default::default(), + }, + NodePerformance { + node_id: nodes[3], + performance: Default::default(), + }, + NodePerformance { + node_id: nodes[4], + performance: Default::default(), + }, + ], + )?; + let data = storage.last_performance_submission.load(&tester)?; + assert_eq!( + data, + LastSubmission { + block_height: env.block.height, + block_time: env.block.time, + data: Some(LastSubmittedData { + sender: nm.clone(), + epoch_id: 1, + data: NodePerformance { + node_id: nodes[4], + performance: Default::default(), + }, + }), + } + ); + + // another epoch + storage.batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 2, + vec![ + NodePerformance { + node_id: nodes[1], + performance: Default::default(), + }, + NodePerformance { + node_id: nodes[7], + performance: Default::default(), + }, + NodePerformance { + node_id: nodes[8], + performance: Default::default(), + }, + ], + )?; + let data = storage.last_performance_submission.load(&tester)?; + assert_eq!( + data, + LastSubmission { + block_height: env.block.height, + block_time: env.block.time, + data: Some(LastSubmittedData { + sender: nm.clone(), + epoch_id: 2, + data: NodePerformance { + node_id: nodes[8], + performance: Default::default(), + }, + }), + } + ); + + Ok(()) + } + #[test] fn informs_if_associated_node_is_not_bonded() -> anyhow::Result<()> { let storage = NymPerformanceContractStorage::new(); @@ -1496,22 +1980,21 @@ mod tests { } let nym_node1 = tester.bond_dummy_nymnode()?; - let nym_node_between = tester.bond_dummy_nymnode()?; tester.unbond_nymnode(nym_node_between)?; - let nym_node2 = tester.bond_dummy_nymnode()?; let mix_node1 = tester.bond_dummy_legacy_mixnode()?; - let mixnode_between = tester.bond_dummy_legacy_mixnode()?; tester.unbond_legacy_mixnode(mixnode_between)?; - let mix_node2 = tester.bond_dummy_legacy_mixnode()?; + let env = tester.env(); + // single id - nothing bonded let res = storage.batch_submit_performance_results( tester.deps_mut(), + env.clone(), &nm, 0, vec![NodePerformance { @@ -1525,6 +2008,7 @@ mod tests { // one bonded nym-node, one not bonded let res = storage.batch_submit_performance_results( tester.deps_mut(), + env.clone(), &nm, 1, vec![ @@ -1544,6 +2028,7 @@ mod tests { // not-bonded, bonded, not-bonded, bonded let res = storage.batch_submit_performance_results( tester.deps_mut(), + env.clone(), &nm, 2, vec![ @@ -1573,6 +2058,7 @@ mod tests { // one bonded mixnode, one not bonded let res = storage.batch_submit_performance_results( tester.deps_mut(), + env.clone(), &nm, 3, vec![ @@ -1592,6 +2078,7 @@ mod tests { // not-bonded, bonded, not-bonded, bonded let res = storage.batch_submit_performance_results( tester.deps_mut(), + env.clone(), &nm, 4, vec![ @@ -1619,6 +2106,7 @@ mod tests { // nym-node, not bonded, mixnode let res = storage.batch_submit_performance_results( tester.deps_mut(), + env.clone(), &nm, 5, vec![ diff --git a/contracts/performance/src/testing/mod.rs b/contracts/performance/src/testing/mod.rs index c1295d38a5f..4f8e7cf4bd4 100644 --- a/contracts/performance/src/testing/mod.rs +++ b/contracts/performance/src/testing/mod.rs @@ -386,8 +386,10 @@ pub(crate) trait PerformanceContractTesterExt: node_id: NodeId, performance: Percent, ) -> Result<(), NymPerformanceContractError> { + let env = self.env(); NYM_PERFORMANCE_CONTRACT_STORAGE.submit_performance_data( self.deps_mut(), + env, addr, epoch_id, NodePerformance { diff --git a/contracts/performance/src/transactions.rs b/contracts/performance/src/transactions.rs index 6385661fa37..86a2d997721 100644 --- a/contracts/performance/src/transactions.rs +++ b/contracts/performance/src/transactions.rs @@ -23,11 +23,18 @@ pub fn try_update_contract_admin( pub fn try_submit_performance_results( deps: DepsMut<'_>, + env: Env, info: MessageInfo, epoch_id: EpochId, data: NodePerformance, ) -> Result { - NYM_PERFORMANCE_CONTRACT_STORAGE.submit_performance_data(deps, &info.sender, epoch_id, data)?; + NYM_PERFORMANCE_CONTRACT_STORAGE.submit_performance_data( + deps, + env, + &info.sender, + epoch_id, + data, + )?; // TODO: emit events Ok(Response::new()) @@ -35,12 +42,14 @@ pub fn try_submit_performance_results( pub fn try_batch_submit_performance_results( deps: DepsMut<'_>, + env: Env, info: MessageInfo, epoch_id: EpochId, data: Vec, ) -> Result { let res = NYM_PERFORMANCE_CONTRACT_STORAGE.batch_submit_performance_results( deps, + env, &info.sender, epoch_id, data, diff --git a/lefthook.yml b/lefthook.yml index 11b130f92a2..0687e789723 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -47,3 +47,7 @@ pre-commit: glob: "*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc,css}" run: yarn biome check --write --no-errors-on-unmatched --files-ignore-unknown=true --colors=off {staged_files} stage_fixed: true + rust-lint: + glob: "*.rs" + run: cargo fmt + stage_fixed: true diff --git a/nym-api/nym-api-requests/src/models.rs b/nym-api/nym-api-requests/src/models.rs index a9c5c2ccd46..3c23c4b106e 100644 --- a/nym-api/nym-api-requests/src/models.rs +++ b/nym-api/nym-api-requests/src/models.rs @@ -267,6 +267,10 @@ impl RoutingScore { Self { score } } + pub const fn zero() -> RoutingScore { + RoutingScore { score: 0.0 } + } + pub fn legacy_performance(&self) -> Performance { Performance::naive_try_from_f64(self.score).unwrap_or_default() } diff --git a/nym-api/src/circulating_supply_api/cache/data.rs b/nym-api/src/circulating_supply_api/cache/data.rs deleted file mode 100644 index 77ddbaaee27..00000000000 --- a/nym-api/src/circulating_supply_api/cache/data.rs +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2022-2023 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::support::caching::Cache; -use nym_api_requests::models::CirculatingSupplyResponse; -use nym_validator_client::nyxd::Coin; - -pub(crate) struct CirculatingSupplyCacheData { - // no need to cache that one as it's constant, but let's put it here for consistency sake - pub(crate) total_supply: Coin, - pub(crate) mixmining_reserve: Cache, - pub(crate) vesting_tokens: Cache, - pub(crate) circulating_supply: Cache, -} - -impl CirculatingSupplyCacheData { - pub fn new(mix_denom: String) -> CirculatingSupplyCacheData { - let zero_coin = Coin::new(0, &mix_denom); - - CirculatingSupplyCacheData { - total_supply: Coin::new(1_000_000_000_000_000, mix_denom), - mixmining_reserve: Cache::new(zero_coin.clone()), - vesting_tokens: Cache::new(zero_coin.clone()), - circulating_supply: Cache::new(zero_coin), - } - } -} - -impl<'a> From<&'a CirculatingSupplyCacheData> for CirculatingSupplyResponse { - fn from(value: &'a CirculatingSupplyCacheData) -> Self { - CirculatingSupplyResponse { - total_supply: value.total_supply.clone().into(), - mixmining_reserve: value.mixmining_reserve.clone().into(), - vesting_tokens: value.vesting_tokens.clone().into(), - circulating_supply: value.circulating_supply.clone().into(), - } - } -} diff --git a/nym-api/src/circulating_supply_api/cache/mod.rs b/nym-api/src/circulating_supply_api/cache/mod.rs deleted file mode 100644 index 87e401da9d6..00000000000 --- a/nym-api/src/circulating_supply_api/cache/mod.rs +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright 2022-2023 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use self::data::CirculatingSupplyCacheData; -use cosmwasm_std::Addr; -use nym_api_requests::models::CirculatingSupplyResponse; -use nym_validator_client::nyxd::error::NyxdError; -use nym_validator_client::nyxd::Coin; -use std::ops::Deref; -use std::{ - sync::{atomic::AtomicBool, Arc}, - time::Duration, -}; -use thiserror::Error; -use tokio::sync::RwLock; -use tokio::time; -use tracing::{error, info}; - -mod data; -pub(crate) mod refresher; - -#[derive(Debug, Error)] -enum CirculatingSupplyCacheError { - // this can only happen if somebody decides to set their staking address - // before https://github.com/nymtech/nym/pull/2796 is deployed - #[error("vesting account owned by {owner} with id {account_id} appeared more than once in the query response")] - DuplicateVestingAccountEntry { owner: Addr, account_id: u32 }, - - // this can happen if somehow the query was incomplete, like some paged sub-query didn't return full result - // or there's a bug with paging. or if, somehow, a vesting account got removed from the contract - #[error("got an inconsistent number of vesting account. received data on {got}, but expected {expected}")] - InconsistentNumberOfVestingAccounts { expected: usize, got: usize }, - - #[error(transparent)] - ClientError { - #[from] - source: NyxdError, - }, -} - -/// A cache for the circulating supply of the network. Circulating supply is calculated by -/// taking the initial supply of 1bn coins, and subtracting the amount of coins that are -/// in the mixmining pool and tied up in vesting. -/// -/// The cache is quite simple and does not include an update listener that the other caches have. -#[derive(Clone)] -pub(crate) struct CirculatingSupplyCache { - initialised: Arc, - data: Arc>, -} - -impl CirculatingSupplyCache { - pub(crate) fn new(mix_denom: String) -> CirculatingSupplyCache { - CirculatingSupplyCache { - initialised: Arc::new(AtomicBool::new(false)), - data: Arc::new(RwLock::new(CirculatingSupplyCacheData::new(mix_denom))), - } - } - - pub(crate) async fn get_circulating_supply(&self) -> Option { - match time::timeout(Duration::from_millis(100), self.data.read()).await { - Ok(cache) => Some(cache.deref().into()), - Err(err) => { - error!("Failed to get circulating supply: {err}"); - None - } - } - } - - pub(crate) async fn update(&self, mixmining_reserve: Coin, vesting_tokens: Coin) { - let mut cache = self.data.write().await; - - let mut circulating_supply = cache.total_supply.clone(); - circulating_supply.amount -= mixmining_reserve.amount; - circulating_supply.amount -= vesting_tokens.amount; - - info!("Updating circulating supply cache"); - info!("the mixmining reserve is now {mixmining_reserve}"); - info!("the number of tokens still vesting is now {vesting_tokens}"); - info!("the circulating supply is now {circulating_supply}"); - - cache.mixmining_reserve.unchecked_update(mixmining_reserve); - cache.vesting_tokens.unchecked_update(vesting_tokens); - cache - .circulating_supply - .unchecked_update(circulating_supply); - } -} diff --git a/nym-api/src/circulating_supply_api/cache/refresher.rs b/nym-api/src/circulating_supply_api/cache/refresher.rs deleted file mode 100644 index 60d7c2bafcd..00000000000 --- a/nym-api/src/circulating_supply_api/cache/refresher.rs +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright 2022-2023 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use super::CirculatingSupplyCache; -use crate::circulating_supply_api::cache::CirculatingSupplyCacheError; -use crate::support::nyxd::Client; -use nym_contracts_common::truncate_decimal; -use nym_task::TaskClient; -use nym_validator_client::nyxd::Coin; -use std::collections::HashSet; -use std::sync::atomic::Ordering; -use std::time::Duration; -use tokio::time; -use tracing::{error, trace}; - -pub(crate) struct CirculatingSupplyCacheRefresher { - nyxd_client: Client, - cache: CirculatingSupplyCache, - caching_interval: Duration, -} - -impl CirculatingSupplyCacheRefresher { - pub(crate) fn new( - nyxd_client: Client, - cache: CirculatingSupplyCache, - caching_interval: Duration, - ) -> Self { - CirculatingSupplyCacheRefresher { - nyxd_client, - cache, - caching_interval, - } - } - - pub(crate) async fn run(&self, mut shutdown: TaskClient) { - let mut interval = time::interval(self.caching_interval); - while !shutdown.is_shutdown() { - tokio::select! { - _ = interval.tick() => { - tokio::select! { - biased; - _ = shutdown.recv() => { - trace!("CirculatingSupplyCacheRefresher: Received shutdown"); - } - ret = self.refresh() => { - if let Err(err) = ret { - error!("Failed to refresh circulating supply cache - {err}"); - } else { - // relaxed memory ordering is fine here. worst case scenario network monitor - // will just have to wait for an additional backoff to see the change. - // And so this will not really incur any performance penalties by setting it every loop iteration - self.cache.initialised.store(true, Ordering::Relaxed) - } - } - } - } - _ = shutdown.recv() => { - trace!("CirculatingSupplyCacheRefresher: Received shutdown"); - } - } - } - } - - async fn get_mixmining_reserve( - &self, - mix_denom: &str, - ) -> Result { - let reward_pool = self - .nyxd_client - .get_current_rewarding_parameters() - .await? - .interval - .reward_pool; - - Ok(Coin::new(truncate_decimal(reward_pool).u128(), mix_denom)) - } - - async fn get_total_vesting_tokens( - &self, - mix_denom: &str, - ) -> Result { - let all_vesting = self.nyxd_client.get_all_vesting_coins().await?; - - // sanity check invariants to make sure all accounts got considered and we got no duplicates - // the cache refreshes so infrequently that the performance penalty is negligible - let mut owners = HashSet::new(); - let mut ids = HashSet::new(); - for acc in &all_vesting { - if !owners.insert(acc.owner.clone()) { - return Err(CirculatingSupplyCacheError::DuplicateVestingAccountEntry { - owner: acc.owner.clone(), - account_id: acc.account_id, - }); - } - - if !ids.insert(acc.account_id) { - return Err(CirculatingSupplyCacheError::DuplicateVestingAccountEntry { - owner: acc.owner.clone(), - account_id: acc.account_id, - }); - } - } - - let current_storage_key = self - .nyxd_client - .get_current_vesting_account_storage_key() - .await?; - if all_vesting.len() != current_storage_key as usize { - return Err( - CirculatingSupplyCacheError::InconsistentNumberOfVestingAccounts { - expected: current_storage_key as usize, - got: all_vesting.len(), - }, - ); - } - - let mut total = Coin::new(0, mix_denom); - for account in all_vesting { - total.amount += account.still_vesting.amount.u128(); - } - - Ok(total) - } - - async fn refresh(&self) -> Result<(), CirculatingSupplyCacheError> { - let chain_details = self.nyxd_client.chain_details().await; - let mix_denom = &chain_details.mix_denom.base; - - let mixmining_reserve = self.get_mixmining_reserve(mix_denom).await?; - let vesting_tokens = self.get_total_vesting_tokens(mix_denom).await?; - - self.cache.update(mixmining_reserve, vesting_tokens).await; - Ok(()) - } -} diff --git a/nym-api/src/circulating_supply_api/handlers.rs b/nym-api/src/circulating_supply_api/handlers.rs index afc04c0b123..3299afa7f10 100644 --- a/nym-api/src/circulating_supply_api/handlers.rs +++ b/nym-api/src/circulating_supply_api/handlers.rs @@ -1,10 +1,11 @@ // Copyright 2022-2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use crate::mixnet_contract_cache::cache::MixnetContractCache; use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; use crate::support::http::state::AppState; -use axum::extract::Query; -use axum::{extract, Router}; +use axum::extract::{Query, State}; +use axum::Router; use nym_api_requests::models::CirculatingSupplyResponse; use nym_http_api_common::{FormattedResponse, OutputParams}; use nym_validator_client::nyxd::Coin; @@ -34,15 +35,11 @@ pub(crate) fn circulating_supply_routes() -> Router { )] async fn get_full_circulating_supply( Query(output): Query, - extract::State(state): extract::State, + State(contract_cache): State, ) -> AxumResult> { let output = output.output.unwrap_or_default(); - match state - .circulating_supply_cache() - .get_circulating_supply() - .await - { + match contract_cache.get_circulating_supply().await { Some(value) => Ok(output.to_response(value)), None => Err(AxumErrorResponse::internal_msg("unavailable")), } @@ -63,14 +60,10 @@ async fn get_full_circulating_supply( )] async fn get_total_supply( Query(output): Query, - extract::State(state): extract::State, + State(contract_cache): State, ) -> AxumResult> { let output = output.output.unwrap_or_default(); - let full_circulating_supply = match state - .circulating_supply_cache() - .get_circulating_supply() - .await - { + let full_circulating_supply = match contract_cache.get_circulating_supply().await { Some(res) => res, None => return Err(AxumErrorResponse::internal_msg("unavailable")), }; @@ -95,15 +88,11 @@ async fn get_total_supply( )] async fn get_circulating_supply( Query(output): Query, - extract::State(state): extract::State, + State(contract_cache): State, ) -> AxumResult> { let output = output.output.unwrap_or_default(); - let full_circulating_supply = match state - .circulating_supply_cache() - .get_circulating_supply() - .await - { + let full_circulating_supply = match contract_cache.get_circulating_supply().await { Some(res) => res, None => return Err(AxumErrorResponse::internal_msg("unavailable")), }; diff --git a/nym-api/src/circulating_supply_api/mod.rs b/nym-api/src/circulating_supply_api/mod.rs index 3253ccaaab2..d1a19b2932e 100644 --- a/nym-api/src/circulating_supply_api/mod.rs +++ b/nym-api/src/circulating_supply_api/mod.rs @@ -1,27 +1,4 @@ // Copyright 2022-2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use self::cache::refresher::CirculatingSupplyCacheRefresher; -use crate::support::{config, nyxd}; -use nym_task::TaskManager; - -pub(crate) mod cache; pub(crate) mod handlers; - -/// Spawn the circulating supply cache refresher. -pub(crate) fn start_cache_refresh( - config: &config::CirculatingSupplyCacher, - nyxd_client: nyxd::Client, - circulating_supply_cache: &cache::CirculatingSupplyCache, - shutdown: &TaskManager, -) { - if config.enabled { - let refresher = CirculatingSupplyCacheRefresher::new( - nyxd_client, - circulating_supply_cache.to_owned(), - config.debug.caching_interval, - ); - let shutdown_listener = shutdown.subscribe(); - tokio::spawn(async move { refresher.run(shutdown_listener).await }); - } -} diff --git a/nym-api/src/ecash/tests/mod.rs b/nym-api/src/ecash/tests/mod.rs index 22ec524a29d..faa142c2eaa 100644 --- a/nym-api/src/ecash/tests/mod.rs +++ b/nym-api/src/ecash/tests/mod.rs @@ -1,20 +1,20 @@ // Copyright 2022-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::circulating_supply_api::cache::CirculatingSupplyCache; use crate::ecash::api_routes::handlers::ecash_routes; use crate::ecash::error::{EcashError, Result}; use crate::ecash::keys::KeyPairWithEpoch; use crate::ecash::state::EcashState; +use crate::mixnet_contract_cache::cache::MixnetContractCache; use crate::network::models::NetworkDetails; use crate::node_describe_cache::cache::DescribedNodes; use crate::node_status_api::handlers::unstable; use crate::node_status_api::NodeStatusCache; -use crate::nym_contract_cache::cache::NymContractCache; use crate::status::ApiStatusState; use crate::support::caching::cache::SharedCache; use crate::support::config; use crate::support::http::state::chain_status::ChainStatusCache; +use crate::support::http::state::contract_details::ContractDetailsCache; use crate::support::http::state::force_refresh::ForcedRefresh; use crate::support::http::state::AppState; use crate::support::nyxd::Client; @@ -1279,9 +1279,8 @@ impl TestFixture { chain_status_cache: ChainStatusCache::new(Duration::from_secs(42)), address_info_cache: AddressInfoCache::new(Duration::from_secs(42), 1000), forced_refresh: ForcedRefresh::new(true), - nym_contract_cache: NymContractCache::new(), + mixnet_contract_cache: MixnetContractCache::new(), node_status_cache: NodeStatusCache::new(), - circulating_supply_cache: CirculatingSupplyCache::new("unym".to_owned()), storage, described_nodes_cache: SharedCache::::new(), network_details: NetworkDetails::new( @@ -1289,6 +1288,7 @@ impl TestFixture { NymNetworkDetails::new_empty(), ), node_info_cache: unstable::NodeInfoCache::default(), + contract_info_cache: ContractDetailsCache::new(Duration::from_secs(42)), api_status: ApiStatusState::new(None), ecash_state: Arc::new(ecash_state), } diff --git a/nym-api/src/epoch_operations/mod.rs b/nym-api/src/epoch_operations/mod.rs index dd2c64945e8..572b941c99e 100644 --- a/nym-api/src/epoch_operations/mod.rs +++ b/nym-api/src/epoch_operations/mod.rs @@ -12,9 +12,9 @@ // 3. Eventually this whole procedure is going to get expanded to allow for distribution of rewarded set generation // and hence this might be a good place for it. +use crate::mixnet_contract_cache::cache::MixnetContractCache; use crate::node_describe_cache::cache::DescribedNodes; use crate::node_status_api::{NodeStatusCache, ONE_DAY}; -use crate::nym_contract_cache::cache::NymContractCache; use crate::support::caching::cache::SharedCache; use crate::support::nyxd::Client; use crate::support::storage::NymApiStorage; @@ -37,7 +37,7 @@ mod transition_beginning; // this is struct responsible for advancing an epoch pub struct EpochAdvancer { nyxd_client: Client, - nym_contract_cache: NymContractCache, + nym_contract_cache: MixnetContractCache, described_cache: SharedCache, status_cache: NodeStatusCache, storage: NymApiStorage, @@ -52,7 +52,7 @@ impl EpochAdvancer { pub(crate) fn new( nyxd_client: Client, - nym_contract_cache: NymContractCache, + nym_contract_cache: MixnetContractCache, status_cache: NodeStatusCache, described_cache: SharedCache, storage: NymApiStorage, @@ -239,7 +239,7 @@ impl EpochAdvancer { pub(crate) fn start( nyxd_client: Client, - nym_contract_cache: &NymContractCache, + nym_contract_cache: &MixnetContractCache, status_cache: &NodeStatusCache, described_cache: SharedCache, storage: &NymApiStorage, diff --git a/nym-api/src/key_rotation/mod.rs b/nym-api/src/key_rotation/mod.rs index 6d648c2e92e..304e9ae3285 100644 --- a/nym-api/src/key_rotation/mod.rs +++ b/nym-api/src/key_rotation/mod.rs @@ -1,7 +1,7 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::nym_contract_cache::cache::NymContractCache; +use crate::mixnet_contract_cache::cache::MixnetContractCache; use crate::support::caching::refresher::{CacheUpdateWatcher, RefreshRequester}; use nym_mixnet_contract_common::{Interval, KeyRotationState}; use nym_task::TaskClient; @@ -59,14 +59,14 @@ pub(crate) struct KeyRotationController { pub(crate) describe_cache_refresher: RefreshRequester, pub(crate) contract_cache_watcher: CacheUpdateWatcher, - pub(crate) contract_cache: NymContractCache, + pub(crate) contract_cache: MixnetContractCache, } impl KeyRotationController { pub(crate) fn new( describe_cache_refresher: RefreshRequester, contract_cache_watcher: CacheUpdateWatcher, - contract_cache: NymContractCache, + contract_cache: MixnetContractCache, ) -> KeyRotationController { KeyRotationController { last_described_refreshed_for: None, diff --git a/nym-api/src/main.rs b/nym-api/src/main.rs index 76bd1ab3dd0..c07a6c2e8f1 100644 --- a/nym-api/src/main.rs +++ b/nym-api/src/main.rs @@ -1,17 +1,14 @@ // Copyright 2020-2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -#![warn(clippy::todo)] -#![warn(clippy::dbg_macro)] - use crate::epoch_operations::EpochAdvancer; use crate::support::cli; use crate::support::storage; use ::nym_config::defaults::setup_env; use clap::Parser; +use mixnet_contract_cache::cache::MixnetContractCache; use node_status_api::NodeStatusCache; use nym_bin_common::logging::setup_tracing_logger; -use nym_contract_cache::cache::NymContractCache; use support::nyxd; use tracing::{info, trace}; @@ -19,11 +16,12 @@ mod circulating_supply_api; mod ecash; mod epoch_operations; mod key_rotation; +pub(crate) mod mixnet_contract_cache; pub(crate) mod network; mod network_monitor; pub(crate) mod node_describe_cache; +mod node_performance; pub(crate) mod node_status_api; -pub(crate) mod nym_contract_cache; pub(crate) mod nym_nodes; mod status; pub(crate) mod support; diff --git a/nym-api/src/nym_contract_cache/cache/data.rs b/nym-api/src/mixnet_contract_cache/cache/data.rs similarity index 63% rename from nym-api/src/nym_contract_cache/cache/data.rs rename to nym-api/src/mixnet_contract_cache/cache/data.rs index f54f9c48f1f..d648c1037b0 100644 --- a/nym-api/src/nym_contract_cache/cache/data.rs +++ b/nym-api/src/mixnet_contract_cache/cache/data.rs @@ -3,14 +3,11 @@ use nym_api_requests::legacy::{LegacyGatewayBondWithId, LegacyMixNodeDetailsWithLayer}; use nym_api_requests::models::ConfigScoreDataResponse; -use nym_contracts_common::ContractBuildInformation; use nym_mixnet_contract_common::{ ConfigScoreParams, HistoricalNymNodeVersionEntry, Interval, KeyRotationState, NymNodeDetails, RewardingParams, }; use nym_topology::CachedEpochRewardedSet; -use nym_validator_client::nyxd::AccountId; -use std::collections::HashMap; #[derive(Clone)] pub(crate) struct ConfigScoreData { @@ -31,7 +28,9 @@ impl From for ConfigScoreDataResponse { } } -pub(crate) struct ContractCacheData { +pub(crate) struct MixnetContractCacheData { + pub(crate) rewarding_denom: String, + pub(crate) legacy_mixnodes: Vec, pub(crate) legacy_gateways: Vec, pub(crate) nym_nodes: Vec, @@ -41,30 +40,4 @@ pub(crate) struct ContractCacheData { pub(crate) current_reward_params: RewardingParams, pub(crate) current_interval: Interval, pub(crate) key_rotation_state: KeyRotationState, - - pub(crate) contracts_info: CachedContractsInfo, -} - -type ContractAddress = String; -pub type CachedContractsInfo = HashMap; - -#[derive(Clone)] -pub struct CachedContractInfo { - pub(crate) address: Option, - pub(crate) base: Option, - pub(crate) detailed: Option, -} - -impl CachedContractInfo { - pub fn new( - address: Option<&AccountId>, - base: Option, - detailed: Option, - ) -> Self { - Self { - address: address.cloned(), - base, - detailed, - } - } } diff --git a/nym-api/src/nym_contract_cache/cache/mod.rs b/nym-api/src/mixnet_contract_cache/cache/mod.rs similarity index 78% rename from nym-api/src/nym_contract_cache/cache/mod.rs rename to nym-api/src/mixnet_contract_cache/cache/mod.rs index e73dde0f41a..39915845e8a 100644 --- a/nym-api/src/nym_contract_cache/cache/mod.rs +++ b/nym-api/src/mixnet_contract_cache/cache/mod.rs @@ -1,52 +1,56 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use crate::mixnet_contract_cache::cache::data::ConfigScoreData; use crate::node_describe_cache::refresh::RefreshData; -use crate::nym_contract_cache::cache::data::{CachedContractsInfo, ConfigScoreData}; use crate::support::caching::cache::{SharedCache, UninitialisedCache}; use crate::support::caching::Cache; -use data::ContractCacheData; +use data::MixnetContractCacheData; use nym_api_requests::legacy::{ LegacyGatewayBondWithId, LegacyMixNodeBondWithLayer, LegacyMixNodeDetailsWithLayer, }; -use nym_api_requests::models::MixnodeStatus; +use nym_api_requests::models::{CirculatingSupplyResponse, MixnodeStatus}; +use nym_contracts_common::truncate_decimal; use nym_crypto::asymmetric::ed25519; use nym_mixnet_contract_common::{ Interval, KeyRotationState, NodeId, NymNodeDetails, RewardingParams, }; use nym_topology::CachedEpochRewardedSet; +use nym_validator_client::nyxd::Coin; use time::OffsetDateTime; use tokio::sync::RwLockReadGuard; pub(crate) mod data; pub(crate) mod refresher; +const TOTAL_SUPPLY_AMOUNT: u128 = 1_000_000_000_000_000; // 1B tokens + #[derive(Clone)] -pub struct NymContractCache { - pub(crate) inner: SharedCache, +pub struct MixnetContractCache { + pub(crate) inner: SharedCache, } -impl NymContractCache { +impl MixnetContractCache { pub(crate) fn new() -> Self { - NymContractCache { + MixnetContractCache { inner: SharedCache::new(), } } - pub(crate) fn inner(&self) -> SharedCache { + pub(crate) fn inner(&self) -> SharedCache { self.inner.clone() } async fn get_owned( &self, - fn_arg: impl FnOnce(&ContractCacheData) -> T, + fn_arg: impl FnOnce(&MixnetContractCacheData) -> T, ) -> Result { Ok(fn_arg(&**self.inner.get().await?)) } async fn get<'a, T: 'a>( &'a self, - fn_arg: impl FnOnce(&Cache) -> &T, + fn_arg: impl FnOnce(&Cache) -> &T, ) -> Result, UninitialisedCache> { let guard = self.inner.get().await?; Ok(RwLockReadGuard::map(guard, fn_arg)) @@ -157,12 +161,6 @@ impl NymContractCache { .key_rotation_id(current_absolute_epoch_id)) } - pub(crate) async fn contract_details(&self) -> CachedContractsInfo { - self.get_owned(|c| c.contracts_info.clone()) - .await - .unwrap_or_default() - } - pub async fn mixnode_status(&self, mix_id: NodeId) -> MixnodeStatus { let Ok(cache) = self.inner.get().await else { return Default::default(); @@ -215,6 +213,31 @@ impl NymContractCache { None } + pub(crate) async fn get_circulating_supply(&self) -> Option { + let mix_denom = self.get_owned(|c| c.rewarding_denom.clone()).await.ok()?; + let reward_pool = self + .interval_reward_params() + .await + .ok()? + .interval + .reward_pool; + + let mixmining_reserve_amount = truncate_decimal(reward_pool).u128(); + let mixmining_reserve = Coin::new(mixmining_reserve_amount, &mix_denom).into(); + + // given all tokens have already vested, the circulating supply is total supply - mixmining reserve + let circulating_supply = + Coin::new(TOTAL_SUPPLY_AMOUNT - mixmining_reserve_amount, &mix_denom).into(); + + Some(CirculatingSupplyResponse { + total_supply: Coin::new(TOTAL_SUPPLY_AMOUNT, &mix_denom).into(), + mixmining_reserve, + // everything has already vested + vesting_tokens: Coin::new(0, &mix_denom).into(), + circulating_supply, + }) + } + pub(crate) async fn naive_wait_for_initial_values(&self) { self.inner.naive_wait_for_initial_values().await } diff --git a/nym-api/src/nym_contract_cache/cache/refresher.rs b/nym-api/src/mixnet_contract_cache/cache/refresher.rs similarity index 59% rename from nym-api/src/nym_contract_cache/cache/refresher.rs rename to nym-api/src/mixnet_contract_cache/cache/refresher.rs index 87af6d2d049..7e0735ca213 100644 --- a/nym-api/src/nym_contract_cache/cache/refresher.rs +++ b/nym-api/src/mixnet_contract_cache/cache/refresher.rs @@ -1,9 +1,7 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::nym_contract_cache::cache::data::{ - CachedContractInfo, CachedContractsInfo, ConfigScoreData, ContractCacheData, -}; +use crate::mixnet_contract_cache::cache::data::{ConfigScoreData, MixnetContractCacheData}; use crate::nyxd::Client; use crate::support::caching::refresher::CacheItemProvider; use anyhow::Result; @@ -12,9 +10,6 @@ use nym_api_requests::legacy::{ LegacyGatewayBondWithId, LegacyMixNodeBondWithLayer, LegacyMixNodeDetailsWithLayer, }; use nym_mixnet_contract_common::LegacyMixLayer; -use nym_validator_client::nyxd::contract_traits::{ - MixnetQueryClient, NymContractsProvider, VestingQueryClient, -}; use nym_validator_client::nyxd::error::NyxdError; use rand::prelude::SliceRandom; use rand::rngs::OsRng; @@ -22,90 +17,29 @@ use std::collections::HashMap; use std::collections::HashSet; use tracing::info; -pub struct ContractDataProvider { +pub struct MixnetContractDataProvider { nyxd_client: Client, } #[async_trait] -impl CacheItemProvider for ContractDataProvider { - type Item = ContractCacheData; +impl CacheItemProvider for MixnetContractDataProvider { + type Item = MixnetContractCacheData; type Error = NyxdError; - async fn try_refresh(&self) -> std::result::Result { - self.refresh().await + async fn try_refresh(&mut self) -> std::result::Result, Self::Error> { + self.refresh().await.map(Some) } } -impl ContractDataProvider { +impl MixnetContractDataProvider { pub(crate) fn new(nyxd_client: Client) -> Self { - ContractDataProvider { nyxd_client } - } - - async fn get_nym_contracts_info(&self) -> Result { - use crate::query_guard; - - let mut updated = HashMap::new(); - - let client_guard = self.nyxd_client.read().await; - - let mixnet = query_guard!(client_guard, mixnet_contract_address()); - let vesting = query_guard!(client_guard, vesting_contract_address()); - let coconut_dkg = query_guard!(client_guard, dkg_contract_address()); - let group = query_guard!(client_guard, group_contract_address()); - let multisig = query_guard!(client_guard, multisig_contract_address()); - let ecash = query_guard!(client_guard, ecash_contract_address()); - - for (address, name) in [ - (mixnet, "nym-mixnet-contract"), - (vesting, "nym-vesting-contract"), - (coconut_dkg, "nym-coconut-dkg-contract"), - (group, "nym-cw4-group-contract"), - (multisig, "nym-cw3-multisig-contract"), - (ecash, "nym-ecash-contract"), - ] { - let (cw2, build_info) = if let Some(address) = address { - let cw2 = query_guard!(client_guard, try_get_cw2_contract_version(address).await); - let mut build_info = query_guard!( - client_guard, - try_get_contract_build_information(address).await - ); - - // for backwards compatibility until we migrate the contracts - if build_info.is_none() { - match name { - "nym-mixnet-contract" => { - build_info = Some(query_guard!( - client_guard, - get_mixnet_contract_version().await - )?) - } - "nym-vesting-contract" => { - build_info = Some(query_guard!( - client_guard, - get_vesting_contract_version().await - )?) - } - _ => (), - } - } - - (cw2, build_info) - } else { - (None, None) - }; - - updated.insert( - name.to_string(), - CachedContractInfo::new(address, cw2, build_info), - ); - } - - Ok(updated) + MixnetContractDataProvider { nyxd_client } } - async fn refresh(&self) -> Result { + async fn refresh(&self) -> Result { let current_reward_params = self.nyxd_client.get_current_rewarding_parameters().await?; let current_interval = self.nyxd_client.get_current_interval().await?.interval; + let contract_state = self.nyxd_client.get_mixnet_contract_state().await?; let nym_nodes = self.nyxd_client.get_nymnodes().await?; let mixnode_details = self.nyxd_client.get_mixnodes().await?; @@ -184,7 +118,6 @@ impl ContractDataProvider { let key_rotation_state = self.nyxd_client.get_key_rotation_state().await?; let config_score_params = self.nyxd_client.get_config_score_params().await?; let nym_node_version_history = self.nyxd_client.get_nym_node_version_history().await?; - let contracts_info = self.get_nym_contracts_info().await?; info!( "Updating validator cache. There are {} [legacy] mixnodes, {} [legacy] gateways and {} nym nodes", @@ -193,7 +126,8 @@ impl ContractDataProvider { nym_nodes.len(), ); - Ok(ContractCacheData { + Ok(MixnetContractCacheData { + rewarding_denom: contract_state.rewarding_denom, legacy_mixnodes, legacy_gateways, nym_nodes, @@ -205,7 +139,6 @@ impl ContractDataProvider { current_reward_params, current_interval, key_rotation_state, - contracts_info, }) } } diff --git a/nym-api/src/nym_contract_cache/handlers.rs b/nym-api/src/mixnet_contract_cache/handlers.rs similarity index 100% rename from nym-api/src/nym_contract_cache/handlers.rs rename to nym-api/src/mixnet_contract_cache/handlers.rs diff --git a/nym-api/src/nym_contract_cache/mod.rs b/nym-api/src/mixnet_contract_cache/mod.rs similarity index 50% rename from nym-api/src/nym_contract_cache/mod.rs rename to nym-api/src/mixnet_contract_cache/mod.rs index cdb6bcd8e05..9911c55683c 100644 --- a/nym-api/src/nym_contract_cache/mod.rs +++ b/nym-api/src/mixnet_contract_cache/mod.rs @@ -1,9 +1,9 @@ // Copyright 2021-2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::nym_contract_cache::cache::data::ContractCacheData; -use crate::nym_contract_cache::cache::refresher::ContractDataProvider; -use crate::nym_contract_cache::cache::NymContractCache; +use crate::mixnet_contract_cache::cache::data::MixnetContractCacheData; +use crate::mixnet_contract_cache::cache::refresher::MixnetContractDataProvider; +use crate::mixnet_contract_cache::cache::MixnetContractCache; use crate::support::caching::refresher::CacheRefresher; use crate::support::{config, nyxd}; use nym_validator_client::nyxd::error::NyxdError; @@ -12,14 +12,14 @@ pub(crate) mod cache; pub(crate) mod handlers; pub(crate) fn build_refresher( - config: &config::NodeStatusAPI, - nym_contract_cache_state: &NymContractCache, + config: &config::MixnetContractCache, + nym_contract_cache_state: &MixnetContractCache, nyxd_client: nyxd::Client, -) -> CacheRefresher { +) -> CacheRefresher { CacheRefresher::new_with_initial_value( - Box::new(ContractDataProvider::new(nyxd_client)), + Box::new(MixnetContractDataProvider::new(nyxd_client)), config.debug.caching_interval, nym_contract_cache_state.inner(), ) - .named("contract-cache-refresher") + .named("mixnet-contract-cache-refresher") } diff --git a/nym-api/src/network/handlers.rs b/nym-api/src/network/handlers.rs index f5a3e127364..4d6c8e01f7b 100644 --- a/nym-api/src/network/handlers.rs +++ b/nym-api/src/network/handlers.rs @@ -5,7 +5,7 @@ use crate::network::models::{ContractInformation, NetworkDetails}; use crate::node_status_api::models::AxumResult; use crate::support::http::state::AppState; use axum::extract::{Query, State}; -use axum::{extract, Router}; +use axum::Router; use nym_api_requests::models::ChainStatusResponse; use nym_contracts_common::ContractBuildInformation; use nym_http_api_common::{FormattedResponse, OutputParams}; @@ -40,7 +40,7 @@ pub(crate) fn nym_network_routes() -> Router { )] async fn network_details( Query(output): Query, - extract::State(state): extract::State, + State(state): State, ) -> FormattedResponse { let output = output.output.unwrap_or_default(); @@ -116,13 +116,18 @@ pub struct ContractInformationContractVersion { )] async fn nym_contracts( Query(output): Query, - extract::State(state): extract::State, -) -> FormattedResponse>> { + State(state): State, +) -> AxumResult>>> { let output = output.output.unwrap_or_default(); - let info = state.nym_contract_cache().contract_details().await; - output.to_response( - info.iter() + let contract_info = state + .contract_info_cache + .get_or_refresh(&state.nyxd_client) + .await?; + + Ok(output.to_response( + contract_info + .iter() .map(|(contract, info)| { ( contract.to_owned(), @@ -133,7 +138,7 @@ async fn nym_contracts( ) }) .collect::>(), - ) + )) } #[allow(dead_code)] // not dead, used in OpenAPI docs @@ -158,13 +163,18 @@ pub struct ContractInformationBuildInformation { )] async fn nym_contracts_detailed( Query(output): Query, - extract::State(state): extract::State, -) -> FormattedResponse>> { + State(state): State, +) -> AxumResult>>> { let output = output.output.unwrap_or_default(); - let info = state.nym_contract_cache().contract_details().await; - output.to_response( - info.iter() + let contract_info = state + .contract_info_cache + .get_or_refresh(&state.nyxd_client) + .await?; + + Ok(output.to_response( + contract_info + .iter() .map(|(contract, info)| { ( contract.to_owned(), @@ -175,5 +185,5 @@ async fn nym_contracts_detailed( ) }) .collect::>(), - ) + )) } diff --git a/nym-api/src/network_monitor/mod.rs b/nym-api/src/network_monitor/mod.rs index ea1421105f4..d0134cafa6b 100644 --- a/nym-api/src/network_monitor/mod.rs +++ b/nym-api/src/network_monitor/mod.rs @@ -1,6 +1,7 @@ // Copyright 2021-2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use crate::mixnet_contract_cache::cache::MixnetContractCache; use crate::network_monitor::monitor::preparer::PacketPreparer; use crate::network_monitor::monitor::processor::{ ReceivedProcessor, ReceivedProcessorReceiver, ReceivedProcessorSender, @@ -13,7 +14,6 @@ use crate::network_monitor::monitor::summary_producer::SummaryProducer; use crate::network_monitor::monitor::Monitor; use crate::node_describe_cache::cache::DescribedNodes; use crate::node_status_api::NodeStatusCache; -use crate::nym_contract_cache::cache::NymContractCache; use crate::storage::NymApiStorage; use crate::support::caching::cache::SharedCache; use crate::support::config::Config; @@ -38,7 +38,7 @@ pub(crate) const ROUTE_TESTING_TEST_NONCE: u64 = 0; pub(crate) fn setup<'a>( config: &'a Config, - nym_contract_cache: &NymContractCache, + nym_contract_cache: &MixnetContractCache, described_cache: SharedCache, node_status_cache: NodeStatusCache, storage: &NymApiStorage, @@ -58,7 +58,7 @@ pub(crate) struct NetworkMonitorBuilder<'a> { config: &'a Config, nyxd_client: nyxd::Client, node_status_storage: NymApiStorage, - contract_cache: NymContractCache, + contract_cache: MixnetContractCache, described_cache: SharedCache, node_status_cache: NodeStatusCache, } @@ -68,7 +68,7 @@ impl<'a> NetworkMonitorBuilder<'a> { config: &'a Config, nyxd_client: nyxd::Client, node_status_storage: NymApiStorage, - contract_cache: NymContractCache, + contract_cache: MixnetContractCache, described_cache: SharedCache, node_status_cache: NodeStatusCache, ) -> Self { @@ -178,7 +178,7 @@ impl NetworkMonitorRunnables { } fn new_packet_preparer( - contract_cache: NymContractCache, + contract_cache: MixnetContractCache, described_cache: SharedCache, node_status_cache: NodeStatusCache, per_node_test_packets: usize, @@ -236,7 +236,7 @@ fn new_packet_receiver( // TODO: 2) how do we make it non-async as other 'start' methods? pub(crate) async fn start( config: &Config, - nym_contract_cache: &NymContractCache, + nym_contract_cache: &MixnetContractCache, described_cache: SharedCache, node_status_cache: NodeStatusCache, storage: &NymApiStorage, diff --git a/nym-api/src/network_monitor/monitor/preparer.rs b/nym-api/src/network_monitor/monitor/preparer.rs index f0d6c4ebfa2..49d48ab7839 100644 --- a/nym-api/src/network_monitor/monitor/preparer.rs +++ b/nym-api/src/network_monitor/monitor/preparer.rs @@ -1,12 +1,12 @@ // Copyright 2021-2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use crate::mixnet_contract_cache::cache::MixnetContractCache; use crate::network_monitor::monitor::sender::GatewayPackets; use crate::network_monitor::test_route::TestRoute; use crate::node_describe_cache::cache::DescribedNodes; use crate::node_describe_cache::NodeDescriptionTopologyExt; use crate::node_status_api::NodeStatusCache; -use crate::nym_contract_cache::cache::NymContractCache; use crate::support::caching::cache::SharedCache; use crate::support::legacy_helpers::legacy_host_to_ips_and_hostname; use nym_api_requests::legacy::{LegacyGatewayBondWithId, LegacyMixNodeBondWithLayer}; @@ -78,7 +78,7 @@ pub(crate) struct PreparedPackets { #[derive(Clone)] pub(crate) struct PacketPreparer { - contract_cache: NymContractCache, + contract_cache: MixnetContractCache, described_cache: SharedCache, node_status_cache: NodeStatusCache, @@ -96,7 +96,7 @@ pub(crate) struct PacketPreparer { impl PacketPreparer { pub(crate) fn new( - contract_cache: NymContractCache, + contract_cache: MixnetContractCache, described_cache: SharedCache, node_status_cache: NodeStatusCache, per_node_test_packets: usize, diff --git a/nym-api/src/node_describe_cache/provider.rs b/nym-api/src/node_describe_cache/provider.rs index da74b2a5f96..259b14ba1ea 100644 --- a/nym-api/src/node_describe_cache/provider.rs +++ b/nym-api/src/node_describe_cache/provider.rs @@ -1,10 +1,10 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use crate::mixnet_contract_cache::cache::MixnetContractCache; use crate::node_describe_cache::cache::DescribedNodes; use crate::node_describe_cache::refresh::RefreshData; use crate::node_describe_cache::NodeDescribeCacheError; -use crate::nym_contract_cache::cache::NymContractCache; use crate::support::caching::cache::SharedCache; use crate::support::caching::refresher::{CacheItemProvider, CacheRefresher}; use crate::support::config; @@ -15,7 +15,7 @@ use std::collections::HashMap; use tracing::{error, info}; pub struct NodeDescriptionProvider { - contract_cache: NymContractCache, + contract_cache: MixnetContractCache, allow_all_ips: bool, batch_size: usize, @@ -23,7 +23,7 @@ pub struct NodeDescriptionProvider { impl NodeDescriptionProvider { pub(crate) fn new( - contract_cache: NymContractCache, + contract_cache: MixnetContractCache, allow_all_ips: bool, ) -> NodeDescriptionProvider { NodeDescriptionProvider { @@ -49,7 +49,7 @@ impl CacheItemProvider for NodeDescriptionProvider { self.contract_cache.naive_wait_for_initial_values().await } - async fn try_refresh(&self) -> Result { + async fn try_refresh(&mut self) -> Result, Self::Error> { // we need to query: // - legacy mixnodes (because they might already be running nym-nodes, but haven't updated contract info) // - legacy gateways (because they might already be running nym-nodes, but haven't updated contract info) @@ -110,45 +110,37 @@ impl CacheItemProvider for NodeDescriptionProvider { info!("refreshed self described data for {} nodes", nodes.len()); info!("with {} unique ip addresses", addresses_cache.len()); - Ok(DescribedNodes { + Ok(Some(DescribedNodes { nodes, addresses_cache, - }) + })) } } // currently dead code : ( #[allow(dead_code)] pub(crate) fn new_refresher( - config: &config::TopologyCacher, - contract_cache: NymContractCache, + config: &config::DescribeCache, + contract_cache: MixnetContractCache, ) -> CacheRefresher { CacheRefresher::new( - Box::new( - NodeDescriptionProvider::new( - contract_cache, - config.debug.node_describe_allow_illegal_ips, - ) - .with_batch_size(config.debug.node_describe_batch_size), - ), - config.debug.node_describe_caching_interval, + NodeDescriptionProvider::new(contract_cache, config.debug.allow_illegal_ips) + .with_batch_size(config.debug.batch_size), + config.debug.caching_interval, ) } pub(crate) fn new_provider_with_initial_value( - config: &config::TopologyCacher, - contract_cache: NymContractCache, + config: &config::DescribeCache, + contract_cache: MixnetContractCache, initial: SharedCache, ) -> CacheRefresher { CacheRefresher::new_with_initial_value( Box::new( - NodeDescriptionProvider::new( - contract_cache, - config.debug.node_describe_allow_illegal_ips, - ) - .with_batch_size(config.debug.node_describe_batch_size), + NodeDescriptionProvider::new(contract_cache, config.debug.allow_illegal_ips) + .with_batch_size(config.debug.batch_size), ), - config.debug.node_describe_caching_interval, + config.debug.caching_interval, initial, ) } diff --git a/nym-api/src/node_performance/contract_cache/data.rs b/nym-api/src/node_performance/contract_cache/data.rs new file mode 100644 index 00000000000..985ee0a4d70 --- /dev/null +++ b/nym-api/src/node_performance/contract_cache/data.rs @@ -0,0 +1,88 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node_performance::provider::PerformanceRetrievalFailure; +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::NodePerformance; +use std::collections::{BTreeMap, HashMap}; + +pub(crate) struct PerformanceContractEpochCacheData { + pub(crate) epoch_id: EpochId, + pub(crate) median_performance: HashMap, +} + +impl PerformanceContractEpochCacheData { + pub(crate) fn from_node_performance( + performance: Vec, + epoch_id: EpochId, + ) -> Self { + let median_performance = performance + .into_iter() + .map(|node_performance| (node_performance.node_id, node_performance.performance)) + .collect(); + PerformanceContractEpochCacheData { + epoch_id, + median_performance, + } + } +} + +pub(crate) struct PerformanceContractCacheData { + pub(crate) epoch_performance: BTreeMap, +} + +impl PerformanceContractCacheData { + pub(crate) fn update( + &mut self, + update: PerformanceContractEpochCacheData, + values_to_retain: usize, + ) { + self.epoch_performance.insert(update.epoch_id, update); + if self.epoch_performance.len() > values_to_retain { + // remove the oldest entry, i.e. one with the lowest epoch id + self.epoch_performance.pop_first(); + } + } + + pub(crate) fn node_routing_score( + &self, + node_id: NodeId, + epoch_id: EpochId, + ) -> Result { + // TODO: somehow send a signal to refresh this epoch + let epoch_scores = self.epoch_performance.get(&epoch_id).ok_or_else(|| { + PerformanceRetrievalFailure::new( + node_id, + epoch_id, + format!("no cached performance results for epoch {epoch_id}"), + ) + })?; + + let node_score = epoch_scores + .median_performance + .get(&node_id) + .ok_or_else(|| { + PerformanceRetrievalFailure::new( + node_id, + epoch_id, + format!( + "no cached performance results for node {node_id} for epoch {epoch_id}" + ), + ) + })?; + + Ok(RoutingScore::new(node_score.naive_to_f64())) + } +} + +// needed for cache initialisation +impl From for PerformanceContractCacheData { + fn from(cache_data: PerformanceContractEpochCacheData) -> Self { + let mut epoch_performance = BTreeMap::new(); + epoch_performance.insert(cache_data.epoch_id, cache_data); + PerformanceContractCacheData { epoch_performance } + } +} diff --git a/nym-api/src/node_performance/contract_cache/mod.rs b/nym-api/src/node_performance/contract_cache/mod.rs new file mode 100644 index 00000000000..412b2f00b1a --- /dev/null +++ b/nym-api/src/node_performance/contract_cache/mod.rs @@ -0,0 +1,51 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::mixnet_contract_cache::cache::MixnetContractCache; +use crate::node_performance::contract_cache::data::PerformanceContractCacheData; +use crate::node_performance::contract_cache::refresher::{ + refresher_update_fn, PerformanceContractDataProvider, +}; +use crate::support::caching::cache::SharedCache; +use crate::support::caching::refresher::CacheRefresher; +use crate::support::{config, nyxd}; +use anyhow::bail; +use nym_task::TaskManager; + +pub(crate) mod data; +pub(crate) mod refresher; + +pub(crate) async fn start_cache_refresher( + config: &config::PerformanceProvider, + nyxd_client: nyxd::Client, + mixnet_contract_cache: MixnetContractCache, + task_manager: &TaskManager, +) -> anyhow::Result> { + let values_to_retain = config.debug.max_epoch_entries_to_retain; + + let mut item_provider = + PerformanceContractDataProvider::new(nyxd_client, mixnet_contract_cache); + + if !item_provider.cache_has_values().await { + bail!("performance contract is empty - can't use it as source of node performance") + } + + let warmed_up_cache = SharedCache::new_with_value( + item_provider + .provide_initial_warmed_up_cache(values_to_retain) + .await?, + ); + + CacheRefresher::new_with_initial_value( + Box::new(item_provider), + config.debug.contract_polling_interval, + warmed_up_cache.clone(), + ) + .named("performance-contract-cache-refresher") + .with_update_fn(move |main_cache, update| { + refresher_update_fn(main_cache, update, values_to_retain) + }) + .start(task_manager.subscribe_named("performance-contract-cache-refresher")); + + Ok(warmed_up_cache) +} diff --git a/nym-api/src/node_performance/contract_cache/refresher.rs b/nym-api/src/node_performance/contract_cache/refresher.rs new file mode 100644 index 00000000000..887374b0184 --- /dev/null +++ b/nym-api/src/node_performance/contract_cache/refresher.rs @@ -0,0 +1,135 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::mixnet_contract_cache::cache::MixnetContractCache; +use crate::node_performance::contract_cache::data::{ + PerformanceContractCacheData, PerformanceContractEpochCacheData, +}; +use crate::support::caching::refresher::CacheItemProvider; +use crate::support::nyxd::Client; +use async_trait::async_trait; +use nym_validator_client::nyxd::contract_traits::performance_query_client::LastSubmission; +use nym_validator_client::nyxd::error::NyxdError; +use std::collections::BTreeMap; + +pub struct PerformanceContractDataProvider { + nyxd_client: Client, + mixnet_contract_cache: MixnetContractCache, + last_submission: Option, +} + +pub(crate) fn refresher_update_fn( + main_cache: &mut PerformanceContractCacheData, + update: PerformanceContractEpochCacheData, + values_to_retain: usize, +) { + main_cache.update(update, values_to_retain); +} + +#[async_trait] +impl CacheItemProvider for PerformanceContractDataProvider { + type Item = PerformanceContractEpochCacheData; + type Error = NyxdError; + + async fn wait_until_ready(&self) { + self.mixnet_contract_cache + .naive_wait_for_initial_values() + .await + } + + async fn try_refresh(&mut self) -> Result, Self::Error> { + self.refresh().await + } +} + +impl PerformanceContractDataProvider { + pub(crate) fn new(nyxd_client: Client, mixnet_contract_cache: MixnetContractCache) -> Self { + PerformanceContractDataProvider { + nyxd_client, + mixnet_contract_cache, + last_submission: None, + } + } + + pub(crate) async fn cache_has_values(&self) -> bool { + let Ok(last_submitted) = self + .nyxd_client + .get_last_performance_contract_submission() + .await + else { + return false; + }; + last_submitted.data.is_some() + } + + pub(crate) async fn provide_initial_warmed_up_cache( + &mut self, + values_to_keep: usize, + ) -> Result { + let last_submitted = self + .nyxd_client + .get_last_performance_contract_submission() + .await?; + + self.mixnet_contract_cache + .naive_wait_for_initial_values() + .await; + + // SAFETY: we just waited for cache to be available + #[allow(clippy::unwrap_used)] + let current_epoch = self + .mixnet_contract_cache + .current_interval() + .await + .unwrap() + .current_epoch_absolute_id(); + + let last = current_epoch.saturating_sub(values_to_keep as u32); + + let mut epoch_performance = BTreeMap::default(); + for epoch in current_epoch..last { + let performance = self.nyxd_client.get_full_epoch_performance(epoch).await?; + let per_epoch_performance = + PerformanceContractEpochCacheData::from_node_performance(performance, epoch); + epoch_performance.insert(epoch, per_epoch_performance); + } + + self.last_submission = Some(last_submitted); + + Ok(PerformanceContractCacheData { epoch_performance }) + } + + async fn refresh(&mut self) -> Result, NyxdError> { + let last_submitted = self + .nyxd_client + .get_last_performance_contract_submission() + .await?; + + // no updates + if let Some(prior_submission) = &self.last_submission { + if prior_submission == &last_submitted { + return Ok(None); + } + } + + // SAFETY: refresher is not started until the mixnet contract cache had been initialised + #[allow(clippy::unwrap_used)] + let current_epoch = self + .mixnet_contract_cache + .current_interval() + .await + .unwrap() + .current_epoch_absolute_id(); + + let performance = self + .nyxd_client + .get_full_epoch_performance(current_epoch) + .await?; + + self.last_submission = Some(last_submitted); + + Ok(Some( + PerformanceContractEpochCacheData::from_node_performance(performance, current_epoch), + )) + } +} diff --git a/nym-api/src/node_performance/mod.rs b/nym-api/src/node_performance/mod.rs new file mode 100644 index 00000000000..f7f5eb5dd1d --- /dev/null +++ b/nym-api/src/node_performance/mod.rs @@ -0,0 +1,5 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +pub(crate) mod contract_cache; +pub(crate) mod provider; diff --git a/nym-api/src/node_performance/provider/contract_provider.rs b/nym-api/src/node_performance/provider/contract_provider.rs new file mode 100644 index 00000000000..29923ccd201 --- /dev/null +++ b/nym-api/src/node_performance/provider/contract_provider.rs @@ -0,0 +1,94 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node_performance::contract_cache::data::PerformanceContractCacheData; +use crate::node_performance::provider::{NodesRoutingScores, PerformanceRetrievalFailure}; +use crate::support::caching::cache::SharedCache; +use crate::support::config; +use nym_api_requests::models::RoutingScore; +use nym_mixnet_contract_common::{EpochId, NodeId}; +use std::collections::HashMap; +use tracing::warn; + +pub(crate) struct ContractPerformanceProvider { + cached: SharedCache, + max_epochs_fallback: u32, +} + +impl ContractPerformanceProvider { + pub(crate) fn new( + config: &config::PerformanceProvider, + contract_cache: SharedCache, + ) -> Self { + ContractPerformanceProvider { + cached: contract_cache, + max_epochs_fallback: config.debug.max_performance_fallback_epochs, + } + } + + fn node_routing_score_with_fallback( + &self, + contract_cache: &PerformanceContractCacheData, + node_id: NodeId, + epoch_id: EpochId, + ) -> Result { + let err = match contract_cache.node_routing_score(node_id, epoch_id) { + Ok(res) => return Ok(res), + Err(err) => err, + }; + + warn!("failed to retrieve performance score of node {node_id} for epoch {epoch_id}. falling back to at most {} past epochs", self.max_epochs_fallback); + + let threshold = epoch_id.saturating_sub(self.max_epochs_fallback); + let start = epoch_id.saturating_sub(1); + for epoch_id in start..threshold { + if let Ok(res) = contract_cache.node_routing_score(node_id, epoch_id) { + return Ok(res); + } + } + + Err(err) + } + + pub(crate) async fn node_routing_score( + &self, + node_id: NodeId, + epoch_id: EpochId, + ) -> Result { + let contract_cache = self.cached.get().await.map_err(|_| { + PerformanceRetrievalFailure::new( + node_id, + epoch_id, + "performance contract cache has not been initialised yet", + ) + })?; + + self.node_routing_score_with_fallback(&contract_cache, node_id, epoch_id) + } + + pub(crate) async fn node_routing_scores( + &self, + node_ids: Vec, + epoch_id: EpochId, + ) -> Result { + let Some(first) = node_ids.first() else { + return Ok(NodesRoutingScores::empty()); + }; + + let contract_cache = self.cached.get().await.map_err(|_| { + PerformanceRetrievalFailure::new( + *first, + epoch_id, + "performance contract cache has not been initialised yet", + ) + })?; + + let mut scores = HashMap::new(); + for node_id in node_ids { + let score = self.node_routing_score_with_fallback(&contract_cache, node_id, epoch_id); + scores.insert(node_id, score); + } + + Ok(NodesRoutingScores { inner: scores }) + } +} diff --git a/nym-api/src/node_performance/provider/legacy_storage_provider.rs b/nym-api/src/node_performance/provider/legacy_storage_provider.rs new file mode 100644 index 00000000000..99a886a19b9 --- /dev/null +++ b/nym-api/src/node_performance/provider/legacy_storage_provider.rs @@ -0,0 +1,91 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::mixnet_contract_cache::cache::MixnetContractCache; +use crate::node_performance::provider::PerformanceRetrievalFailure; +use crate::support::caching::cache::UninitialisedCache; +use crate::support::storage::NymApiStorage; +use nym_api_requests::models::RoutingScore; +use nym_mixnet_contract_common::{EpochId, NodeId}; + +pub(crate) struct LegacyStoragePerformanceProvider { + storage: NymApiStorage, + mixnet_contract_cache: MixnetContractCache, +} + +impl LegacyStoragePerformanceProvider { + pub(crate) fn new(storage: NymApiStorage, mixnet_contract_cache: MixnetContractCache) -> Self { + LegacyStoragePerformanceProvider { + storage, + mixnet_contract_cache, + } + } + + async fn map_epoch_id_to_end_unix_timestamp( + &self, + epoch_id: EpochId, + ) -> Result { + let interval_details = self.mixnet_contract_cache.current_interval().await?; + let duration = interval_details.epoch_length(); + let current_end = interval_details.current_epoch_end(); + let current_id = interval_details.current_epoch_absolute_id(); + + if current_id == epoch_id { + return Ok(current_end.unix_timestamp()); + } + + if current_id < epoch_id { + let diff = epoch_id - current_id; + let end = current_end + diff * duration; + return Ok(end.unix_timestamp()); + } + + // epoch_id > current_id + let diff = current_id - epoch_id; + let end = current_end - diff * duration; + Ok(end.unix_timestamp()) + } + + pub(crate) async fn epoch_id_unix_timestamp( + &self, + epoch_id: EpochId, + ) -> Result { + self.map_epoch_id_to_end_unix_timestamp(epoch_id) + .await + .map_err(|_| { + PerformanceRetrievalFailure::new( + 0, + epoch_id, + "mixnet contract cache has not been initialised yet", + ) + }) + } + + pub(crate) async fn node_routing_score( + &self, + node_id: NodeId, + epoch_id: EpochId, + ) -> Result { + let end_ts = self.epoch_id_unix_timestamp(epoch_id).await?; + self.get_node_routing_score_with_unix_timestamp(node_id, epoch_id, end_ts) + .await + } + + pub(crate) async fn get_node_routing_score_with_unix_timestamp( + &self, + node_id: NodeId, + epoch_id: EpochId, + end_ts: i64, + ) -> Result { + let reliability = self + .storage + .get_average_node_reliability_in_the_last_24hrs(node_id, end_ts) + .await + .map_err(|err| PerformanceRetrievalFailure::new(node_id, epoch_id, err.to_string()))?; + + // reliability: 0-100 + // score: 0-1 + let score = reliability / 100.; + Ok(RoutingScore::new(score as f64)) + } +} diff --git a/nym-api/src/node_performance/provider/mod.rs b/nym-api/src/node_performance/provider/mod.rs new file mode 100644 index 00000000000..aadc61a1d59 --- /dev/null +++ b/nym-api/src/node_performance/provider/mod.rs @@ -0,0 +1,123 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node_performance::provider::contract_provider::ContractPerformanceProvider; +use async_trait::async_trait; +use legacy_storage_provider::LegacyStoragePerformanceProvider; +use nym_api_requests::models::RoutingScore; +use nym_mixnet_contract_common::{EpochId, NodeId}; +use std::collections::HashMap; +use thiserror::Error; +use tracing::{debug, error}; + +pub(crate) mod contract_provider; +pub(crate) mod legacy_storage_provider; + +#[derive(Debug, Error)] +#[error("failed to retrieve performance score for node {node_id} for epoch {epoch_id}: {error}")] +pub(crate) struct PerformanceRetrievalFailure { + pub(crate) node_id: NodeId, + pub(crate) epoch_id: EpochId, + pub(crate) error: String, +} + +impl PerformanceRetrievalFailure { + pub(crate) fn new(node_id: NodeId, epoch_id: EpochId, error: impl Into) -> Self { + PerformanceRetrievalFailure { + node_id, + epoch_id, + error: error.into(), + } + } +} + +pub(crate) struct NodesRoutingScores { + inner: HashMap>, +} + +impl NodesRoutingScores { + pub(crate) fn empty() -> Self { + NodesRoutingScores { + inner: HashMap::new(), + } + } + pub(crate) fn get_or_log(&self, node_id: NodeId) -> RoutingScore { + match self.inner.get(&node_id) { + Some(Ok(score)) => *score, + Some(Err(err)) => { + debug!("{err}"); + RoutingScore::zero() + } + None => RoutingScore::zero(), + } + } +} + +#[async_trait] +pub(crate) trait NodePerformanceProvider { + /// Obtain a performance/routing score of a particular node for given epoch + #[allow(unused)] + async fn get_node_score( + &self, + node_id: NodeId, + epoch_id: EpochId, + ) -> Result; + + /// An optimisation for obtaining node scores of multiple nodes at once + async fn get_batch_node_scores( + &self, + node_ids: Vec, + epoch_id: EpochId, + ) -> Result; +} + +#[async_trait] +impl NodePerformanceProvider for ContractPerformanceProvider { + #[allow(unused)] + async fn get_node_score( + &self, + node_id: NodeId, + epoch_id: EpochId, + ) -> Result { + self.node_routing_score(node_id, epoch_id).await + } + + async fn get_batch_node_scores( + &self, + node_ids: Vec, + epoch_id: EpochId, + ) -> Result { + self.node_routing_scores(node_ids, epoch_id).await + } +} + +#[async_trait] +impl NodePerformanceProvider for LegacyStoragePerformanceProvider { + #[allow(unused)] + async fn get_node_score( + &self, + node_id: NodeId, + epoch_id: EpochId, + ) -> Result { + self.node_routing_score(node_id, epoch_id).await + } + + async fn get_batch_node_scores( + &self, + node_ids: Vec, + epoch_id: EpochId, + ) -> Result { + let mut scores = HashMap::new(); + + let epoch_timestamp = self.epoch_id_unix_timestamp(epoch_id).await?; + for node_id in node_ids { + scores.insert( + node_id, + self.get_node_routing_score_with_unix_timestamp(node_id, epoch_id, epoch_timestamp) + .await, + ); + } + + Ok(NodesRoutingScores { inner: scores }) + } +} diff --git a/nym-api/src/node_status_api/cache/config_score.rs b/nym-api/src/node_status_api/cache/config_score.rs new file mode 100644 index 00000000000..30f24d0b40f --- /dev/null +++ b/nym-api/src/node_status_api/cache/config_score.rs @@ -0,0 +1,63 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::mixnet_contract_cache::cache::data::ConfigScoreData; +use nym_api_requests::models::{ConfigScore, NymNodeDescription}; +use nym_contracts_common::NaiveFloat; +use nym_mixnet_contract_common::VersionScoreFormulaParams; + +fn versions_behind_factor_to_config_score( + versions_behind: u32, + params: VersionScoreFormulaParams, +) -> f64 { + let penalty = params.penalty.naive_to_f64(); + let scaling = params.penalty_scaling.naive_to_f64(); + + // version_score = penalty ^ (num_versions_behind ^ penalty_scaling) + penalty.powf((versions_behind as f64).powf(scaling)) +} + +pub(crate) fn calculate_config_score( + config_score_data: &ConfigScoreData, + described_data: Option<&NymNodeDescription>, +) -> ConfigScore { + let Some(described) = described_data else { + return ConfigScore::unavailable(); + }; + + let node_version = &described.description.build_information.build_version; + let Ok(reported_semver) = node_version.parse::() else { + return ConfigScore::bad_semver(); + }; + let versions_behind = config_score_data + .config_score_params + .version_weights + .versions_behind_factor( + &reported_semver, + &config_score_data.nym_node_version_history, + ); + + let runs_nym_node = described.description.build_information.binary_name == "nym-node"; + let accepted_terms_and_conditions = described + .description + .auxiliary_details + .accepted_operator_terms_and_conditions; + + let version_score = if !runs_nym_node || !accepted_terms_and_conditions { + 0. + } else { + versions_behind_factor_to_config_score( + versions_behind, + config_score_data + .config_score_params + .version_score_formula_params, + ) + }; + + ConfigScore::new( + version_score, + versions_behind, + accepted_terms_and_conditions, + runs_nym_node, + ) +} diff --git a/nym-api/src/node_status_api/cache/mod.rs b/nym-api/src/node_status_api/cache/mod.rs index 01d1be13bba..73aca9c848a 100644 --- a/nym-api/src/node_status_api/cache/mod.rs +++ b/nym-api/src/node_status_api/cache/mod.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only use self::data::NodeStatusCacheData; +use crate::node_performance::provider::PerformanceRetrievalFailure; use crate::support::caching::cache::UninitialisedCache; use crate::support::caching::Cache; use nym_api_requests::models::{GatewayBondAnnotated, MixNodeBondAnnotated, NodeAnnotation}; @@ -16,9 +17,9 @@ use tracing::error; const CACHE_TIMEOUT_MS: u64 = 100; +mod config_score; pub mod data; mod inclusion_probabilities; -mod node_sets; pub mod refresher; #[derive(Debug, Error)] @@ -28,6 +29,9 @@ enum NodeStatusCacheError { #[error("the self-described cache data is not available")] UnavailableDescribedCache, + + #[error(transparent)] + PerformanceRetrievalFailure(#[from] PerformanceRetrievalFailure), } impl From for NodeStatusCacheError { diff --git a/nym-api/src/node_status_api/cache/node_sets.rs b/nym-api/src/node_status_api/cache/node_sets.rs deleted file mode 100644 index 09ea897c33f..00000000000 --- a/nym-api/src/node_status_api/cache/node_sets.rs +++ /dev/null @@ -1,385 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::node_describe_cache::cache::DescribedNodes; -use crate::node_status_api::helpers::RewardedSetStatus; -use crate::node_status_api::models::Uptime; -use crate::node_status_api::reward_estimate::{compute_apy_from_reward, compute_reward_estimate}; -use crate::nym_contract_cache::cache::data::ConfigScoreData; -use crate::support::legacy_helpers::legacy_host_to_ips_and_hostname; -use crate::support::storage::NymApiStorage; -use nym_api_requests::legacy::{LegacyGatewayBondWithId, LegacyMixNodeDetailsWithLayer}; -use nym_api_requests::models::{ - ConfigScore, DescribedNodeType, DetailedNodePerformance, GatewayBondAnnotated, - MixNodeBondAnnotated, NodeAnnotation, NodePerformance, NymNodeDescription, RoutingScore, -}; -use nym_contracts_common::NaiveFloat; -use nym_mixnet_contract_common::{Interval, NodeId, VersionScoreFormulaParams}; -use nym_mixnet_contract_common::{NymNodeDetails, RewardingParams}; -use nym_topology::CachedEpochRewardedSet; -use std::collections::HashMap; -use tracing::trace; - -pub(super) async fn get_mixnode_reliability_from_storage( - storage: &NymApiStorage, - mix_id: NodeId, - epoch: Interval, -) -> Option { - storage - .get_average_mixnode_reliability_in_the_last_24hrs( - mix_id, - epoch.current_epoch_end_unix_timestamp(), - ) - .await - .ok() -} - -pub(super) async fn get_gateway_reliability_from_storage( - storage: &NymApiStorage, - node_id: NodeId, - epoch: Interval, -) -> Option { - storage - .get_average_gateway_reliability_in_the_last_24hrs( - node_id, - epoch.current_epoch_end_unix_timestamp(), - ) - .await - .ok() -} - -pub(super) async fn get_node_reliability_from_storage( - storage: &NymApiStorage, - node_id: NodeId, - epoch: Interval, -) -> Option { - storage - .get_average_node_reliability_in_the_last_24hrs( - node_id, - epoch.current_epoch_end_unix_timestamp(), - ) - .await - .ok() -} - -async fn get_routing_score( - storage: &NymApiStorage, - node_id: NodeId, - typ: DescribedNodeType, - epoch: Interval, -) -> RoutingScore { - let maybe_reliability = match typ { - DescribedNodeType::LegacyMixnode => { - get_mixnode_reliability_from_storage(storage, node_id, epoch).await - } - DescribedNodeType::LegacyGateway => { - get_gateway_reliability_from_storage(storage, node_id, epoch).await - } - DescribedNodeType::NymNode => { - get_node_reliability_from_storage(storage, node_id, epoch).await - } - }; - // reliability: 0-100 - // score: 0-1 - let reliability = maybe_reliability.unwrap_or_default(); - let score = reliability / 100.; - - trace!("reliability for {node_id}: {maybe_reliability:?}. routing score: {score}"); - RoutingScore::new(score as f64) -} - -fn versions_behind_factor_to_config_score( - versions_behind: u32, - params: VersionScoreFormulaParams, -) -> f64 { - let penalty = params.penalty.naive_to_f64(); - let scaling = params.penalty_scaling.naive_to_f64(); - - // version_score = penalty ^ (num_versions_behind ^ penalty_scaling) - penalty.powf((versions_behind as f64).powf(scaling)) -} - -fn calculate_config_score( - config_score_data: &ConfigScoreData, - described_data: Option<&NymNodeDescription>, -) -> ConfigScore { - let Some(described) = described_data else { - return ConfigScore::unavailable(); - }; - - let node_version = &described.description.build_information.build_version; - let Ok(reported_semver) = node_version.parse::() else { - return ConfigScore::bad_semver(); - }; - let versions_behind = config_score_data - .config_score_params - .version_weights - .versions_behind_factor( - &reported_semver, - &config_score_data.nym_node_version_history, - ); - - let runs_nym_node = described.description.build_information.binary_name == "nym-node"; - let accepted_terms_and_conditions = described - .description - .auxiliary_details - .accepted_operator_terms_and_conditions; - - let version_score = if !runs_nym_node || !accepted_terms_and_conditions { - 0. - } else { - versions_behind_factor_to_config_score( - versions_behind, - config_score_data - .config_score_params - .version_score_formula_params, - ) - }; - - ConfigScore::new( - version_score, - versions_behind, - accepted_terms_and_conditions, - runs_nym_node, - ) -} - -// TODO: this might have to be moved to a different file if other places also rely on this functionality -fn get_rewarded_set_status( - rewarded_set: &CachedEpochRewardedSet, - node_id: NodeId, -) -> RewardedSetStatus { - if rewarded_set.is_standby(&node_id) { - RewardedSetStatus::Standby - } else if rewarded_set.is_active_mixnode(&node_id) { - RewardedSetStatus::Active - } else { - RewardedSetStatus::Inactive - } -} - -#[deprecated] -pub(super) async fn annotate_legacy_mixnodes_nodes_with_details( - storage: &NymApiStorage, - mixnodes: Vec, - interval_reward_params: RewardingParams, - current_interval: Interval, - rewarded_set: &CachedEpochRewardedSet, -) -> HashMap { - let mut annotated = HashMap::new(); - for mixnode in mixnodes { - let stake_saturation = mixnode - .rewarding_details - .bond_saturation(&interval_reward_params); - - let uncapped_stake_saturation = mixnode - .rewarding_details - .uncapped_bond_saturation(&interval_reward_params); - - let rewarded_set_status = get_rewarded_set_status(rewarded_set, mixnode.mix_id()); - - // If the performance can't be obtained, because the nym-api was not started with - // the monitoring (and hence, storage), then reward estimates will be all zero - let performance = - get_mixnode_reliability_from_storage(storage, mixnode.mix_id(), current_interval) - .await - .map(Uptime::new) - .map(Into::into) - .unwrap_or_default(); - - let reward_estimate = compute_reward_estimate( - &mixnode, - performance, - rewarded_set_status, - interval_reward_params, - current_interval, - ); - - let node_performance = storage - .construct_mixnode_report(mixnode.mix_id()) - .await - .map(NodePerformance::from) - .ok() - .unwrap_or_default(); - - let Some((ip_addresses, _)) = - legacy_host_to_ips_and_hostname(&mixnode.bond_information.mix_node.host) - else { - continue; - }; - - let (estimated_operator_apy, estimated_delegators_apy) = - compute_apy_from_reward(&mixnode, reward_estimate, current_interval); - - annotated.insert( - mixnode.mix_id(), - MixNodeBondAnnotated { - // all legacy nodes are always blacklisted - blacklisted: true, - mixnode_details: mixnode, - stake_saturation, - uncapped_stake_saturation, - performance, - node_performance, - estimated_operator_apy, - estimated_delegators_apy, - ip_addresses, - }, - ); - } - annotated -} - -#[deprecated] -pub(crate) async fn annotate_legacy_gateways_with_details( - storage: &NymApiStorage, - gateway_bonds: Vec, - current_interval: Interval, -) -> HashMap { - let mut annotated = HashMap::new(); - for gateway_bond in gateway_bonds { - let performance = - get_gateway_reliability_from_storage(storage, gateway_bond.node_id, current_interval) - .await - .map(Uptime::new) - .map(Into::into) - .unwrap_or_default(); - - let node_performance = storage - .construct_gateway_report(gateway_bond.node_id) - .await - .map(NodePerformance::from) - .ok() - .unwrap_or_default(); - - let Some((ip_addresses, _)) = - legacy_host_to_ips_and_hostname(&gateway_bond.bond.gateway.host) - else { - continue; - }; - - annotated.insert( - gateway_bond.node_id, - GatewayBondAnnotated { - // all legacy nodes are always blacklisted - blacklisted: true, - gateway_bond, - self_described: None, - performance, - node_performance, - ip_addresses, - }, - ); - } - annotated -} - -#[allow(clippy::too_many_arguments)] -pub(crate) async fn produce_node_annotations( - storage: &NymApiStorage, - config_score_data: &ConfigScoreData, - legacy_mixnodes: &[LegacyMixNodeDetailsWithLayer], - legacy_gateways: &[LegacyGatewayBondWithId], - nym_nodes: &[NymNodeDetails], - rewarded_set: &CachedEpochRewardedSet, - current_interval: Interval, - described_nodes: &DescribedNodes, -) -> HashMap { - let mut annotations = HashMap::new(); - - for legacy_mix in legacy_mixnodes { - let node_id = legacy_mix.mix_id(); - - let routing_score = get_routing_score( - storage, - node_id, - DescribedNodeType::LegacyMixnode, - current_interval, - ) - .await; - let config_score = - calculate_config_score(config_score_data, described_nodes.get_node(&node_id)); - - let performance = routing_score.score * config_score.score; - // map it from 0-1 range into 0-100 - let scaled_performance = performance * 100.; - let legacy_performance = Uptime::new(scaled_performance as f32).into(); - - annotations.insert( - legacy_mix.mix_id(), - NodeAnnotation { - last_24h_performance: legacy_performance, - current_role: rewarded_set.role(legacy_mix.mix_id()).map(|r| r.into()), - detailed_performance: DetailedNodePerformance::new( - performance, - routing_score, - config_score, - ), - }, - ); - } - - for legacy_gateway in legacy_gateways { - let node_id = legacy_gateway.node_id; - let routing_score = get_routing_score( - storage, - node_id, - DescribedNodeType::LegacyGateway, - current_interval, - ) - .await; - let config_score = - calculate_config_score(config_score_data, described_nodes.get_node(&node_id)); - - let performance = routing_score.score * config_score.score; - // map it from 0-1 range into 0-100 - let scaled_performance = performance * 100.; - let legacy_performance = Uptime::new(scaled_performance as f32).into(); - - annotations.insert( - legacy_gateway.node_id, - NodeAnnotation { - last_24h_performance: legacy_performance, - current_role: rewarded_set.role(legacy_gateway.node_id).map(|r| r.into()), - detailed_performance: DetailedNodePerformance::new( - performance, - routing_score, - config_score, - ), - }, - ); - } - - for nym_node in nym_nodes { - let node_id = nym_node.node_id(); - let routing_score = get_routing_score( - storage, - node_id, - DescribedNodeType::NymNode, - current_interval, - ) - .await; - let config_score = - calculate_config_score(config_score_data, described_nodes.get_node(&node_id)); - - let performance = routing_score.score * config_score.score; - // map it from 0-1 range into 0-100 - let scaled_performance = performance * 100.; - let legacy_performance = Uptime::new(scaled_performance as f32).into(); - - annotations.insert( - nym_node.node_id(), - NodeAnnotation { - last_24h_performance: legacy_performance, - current_role: rewarded_set.role(nym_node.node_id()).map(|r| r.into()), - detailed_performance: DetailedNodePerformance::new( - performance, - routing_score, - config_score, - ), - }, - ); - } - - annotations -} diff --git a/nym-api/src/node_status_api/cache/refresher.rs b/nym-api/src/node_status_api/cache/refresher.rs index 0666b84594a..52321556250 100644 --- a/nym-api/src/node_status_api/cache/refresher.rs +++ b/nym-api/src/node_status_api/cache/refresher.rs @@ -2,15 +2,27 @@ // SPDX-License-Identifier: GPL-3.0-only use super::NodeStatusCache; +use crate::mixnet_contract_cache::cache::data::ConfigScoreData; use crate::node_describe_cache::cache::DescribedNodes; -use crate::node_status_api::cache::node_sets::produce_node_annotations; +use crate::node_performance::provider::{NodePerformanceProvider, NodesRoutingScores}; +use crate::node_status_api::cache::config_score::calculate_config_score; +use crate::node_status_api::models::Uptime; use crate::support::caching::cache::SharedCache; +use crate::support::legacy_helpers::legacy_host_to_ips_and_hostname; use crate::{ - node_status_api::cache::NodeStatusCacheError, nym_contract_cache::cache::NymContractCache, - storage::NymApiStorage, support::caching::CacheNotification, + mixnet_contract_cache::cache::MixnetContractCache, + node_status_api::cache::NodeStatusCacheError, support::caching::CacheNotification, }; use ::time::OffsetDateTime; +use cosmwasm_std::Decimal; +use nym_api_requests::legacy::{LegacyGatewayBondWithId, LegacyMixNodeDetailsWithLayer}; +use nym_api_requests::models::{ + DetailedNodePerformance, GatewayBondAnnotated, MixNodeBondAnnotated, NodeAnnotation, + NodePerformance, +}; +use nym_mixnet_contract_common::{NodeId, NymNodeDetails, RewardingParams}; use nym_task::TaskClient; +use nym_topology::CachedEpochRewardedSet; use std::collections::HashMap; use std::time::Duration; use tokio::sync::watch; @@ -24,31 +36,32 @@ pub struct NodeStatusCacheRefresher { fallback_caching_interval: Duration, // Sources for when refreshing data - contract_cache: NymContractCache, + mixnet_contract_cache: MixnetContractCache, described_cache: SharedCache, - contract_cache_listener: watch::Receiver, + mixnet_contract_cache_listener: watch::Receiver, describe_cache_listener: watch::Receiver, - storage: NymApiStorage, + + performance_provider: Box, } impl NodeStatusCacheRefresher { pub(crate) fn new( cache: NodeStatusCache, fallback_caching_interval: Duration, - contract_cache: NymContractCache, + contract_cache: MixnetContractCache, described_cache: SharedCache, contract_cache_listener: watch::Receiver, describe_cache_listener: watch::Receiver, - storage: NymApiStorage, + performance_provider: Box, ) -> Self { Self { cache, fallback_caching_interval, - contract_cache, + mixnet_contract_cache: contract_cache, described_cache, - contract_cache_listener, + mixnet_contract_cache_listener: contract_cache_listener, describe_cache_listener, - storage, + performance_provider, } } @@ -63,7 +76,7 @@ impl NodeStatusCacheRefresher { trace!("NodeStatusCacheRefresher: Received shutdown"); } // Update node status cache when the contract cache / describe cache is updated - Ok(_) = self.contract_cache_listener.changed() => { + Ok(_) = self.mixnet_contract_cache_listener.changed() => { tokio::select! { _ = self.maybe_refresh(&mut fallback_interval, &mut last_update) => (), _ = shutdown.recv() => { @@ -95,7 +108,8 @@ impl NodeStatusCacheRefresher { } fn caches_available(&self) -> bool { - let contract_cache = *self.contract_cache_listener.borrow() != CacheNotification::Start; + let contract_cache = + *self.mixnet_contract_cache_listener.borrow() != CacheNotification::Start; let describe_cache = *self.describe_cache_listener.borrow() != CacheNotification::Start; let available = contract_cache && describe_cache; @@ -130,19 +144,201 @@ impl NodeStatusCacheRefresher { fallback_interval.reset(); } + #[allow(clippy::too_many_arguments)] + pub(crate) async fn produce_node_annotations( + &self, + config_score_data: &ConfigScoreData, + routing_scores: &NodesRoutingScores, + legacy_mixnodes: &[LegacyMixNodeDetailsWithLayer], + legacy_gateways: &[LegacyGatewayBondWithId], + nym_nodes: &[NymNodeDetails], + rewarded_set: &CachedEpochRewardedSet, + described_nodes: &DescribedNodes, + ) -> HashMap { + let mut annotations = HashMap::new(); + + for legacy_mix in legacy_mixnodes { + let node_id = legacy_mix.mix_id(); + let routing_score = routing_scores.get_or_log(node_id); + + let config_score = + calculate_config_score(config_score_data, described_nodes.get_node(&node_id)); + + let performance = routing_score.score * config_score.score; + // map it from 0-1 range into 0-100 + let scaled_performance = performance * 100.; + let legacy_performance = Uptime::new(scaled_performance as f32).into(); + + annotations.insert( + legacy_mix.mix_id(), + NodeAnnotation { + last_24h_performance: legacy_performance, + current_role: rewarded_set.role(legacy_mix.mix_id()).map(|r| r.into()), + detailed_performance: DetailedNodePerformance::new( + performance, + routing_score, + config_score, + ), + }, + ); + } + + for legacy_gateway in legacy_gateways { + let node_id = legacy_gateway.node_id; + let routing_score = routing_scores.get_or_log(node_id); + let config_score = + calculate_config_score(config_score_data, described_nodes.get_node(&node_id)); + + let performance = routing_score.score * config_score.score; + // map it from 0-1 range into 0-100 + let scaled_performance = performance * 100.; + let legacy_performance = Uptime::new(scaled_performance as f32).into(); + + annotations.insert( + legacy_gateway.node_id, + NodeAnnotation { + last_24h_performance: legacy_performance, + current_role: rewarded_set.role(legacy_gateway.node_id).map(|r| r.into()), + detailed_performance: DetailedNodePerformance::new( + performance, + routing_score, + config_score, + ), + }, + ); + } + + for nym_node in nym_nodes { + let node_id = nym_node.node_id(); + let routing_score = routing_scores.get_or_log(node_id); + let config_score = + calculate_config_score(config_score_data, described_nodes.get_node(&node_id)); + + let performance = routing_score.score * config_score.score; + // map it from 0-1 range into 0-100 + let scaled_performance = performance * 100.; + let legacy_performance = Uptime::new(scaled_performance as f32).into(); + + annotations.insert( + nym_node.node_id(), + NodeAnnotation { + last_24h_performance: legacy_performance, + current_role: rewarded_set.role(nym_node.node_id()).map(|r| r.into()), + detailed_performance: DetailedNodePerformance::new( + performance, + routing_score, + config_score, + ), + }, + ); + } + + annotations + } + + #[deprecated] + pub(super) async fn annotate_legacy_mixnodes_nodes_with_details( + &self, + mixnodes: Vec, + routing_scores: &NodesRoutingScores, + interval_reward_params: RewardingParams, + ) -> HashMap { + let mut annotated = HashMap::new(); + for mixnode in mixnodes { + let stake_saturation = mixnode + .rewarding_details + .bond_saturation(&interval_reward_params); + + let uncapped_stake_saturation = mixnode + .rewarding_details + .uncapped_bond_saturation(&interval_reward_params); + + let score = routing_scores.get_or_log(mixnode.mix_id()); + let legacy_report = NodePerformance { + most_recent: score.legacy_performance(), + last_hour: score.legacy_performance(), + last_24h: score.legacy_performance(), + }; + + let Some((ip_addresses, _)) = + legacy_host_to_ips_and_hostname(&mixnode.bond_information.mix_node.host) + else { + continue; + }; + + // legacy node will never get rewarded + let estimated_operator_apy = Decimal::zero(); + let estimated_delegators_apy = Decimal::zero(); + + annotated.insert( + mixnode.mix_id(), + MixNodeBondAnnotated { + // all legacy nodes are always blacklisted + blacklisted: true, + mixnode_details: mixnode, + stake_saturation, + uncapped_stake_saturation, + performance: score.legacy_performance(), + node_performance: legacy_report, + estimated_operator_apy, + estimated_delegators_apy, + ip_addresses, + }, + ); + } + annotated + } + + #[deprecated] + pub(crate) async fn annotate_legacy_gateways_with_details( + &self, + gateway_bonds: Vec, + routing_scores: &NodesRoutingScores, + ) -> HashMap { + let mut annotated = HashMap::new(); + for gateway_bond in gateway_bonds { + let score = routing_scores.get_or_log(gateway_bond.node_id); + let legacy_report = NodePerformance { + most_recent: score.legacy_performance(), + last_hour: score.legacy_performance(), + last_24h: score.legacy_performance(), + }; + + let Some((ip_addresses, _)) = + legacy_host_to_ips_and_hostname(&gateway_bond.bond.gateway.host) + else { + continue; + }; + + annotated.insert( + gateway_bond.node_id, + GatewayBondAnnotated { + // all legacy nodes are always blacklisted + blacklisted: true, + gateway_bond, + self_described: None, + performance: score.legacy_performance(), + node_performance: legacy_report, + ip_addresses, + }, + ); + } + annotated + } + /// Refreshes the node status cache by fetching the latest data from the contract cache #[allow(deprecated)] async fn refresh(&self) -> Result<(), NodeStatusCacheError> { info!("Updating node status cache"); // Fetch contract cache data to work with - let mixnode_details = self.contract_cache.legacy_mixnodes_all().await; - let interval_reward_params = self.contract_cache.interval_reward_params().await?; - let current_interval = self.contract_cache.current_interval().await?; - let rewarded_set = self.contract_cache.rewarded_set_owned().await?; - let gateway_bonds = self.contract_cache.legacy_gateways_all().await; - let nym_nodes = self.contract_cache.nym_nodes().await; - let config_score_data = self.contract_cache.maybe_config_score_data().await?; + let mixnode_details = self.mixnet_contract_cache.legacy_mixnodes_all().await; + let interval_reward_params = self.mixnet_contract_cache.interval_reward_params().await?; + let current_interval = self.mixnet_contract_cache.current_interval().await?; + let rewarded_set = self.mixnet_contract_cache.rewarded_set_owned().await?; + let gateway_bonds = self.mixnet_contract_cache.legacy_gateways_all().await; + let nym_nodes = self.mixnet_contract_cache.nym_nodes().await; + let config_score_data = self.mixnet_contract_cache.maybe_config_score_data().await?; // Compute inclusion probabilities // (all legacy mixnodes have 0% chance of being selected) @@ -157,37 +353,48 @@ impl NodeStatusCacheRefresher { legacy_gateway_mapping.insert(gateway.identity().clone(), gateway.node_id); } + let all_ids = mixnode_details + .iter() + .map(|m| m.bond_information.mix_id) + .chain( + gateway_bonds + .iter() + .map(|g| g.node_id) + .chain(nym_nodes.iter().map(|n| n.bond_information.node_id)), + ) + .collect::>(); + + // note: any internal errors imply failures for that node in particular + let routing_scores = self + .performance_provider + .get_batch_node_scores(all_ids, current_interval.current_epoch_absolute_id()) + .await?; + // Create annotated data - let node_annotations = produce_node_annotations( - &self.storage, - &config_score_data, - &mixnode_details, - &gateway_bonds, - &nym_nodes, - &rewarded_set, - current_interval, - &described, - ) - .await; - - let mixnodes_annotated = - crate::node_status_api::cache::node_sets::annotate_legacy_mixnodes_nodes_with_details( - &self.storage, - mixnode_details, - interval_reward_params, - current_interval, + let node_annotations = self + .produce_node_annotations( + &config_score_data, + &routing_scores, + &mixnode_details, + &gateway_bonds, + &nym_nodes, &rewarded_set, + &described, ) .await; - let gateways_annotated = - crate::node_status_api::cache::node_sets::annotate_legacy_gateways_with_details( - &self.storage, - gateway_bonds, - current_interval, + let mixnodes_annotated = self + .annotate_legacy_mixnodes_nodes_with_details( + mixnode_details, + &routing_scores, + interval_reward_params, ) .await; + let gateways_annotated = self + .annotate_legacy_gateways_with_details(gateway_bonds, &routing_scores) + .await; + // Update the cache self.cache .update( diff --git a/nym-api/src/node_status_api/helpers.rs b/nym-api/src/node_status_api/helpers.rs index 9e4fcba62ff..91076e5963f 100644 --- a/nym-api/src/node_status_api/helpers.rs +++ b/nym-api/src/node_status_api/helpers.rs @@ -1,12 +1,10 @@ // Copyright 2021-2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use super::reward_estimate::compute_reward_estimate; use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; use crate::storage::NymApiStorage; use crate::support::caching::Cache; -use crate::{NodeStatusCache, NymContractCache}; -use cosmwasm_std::Decimal; +use crate::{MixnetContractCache, NodeStatusCache}; use nym_api_requests::models::{ ComputeRewardEstParam, GatewayBondAnnotated, GatewayCoreStatusResponse, GatewayStatusReportResponse, GatewayUptimeHistoryResponse, GatewayUptimeResponse, @@ -14,6 +12,7 @@ use nym_api_requests::models::{ MixnodeStatusResponse, MixnodeUptimeHistoryResponse, RewardEstimationResponse, StakeSaturationResponse, UptimeResponse, }; +use nym_mixnet_contract_common::rewarding::RewardEstimate; use nym_mixnet_contract_common::NodeId; pub(crate) enum RewardedSetStatus { @@ -90,7 +89,7 @@ pub(crate) async fn _gateway_report( pub(crate) async fn _gateway_uptime_history( storage: &NymApiStorage, - nym_contract_cache: &NymContractCache, + nym_contract_cache: &MixnetContractCache, identity: &str, ) -> AxumResult { let history = storage @@ -144,7 +143,7 @@ pub(crate) async fn _mixnode_report( pub(crate) async fn _mixnode_uptime_history( storage: &NymApiStorage, - nym_contract_cache: &NymContractCache, + nym_contract_cache: &MixnetContractCache, mix_id: NodeId, ) -> AxumResult { let history = storage @@ -179,7 +178,7 @@ pub(crate) async fn _mixnode_core_status_count( } pub(crate) async fn _get_mixnode_status( - cache: &NymContractCache, + cache: &MixnetContractCache, mix_id: NodeId, ) -> MixnodeStatusResponse { MixnodeStatusResponse { @@ -189,14 +188,15 @@ pub(crate) async fn _get_mixnode_status( pub(crate) async fn _get_mixnode_reward_estimation( status_cache: &NodeStatusCache, - contract_cache: &NymContractCache, + contract_cache: &MixnetContractCache, mix_id: NodeId, ) -> AxumResult { - let status = contract_cache.mixnode_status(mix_id).await; - let mixnode = status_cache + let _ = status_cache .mixnode_annotated(mix_id) .await .ok_or_else(|| AxumErrorResponse::not_found("mixnode bond not found"))?; + // legacy mixnode will never get any rewards + let reward_estimation = RewardEstimate::zero(); let reward_params = contract_cache.interval_reward_params().await?; let current_interval = contract_cache.current_interval().await?; @@ -205,14 +205,6 @@ pub(crate) async fn _get_mixnode_reward_estimation( // queries for `reward_params` and `current_interval`, but timestamp is only informative to begin with) let as_at = contract_cache.cache_timestamp().await; - let reward_estimation = compute_reward_estimate( - &mixnode.mixnode_details, - mixnode.performance, - status.into(), - reward_params, - current_interval, - ); - Ok(RewardEstimationResponse { estimation: reward_estimation, reward_params, @@ -222,16 +214,18 @@ pub(crate) async fn _get_mixnode_reward_estimation( } pub(crate) async fn _compute_mixnode_reward_estimation( - user_reward_param: &ComputeRewardEstParam, + _: &ComputeRewardEstParam, status_cache: &NodeStatusCache, - contract_cache: &NymContractCache, + contract_cache: &MixnetContractCache, mix_id: NodeId, ) -> AxumResult { - let mut mixnode = status_cache + let _ = status_cache .mixnode_annotated(mix_id) .await .ok_or_else(|| AxumErrorResponse::not_found("mixnode bond not found"))?; + let reward_estimation = RewardEstimate::zero(); + let reward_params = contract_cache.interval_reward_params().await?; let current_interval = contract_cache.current_interval().await?; @@ -239,60 +233,6 @@ pub(crate) async fn _compute_mixnode_reward_estimation( // queries for `reward_params` and `current_interval`, but timestamp is only informative to begin with) let as_at = contract_cache.cache_timestamp().await; - // For these parameters we either use the provided ones, or fall back to the system ones - let performance = user_reward_param.performance.unwrap_or(mixnode.performance); - - let status = match user_reward_param.active_in_rewarded_set { - Some(true) => RewardedSetStatus::Active, - Some(false) => RewardedSetStatus::Standby, - None => { - let actual_status = contract_cache.mixnode_status(mix_id).await; - actual_status.into() - } - }; - - if let Some(pledge_amount) = user_reward_param.pledge_amount { - mixnode.mixnode_details.rewarding_details.operator = - Decimal::from_ratio(pledge_amount, 1u64); - } - if let Some(total_delegation) = user_reward_param.total_delegation { - mixnode.mixnode_details.rewarding_details.delegates = - Decimal::from_ratio(total_delegation, 1u64); - } - - if let Some(profit_margin_percent) = user_reward_param.profit_margin_percent { - mixnode - .mixnode_details - .rewarding_details - .cost_params - .profit_margin_percent = profit_margin_percent; - } - - if let Some(interval_operating_cost) = &user_reward_param.interval_operating_cost { - mixnode - .mixnode_details - .rewarding_details - .cost_params - .interval_operating_cost = interval_operating_cost.clone(); - } - - if mixnode.mixnode_details.rewarding_details.operator - + mixnode.mixnode_details.rewarding_details.delegates - > reward_params.interval.staking_supply - { - return Err(AxumErrorResponse::unprocessable_entity( - "Pledge plus delegation too large", - )); - } - - let reward_estimation = compute_reward_estimate( - &mixnode.mixnode_details, - performance, - status, - reward_params, - current_interval, - ); - Ok(RewardEstimationResponse { estimation: reward_estimation, reward_params, @@ -303,7 +243,7 @@ pub(crate) async fn _compute_mixnode_reward_estimation( pub(crate) async fn _get_mixnode_stake_saturation( status_cache: &NodeStatusCache, - contract_cache: &NymContractCache, + contract_cache: &MixnetContractCache, mix_id: NodeId, ) -> AxumResult { let mixnode = status_cache @@ -411,7 +351,7 @@ pub(crate) async fn _get_mixnodes_detailed_unfiltered( pub(crate) async fn _get_rewarded_set_legacy_mixnodes_detailed( status_cache: &NodeStatusCache, - contract_cache: &NymContractCache, + contract_cache: &MixnetContractCache, ) -> Vec { let Some(rewarded_set) = contract_cache.rewarded_set().await else { return Vec::new(); @@ -429,7 +369,7 @@ pub(crate) async fn _get_rewarded_set_legacy_mixnodes_detailed( pub(crate) async fn _get_active_set_legacy_mixnodes_detailed( status_cache: &NodeStatusCache, - contract_cache: &NymContractCache, + contract_cache: &MixnetContractCache, ) -> Vec { let Some(rewarded_set) = contract_cache.rewarded_set().await else { return Vec::new(); diff --git a/nym-api/src/node_status_api/mod.rs b/nym-api/src/node_status_api/mod.rs index 0b2a0db4786..1b3d97f8394 100644 --- a/nym-api/src/node_status_api/mod.rs +++ b/nym-api/src/node_status_api/mod.rs @@ -3,11 +3,12 @@ use self::cache::refresher::NodeStatusCacheRefresher; use crate::node_describe_cache::cache::DescribedNodes; +use crate::node_performance::provider::NodePerformanceProvider; use crate::support::caching::cache::SharedCache; use crate::support::config; use crate::{ - nym_contract_cache::cache::NymContractCache, - support::{self, storage}, + mixnet_contract_cache::cache::MixnetContractCache, + support::{self}, }; pub(crate) use cache::NodeStatusCache; use nym_task::TaskManager; @@ -18,7 +19,6 @@ pub(crate) mod cache; pub(crate) mod handlers; pub(crate) mod helpers; pub(crate) mod models; -pub(crate) mod reward_estimate; pub(crate) mod uptime_updater; pub(crate) mod utils; @@ -33,10 +33,10 @@ pub(crate) const ONE_DAY: Duration = Duration::from_secs(86400); #[allow(clippy::too_many_arguments)] pub(crate) fn start_cache_refresh( config: &config::NodeStatusAPI, - nym_contract_cache_state: &NymContractCache, + nym_contract_cache_state: &MixnetContractCache, described_cache: &SharedCache, node_status_cache_state: &NodeStatusCache, - storage: storage::NymApiStorage, + performance_provider: Box, nym_contract_cache_listener: watch::Receiver, described_cache_cache_listener: watch::Receiver, shutdown: &TaskManager, @@ -48,7 +48,7 @@ pub(crate) fn start_cache_refresh( described_cache.clone(), nym_contract_cache_listener, described_cache_cache_listener, - storage, + performance_provider, ); let shutdown_listener = shutdown.subscribe(); tokio::spawn(async move { nym_api_cache_refresher.run(shutdown_listener).await }); diff --git a/nym-api/src/node_status_api/reward_estimate.rs b/nym-api/src/node_status_api/reward_estimate.rs deleted file mode 100644 index 6b1798227d0..00000000000 --- a/nym-api/src/node_status_api/reward_estimate.rs +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright 2021-2023 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::node_status_api::helpers::RewardedSetStatus; -use cosmwasm_std::Decimal; -use nym_api_requests::legacy::LegacyMixNodeDetailsWithLayer; -use nym_mixnet_contract_common::reward_params::{ - NodeRewardingParameters, Performance, RewardingParams, -}; -use nym_mixnet_contract_common::rewarding::RewardEstimate; -use nym_mixnet_contract_common::Interval; - -fn compute_apy(epochs_in_year: Decimal, reward: Decimal, pledge_amount: Decimal) -> Decimal { - if pledge_amount.is_zero() { - return Decimal::zero(); - } - let hundred = Decimal::from_ratio(100u32, 1u32); - - epochs_in_year * hundred * reward / pledge_amount -} - -pub fn compute_reward_estimate( - mixnode: &LegacyMixNodeDetailsWithLayer, - performance: Performance, - rewarded_set_status: RewardedSetStatus, - rewarding_params: RewardingParams, - interval: Interval, -) -> RewardEstimate { - if mixnode.is_unbonding() { - return Default::default(); - } - - if performance.is_zero() { - return Default::default(); - } - - let is_active = match rewarded_set_status { - RewardedSetStatus::Active => true, - RewardedSetStatus::Standby => false, - RewardedSetStatus::Inactive => return Default::default(), - }; - - let work_factor = if is_active { - rewarding_params.active_node_work() - } else { - rewarding_params.standby_node_work() - }; - - let node_reward_params = NodeRewardingParameters { - performance, - work_factor, - }; - let node_reward = mixnode - .rewarding_details - .node_reward(&rewarding_params, node_reward_params); - - let node_cost = mixnode - .rewarding_details - .cost_params - .epoch_operating_cost(interval.epochs_in_interval()) - * performance; - - let reward_distribution = mixnode.rewarding_details.determine_reward_split( - node_reward, - performance, - interval.epochs_in_interval(), - ); - - RewardEstimate { - total_node_reward: node_reward, - operator: reward_distribution.operator, - delegates: reward_distribution.delegates, - operating_cost: node_cost, - } -} - -pub fn compute_apy_from_reward( - mixnode: &LegacyMixNodeDetailsWithLayer, - reward_estimate: RewardEstimate, - interval: Interval, -) -> (Decimal, Decimal) { - let epochs_in_year = Decimal::from_ratio(interval.epoch_length_secs(), 3600u64 * 24 * 365); - - let operator = mixnode.rewarding_details.operator; - let total_delegations = mixnode.rewarding_details.delegates; - let estimated_operator_apy = compute_apy(epochs_in_year, reward_estimate.operator, operator); - let estimated_delegators_apy = - compute_apy(epochs_in_year, reward_estimate.delegates, total_delegations); - (estimated_operator_apy, estimated_delegators_apy) -} diff --git a/nym-api/src/support/caching/cache.rs b/nym-api/src/support/caching/cache.rs index 2f782f6098f..4923b70a9fd 100644 --- a/nym-api/src/support/caching/cache.rs +++ b/nym-api/src/support/caching/cache.rs @@ -32,7 +32,44 @@ impl SharedCache { SharedCache::default() } - pub(crate) async fn try_update(&self, value: impl Into, typ: &str) -> Result<(), T> { + pub(crate) fn new_with_value(value: T) -> Self { + SharedCache(Arc::new(RwLock::new(CachedItem { + inner: Some(Cache::new(value)), + }))) + } + + pub(crate) async fn try_update_value( + &self, + update: S, + update_fn: impl Fn(&mut T, S), + typ: &str, + ) -> Result<(), S> + where + S: Into, + { + let update_value = update; + let mut guard = match tokio::time::timeout(Duration::from_millis(200), self.0.write()).await + { + Ok(guard) => guard, + Err(_) => { + debug!("failed to obtain write permit for {typ} cache"); + return Err(update_value); + } + }; + + if let Some(ref mut existing) = guard.inner { + existing.update(update_value, update_fn); + } else { + guard.inner = Some(Cache::new(update_value.into())) + }; + Ok(()) + } + + pub(crate) async fn try_overwrite_old_value( + &self, + value: impl Into, + typ: &str, + ) -> Result<(), T> { let value = value.into(); let mut guard = match tokio::time::timeout(Duration::from_millis(200), self.0.write()).await { @@ -107,6 +144,19 @@ impl From> for CachedItem { } } +// specialised variant of `Cache` for holding maps of values that allow updates to individual entries + +/* + pub(crate) fn partial_update(&mut self, partial_value: impl Into, update_fn: F) + where + F: FnOnce(&mut T, S), + { + update_fn(&mut self.value, partial_value.into()); + self.as_at = OffsetDateTime::now_utc() + } + +*/ + // don't use this directly! // opt for SharedCache instead pub struct Cache { @@ -166,6 +216,11 @@ impl Cache { } } + pub(crate) fn update(&mut self, update: S, update_fn: impl Fn(&mut T, S)) { + update_fn(&mut self.value, update); + self.as_at = OffsetDateTime::now_utc(); + } + // ugh. I hate to expose it, but it'd have broken pre-existing code pub(crate) fn unchecked_update(&mut self, value: impl Into) { self.value = value.into(); diff --git a/nym-api/src/support/caching/refresher.rs b/nym-api/src/support/caching/refresher.rs index 05e6a3a3d49..b26a918f736 100644 --- a/nym-api/src/support/caching/refresher.rs +++ b/nym-api/src/support/caching/refresher.rs @@ -9,7 +9,7 @@ use std::sync::Arc; use std::time::Duration; use tokio::sync::{watch, Notify}; use tokio::time::interval; -use tracing::{error, info, trace, warn}; +use tracing::{debug, error, info, trace, warn}; pub(crate) type CacheUpdateWatcher = watch::Receiver; @@ -28,13 +28,24 @@ impl Default for RefreshRequester { } } -pub struct CacheRefresher { +/// Explanation on generics: +/// the internal SharedCache can be updated in two ways +/// by default CacheItemProvider will just provide a T and the internal values will be swapped +/// however, an alternative is to make it provide another value of type S with an explicit update closure +/// this way the cache will be updated with a custom method mutating the existing value +/// the reason for this is to allow partial updates of maps, where we might not want to retrieve +/// the entire value, and we might want to just insert a new entry +pub struct CacheRefresher { name: String, refreshing_interval: Duration, refresh_notification_sender: watch::Sender, + // it's not really THAT complex... it's just a boxed function + #[allow(clippy::type_complexity)] + update_fn: Option>, + // TODO: the Send + Sync bounds are only required for the `start` method. could we maybe make it less restrictive? - provider: Box + Send + Sync>, + provider: Box + Send + Sync>, shared_cache: SharedCache, refresh_requester: RefreshRequester, } @@ -46,15 +57,16 @@ pub(crate) trait CacheItemProvider { async fn wait_until_ready(&self) {} - async fn try_refresh(&self) -> Result; + async fn try_refresh(&mut self) -> Result, Self::Error>; } -impl CacheRefresher +impl CacheRefresher where E: std::error::Error, + S: Into, { - pub(crate) fn new( - item_provider: Box + Send + Sync>, + pub(crate) fn new_boxed( + item_provider: Box + Send + Sync>, refreshing_interval: Duration, ) -> Self { let (refresh_notification_sender, _) = watch::channel(CacheNotification::Start); @@ -63,14 +75,22 @@ where name: "GenericCacheRefresher".to_string(), refreshing_interval, refresh_notification_sender, + update_fn: None, provider: item_provider, shared_cache: SharedCache::new(), refresh_requester: Default::default(), } } + pub(crate) fn new

(item_provider: P, refreshing_interval: Duration) -> Self + where + P: CacheItemProvider + Send + Sync + 'static, + { + Self::new_boxed(Box::new(item_provider), refreshing_interval) + } + pub(crate) fn new_with_initial_value( - item_provider: Box + Send + Sync>, + item_provider: Box + Send + Sync>, refreshing_interval: Duration, shared_cache: SharedCache, ) -> Self { @@ -80,12 +100,22 @@ where name: "GenericCacheRefresher".to_string(), refreshing_interval, refresh_notification_sender, + update_fn: None, provider: item_provider, shared_cache, refresh_requester: Default::default(), } } + #[must_use] + pub(crate) fn with_update_fn( + mut self, + update_fn: impl Fn(&mut T, S) + Send + Sync + 'static, + ) -> Self { + self.update_fn = Some(Box::new(update_fn)); + self + } + #[must_use] pub(crate) fn named(mut self, name: impl Into) -> Self { self.name = name.into(); @@ -105,22 +135,39 @@ where self.shared_cache.clone() } - // TODO: in the future offer 2 options of refreshing cache. either provide `T` directly - // or via `FnMut(&mut T)` closure - async fn do_refresh_cache(&self) { - let mut updated_items = match self.provider.try_refresh().await { - Err(err) => { - error!("{}: failed to refresh the cache: {err}", self.name); - return; + async fn update_cache(&self, mut update: S, update_fn: impl Fn(&mut T, S)) { + let mut failures = 0; + + loop { + match self + .shared_cache + .try_update_value(update, &update_fn, &self.name) + .await + { + Ok(_) => break, + Err(returned) => { + failures += 1; + update = returned + } + }; + if failures % 10 == 0 { + warn!( + "failed to obtain write permit for {} cache {failures} times in a row!", + self.name + ); } - Ok(items) => items, - }; + tokio::time::sleep(Duration::from_secs_f32(0.5)).await + } + } + + async fn overwrite_cache(&self, mut updated_items: T) { let mut failures = 0; + loop { match self .shared_cache - .try_update(updated_items, &self.name) + .try_overwrite_old_value(updated_items, &self.name) .await { Ok(_) => break, @@ -138,6 +185,26 @@ where tokio::time::sleep(Duration::from_secs_f32(0.5)).await } + } + + async fn do_refresh_cache(&mut self) { + let updated_items = match self.provider.try_refresh().await { + Err(err) => { + error!("{}: failed to refresh the cache: {err}", self.name); + return; + } + Ok(Some(items)) => items, + Ok(None) => { + debug!("no updates for {} cache this iteration", self.name); + return; + } + }; + + if let Some(update_fn) = self.update_fn.as_ref() { + self.update_cache(updated_items, update_fn).await; + } else { + self.overwrite_cache(updated_items.into()).await; + } if !self.refresh_notification_sender.is_closed() && self @@ -149,7 +216,7 @@ where } } - pub async fn refresh(&self, task_client: &mut TaskClient) { + pub async fn refresh(&mut self, task_client: &mut TaskClient) { info!("{}: refreshing cache state", self.name); tokio::select! { @@ -161,7 +228,7 @@ where } } - pub async fn run(&self, mut task_client: TaskClient) { + pub async fn run(&mut self, mut task_client: TaskClient) { self.provider.wait_until_ready().await; let mut refresh_interval = interval(self.refreshing_interval); @@ -183,10 +250,11 @@ where } } - pub fn start(self, task_client: TaskClient) + pub fn start(mut self, task_client: TaskClient) where T: Send + Sync + 'static, E: Send + Sync + 'static, + S: Send + Sync + 'static, { tokio::spawn(async move { self.run(task_client).await }); } @@ -195,6 +263,7 @@ where where T: Send + Sync + 'static, E: Send + Sync + 'static, + S: Send + Sync + 'static, { let receiver = self.update_watcher(); self.start(task_client); diff --git a/nym-api/src/support/cli/run.rs b/nym-api/src/support/cli/run.rs index 741b2d3f90e..bed8c6efaf3 100644 --- a/nym-api/src/support/cli/run.rs +++ b/nym-api/src/support/cli/run.rs @@ -1,7 +1,6 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::circulating_supply_api::cache::CirculatingSupplyCache; use crate::ecash::client::Client; use crate::ecash::comm::QueryCommunicationChannel; use crate::ecash::dkg::controller::keys::{ @@ -11,17 +10,21 @@ use crate::ecash::dkg::controller::DkgController; use crate::ecash::state::EcashState; use crate::epoch_operations::EpochAdvancer; use crate::key_rotation::KeyRotationController; +use crate::mixnet_contract_cache::cache::MixnetContractCache; use crate::network::models::NetworkDetails; use crate::node_describe_cache::cache::DescribedNodes; +use crate::node_performance::provider::contract_provider::ContractPerformanceProvider; +use crate::node_performance::provider::legacy_storage_provider::LegacyStoragePerformanceProvider; +use crate::node_performance::provider::NodePerformanceProvider; use crate::node_status_api::handlers::unstable; use crate::node_status_api::uptime_updater::HistoricalUptimeUpdater; use crate::node_status_api::NodeStatusCache; -use crate::nym_contract_cache::cache::NymContractCache; use crate::status::{ApiStatusState, SignerState}; use crate::support::caching::cache::SharedCache; use crate::support::config::helpers::try_load_current_config; use crate::support::config::{Config, DEFAULT_CHAIN_STATUS_CACHE_TTL}; use crate::support::http::state::chain_status::ChainStatusCache; +use crate::support::http::state::contract_details::ContractDetailsCache; use crate::support::http::state::force_refresh::ForcedRefresh; use crate::support::http::state::AppState; use crate::support::http::{RouterBuilder, ShutdownHandles, TASK_MANAGER_TIMEOUT_S}; @@ -30,8 +33,8 @@ use crate::support::storage::runtime_migrations::m001_directory_services_v2_1::m use crate::support::storage::NymApiStorage; use crate::unstable_routes::v1::account::cache::AddressInfoCache; use crate::{ - circulating_supply_api, ecash, epoch_operations, network_monitor, node_describe_cache, - node_status_api, nym_contract_cache, + ecash, epoch_operations, mixnet_contract_cache, network_monitor, node_describe_cache, + node_performance, node_status_api, }; use anyhow::{bail, Context}; use nym_config::defaults::NymNetworkDetails; @@ -146,10 +149,9 @@ async fn start_nym_api_tasks(config: &Config) -> anyhow::Result let router = RouterBuilder::with_default_routes(config.network_monitor.enabled); - let nym_contract_cache_state = NymContractCache::new(); + let mixnet_contract_cache_state = MixnetContractCache::new(); let node_status_cache_state = NodeStatusCache::new(); let mix_denom = network_details.network.chain_details.mix_denom.base.clone(); - let circulating_supply_cache = CirculatingSupplyCache::new(mix_denom.to_owned()); let described_nodes_cache = SharedCache::::new(); let node_info_cache = unstable::NodeInfoCache::default(); @@ -208,16 +210,14 @@ async fn start_nym_api_tasks(config: &Config) -> anyhow::Result config.address_cache.time_to_live, config.address_cache.capacity, ), - forced_refresh: ForcedRefresh::new( - config.topology_cacher.debug.node_describe_allow_illegal_ips, - ), - nym_contract_cache: nym_contract_cache_state.clone(), + forced_refresh: ForcedRefresh::new(config.describe_cache.debug.allow_illegal_ips), + mixnet_contract_cache: mixnet_contract_cache_state.clone(), node_status_cache: node_status_cache_state.clone(), - circulating_supply_cache: circulating_supply_cache.clone(), storage: storage.clone(), described_nodes_cache: described_nodes_cache.clone(), - network_details, + network_details: network_details.clone(), node_info_cache, + contract_info_cache: ContractDetailsCache::new(config.contracts_info_cache.time_to_live), api_status: ApiStatusState::new(signer_information), ecash_state: Arc::new(ecash_state), }); @@ -227,8 +227,8 @@ async fn start_nym_api_tasks(config: &Config) -> anyhow::Result // let refresher = node_describe_cache::new_refresher(&config.topology_cacher); // let cache = refresher.get_shared_cache(); let describe_cache_refresher = node_describe_cache::provider::new_provider_with_initial_value( - &config.topology_cacher, - nym_contract_cache_state.clone(), + &config.describe_cache, + mixnet_contract_cache_state.clone(), described_nodes_cache.clone(), ) .named("node-self-described-data-refresher"); @@ -238,31 +238,54 @@ async fn start_nym_api_tasks(config: &Config) -> anyhow::Result let describe_cache_watcher = describe_cache_refresher .start_with_watcher(task_manager.subscribe_named("node-self-described-data-refresher")); + let performance_provider = if config.performance_provider.use_performance_contract_data { + if network_details + .network + .contracts + .performance_contract_address + .is_none() + { + bail!("can't use performance contract data without setting the address of the contract") + } + + let performance_contract_cache = node_performance::contract_cache::start_cache_refresher( + &config.performance_provider, + nyxd_client.clone(), + mixnet_contract_cache_state.clone(), + &task_manager, + ) + .await?; + let provider = ContractPerformanceProvider::new( + &config.performance_provider, + performance_contract_cache, + ); + Box::new(provider) as Box + } else { + Box::new(LegacyStoragePerformanceProvider::new( + storage.clone(), + mixnet_contract_cache_state.clone(), + )) + }; + // start all the caches first - let contract_cache_refresher = nym_contract_cache::build_refresher( - &config.node_status_api, - &nym_contract_cache_state.clone(), + let mixnet_contract_cache_refresher = mixnet_contract_cache::build_refresher( + &config.mixnet_contract_cache, + &mixnet_contract_cache_state.clone(), nyxd_client.clone(), ); let contract_cache_watcher = - contract_cache_refresher.start_with_watcher(task_manager.subscribe()); + mixnet_contract_cache_refresher.start_with_watcher(task_manager.subscribe()); node_status_api::start_cache_refresh( &config.node_status_api, - &nym_contract_cache_state, + &mixnet_contract_cache_state, &described_nodes_cache, &node_status_cache_state, - storage.clone(), + performance_provider, contract_cache_watcher.clone(), describe_cache_watcher, &task_manager, ); - circulating_supply_api::start_cache_refresh( - &config.circulating_supply_cacher, - nyxd_client.clone(), - &circulating_supply_cache, - &task_manager, - ); // start dkg task if config.ecash_signer.enabled { @@ -279,12 +302,15 @@ async fn start_nym_api_tasks(config: &Config) -> anyhow::Result )?; } + let has_performance_data = + config.network_monitor.enabled || config.performance_provider.use_performance_contract_data; + // and then only start the uptime updater (and the monitor itself, duh) // if the monitoring is enabled if config.network_monitor.enabled { network_monitor::start::( config, - &nym_contract_cache_state, + &mixnet_contract_cache_state, described_nodes_cache.clone(), node_status_cache_state.clone(), &storage, @@ -294,19 +320,19 @@ async fn start_nym_api_tasks(config: &Config) -> anyhow::Result .await; HistoricalUptimeUpdater::start(storage.to_owned(), &task_manager); + } - // start 'rewarding' if its enabled - if config.rewarding.enabled { - epoch_operations::ensure_rewarding_permission(&nyxd_client).await?; - EpochAdvancer::start( - nyxd_client, - &nym_contract_cache_state, - &node_status_cache_state, - described_nodes_cache.clone(), - &storage, - &task_manager, - ); - } + // start 'rewarding' if its enabled and there exists source for performance data + if config.rewarding.enabled && has_performance_data { + epoch_operations::ensure_rewarding_permission(&nyxd_client).await?; + EpochAdvancer::start( + nyxd_client, + &mixnet_contract_cache_state, + &node_status_cache_state, + described_nodes_cache.clone(), + &storage, + &task_manager, + ); } // finally start a background task watching the contract changes and requesting @@ -314,7 +340,7 @@ async fn start_nym_api_tasks(config: &Config) -> anyhow::Result KeyRotationController::new( describe_cache_refresh_requester, contract_cache_watcher, - nym_contract_cache_state, + mixnet_contract_cache_state, ) .start(task_manager.subscribe_named("KeyRotationController")); diff --git a/nym-api/src/support/config/mod.rs b/nym-api/src/support/config/mod.rs index cf2161eaa21..8be4ee0bc04 100644 --- a/nym-api/src/support/config/mod.rs +++ b/nym-api/src/support/config/mod.rs @@ -50,9 +50,11 @@ const DEFAULT_MINIMUM_TEST_ROUTES: usize = 1; const DEFAULT_ROUTE_TEST_PACKETS: usize = 1000; const DEFAULT_PER_NODE_TEST_PACKETS: usize = 3; -const DEFAULT_TOPOLOGY_CACHE_INTERVAL: Duration = Duration::from_secs(30); -const DEFAULT_NODE_STATUS_CACHE_INTERVAL: Duration = Duration::from_secs(120); -const DEFAULT_CIRCULATING_SUPPLY_CACHE_INTERVAL: Duration = Duration::from_secs(3600); +const DEFAULT_NODE_STATUS_CACHE_REFRESH_INTERVAL: Duration = Duration::from_secs(305); +const DEFAULT_MIXNET_CACHE_REFRESH_INTERVAL: Duration = Duration::from_secs(150); +const DEFAULT_PERFORMANCE_CONTRACT_POLLING_INTERVAL: Duration = Duration::from_secs(150); +const DEFAULT_PERFORMANCE_CONTRACT_FALLBACK_EPOCHS: u32 = 12; +const DEFAULT_PERFORMANCE_CONTRACT_RETAINED_EPOCHS: usize = 25; pub(crate) const DEFAULT_ADDRESS_CACHE_TTL: Duration = Duration::from_secs(60 * 15); pub(crate) const DEFAULT_ADDRESS_CACHE_CAPACITY: u64 = 1000; @@ -63,6 +65,10 @@ pub(crate) const DEFAULT_NODE_DESCRIBE_BATCH_SIZE: usize = 50; // TODO: make it configurable pub(crate) const DEFAULT_CHAIN_STATUS_CACHE_TTL: Duration = Duration::from_secs(60); +// contract info is changed very infrequently (essentially once per release cycle) +// so this default is more than enough +pub(crate) const DEFAULT_CONTRACT_DETAILS_CACHE_TTL: Duration = Duration::from_secs(60 * 60); + const DEFAULT_MONITOR_THRESHOLD: u8 = 60; const DEFAULT_MIN_MIXNODE_RELIABILITY: u8 = 50; const DEFAULT_MIN_GATEWAY_RELIABILITY: u8 = 20; @@ -101,14 +107,22 @@ pub struct Config { pub base: Base, + #[serde(default)] + pub performance_provider: PerformanceProvider, + // TODO: perhaps introduce separate 'path finder' field for all the paths and directories like we have with other configs pub network_monitor: NetworkMonitor, + #[serde(default)] + pub mixnet_contract_cache: MixnetContractCache, + pub node_status_api: NodeStatusAPI, - pub topology_cacher: TopologyCacher, + #[serde(alias = "topology_cacher")] + pub describe_cache: DescribeCache, - pub circulating_supply_cacher: CirculatingSupplyCacher, + #[serde(default)] + pub contracts_info_cache: ContractsInfoCache, pub rewarding: Rewarding, @@ -130,10 +144,12 @@ impl Config { Config { save_path: None, base: Base::new_default(id.as_ref()), + performance_provider: Default::default(), network_monitor: NetworkMonitor::new_default(id.as_ref()), + mixnet_contract_cache: Default::default(), node_status_api: NodeStatusAPI::new_default(id.as_ref()), - topology_cacher: Default::default(), - circulating_supply_cacher: Default::default(), + describe_cache: Default::default(), + contracts_info_cache: Default::default(), rewarding: Default::default(), ecash_signer: EcashSigner::new_default(id.as_ref()), address_cache: Default::default(), @@ -184,7 +200,7 @@ impl Config { self.base.bind_address = http_bind_address } if args.allow_illegal_ips { - self.topology_cacher.debug.node_describe_allow_illegal_ips = true + self.describe_cache.debug.allow_illegal_ips = true } if let Some(address_cache_ttl) = args.address_cache_ttl { self.address_cache.time_to_live = address_cache_ttl; @@ -305,6 +321,98 @@ impl Base { } } +#[derive(Debug, Deserialize, PartialEq, Eq, Serialize)] +pub struct ContractsInfoCache { + pub time_to_live: Duration, +} + +impl Default for ContractsInfoCache { + fn default() -> Self { + ContractsInfoCache { + time_to_live: DEFAULT_CONTRACT_DETAILS_CACHE_TTL, + } + } +} + +#[derive(Debug, Deserialize, PartialEq, Eq, Serialize)] +pub struct MixnetContractCache { + #[serde(default)] + pub debug: MixnetContractCacheDebug, +} + +#[allow(clippy::derivable_impls)] +impl Default for MixnetContractCache { + fn default() -> Self { + MixnetContractCache { + debug: Default::default(), + } + } +} + +#[derive(Debug, Deserialize, PartialEq, Eq, Serialize)] +#[serde(default)] +pub struct MixnetContractCacheDebug { + #[serde(with = "humantime_serde")] + pub caching_interval: Duration, +} + +impl Default for MixnetContractCacheDebug { + fn default() -> Self { + MixnetContractCacheDebug { + caching_interval: DEFAULT_MIXNET_CACHE_REFRESH_INTERVAL, + } + } +} + +#[derive(Debug, Deserialize, PartialEq, Eq, Serialize)] +pub struct PerformanceProvider { + /// Specifies whether this nym-api should attempt to retrieve node performance + /// information from the performance contract. + pub use_performance_contract_data: bool, + + pub debug: PerformanceProviderDebug, +} + +#[allow(clippy::derivable_impls)] +impl Default for PerformanceProvider { + fn default() -> Self { + PerformanceProvider { + // to be changed later + use_performance_contract_data: false, + debug: Default::default(), + } + } +} + +#[derive(Debug, Deserialize, PartialEq, Eq, Serialize)] +pub struct PerformanceProviderDebug { + /// Specifies interval of polling the performance contract. Note it is only applicable + /// if the contract data is being used. + /// Further note that if there have been no updates to the cache, the performance overhead is negligible + /// (i.e. there will be only a single query performed to check if anything has changed) + #[serde(with = "humantime_serde")] + pub contract_polling_interval: Duration, + + /// Specify the maximum number of epochs we can fallback to if given epoch's performance data + /// is not available in the contract + pub max_performance_fallback_epochs: u32, + + /// Specify the maximum number of epoch entries to be kept in the cache in case we needed non-current data + // (currently we need an equivalent of full day worth of data for legacy endpoints) + pub max_epoch_entries_to_retain: usize, +} + +#[allow(clippy::derivable_impls)] +impl Default for PerformanceProviderDebug { + fn default() -> Self { + PerformanceProviderDebug { + contract_polling_interval: DEFAULT_PERFORMANCE_CONTRACT_POLLING_INTERVAL, + max_performance_fallback_epochs: DEFAULT_PERFORMANCE_CONTRACT_FALLBACK_EPOCHS, + max_epoch_entries_to_retain: DEFAULT_PERFORMANCE_CONTRACT_RETAINED_EPOCHS, + } + } +} + #[derive(Debug, PartialEq, Eq)] pub struct AddressCacheConfig { pub time_to_live: Duration, @@ -447,76 +555,41 @@ pub struct NodeStatusAPIDebug { impl Default for NodeStatusAPIDebug { fn default() -> Self { NodeStatusAPIDebug { - caching_interval: DEFAULT_NODE_STATUS_CACHE_INTERVAL, + caching_interval: DEFAULT_NODE_STATUS_CACHE_REFRESH_INTERVAL, } } } #[derive(Debug, Default, Deserialize, PartialEq, Eq, Serialize)] #[serde(default)] -pub struct TopologyCacher { +pub struct DescribeCache { // pub enabled: bool, // pub paths: TopologyCacherPathfinder, #[serde(default)] - pub debug: TopologyCacherDebug, + pub debug: DescribeCacheDebug, } #[derive(Debug, Deserialize, PartialEq, Eq, Serialize)] #[serde(default)] -pub struct TopologyCacherDebug { +pub struct DescribeCacheDebug { #[serde(with = "humantime_serde")] + #[serde(alias = "node_describe_caching_interval")] pub caching_interval: Duration, - #[serde(with = "humantime_serde")] - pub node_describe_caching_interval: Duration, - - pub node_describe_batch_size: usize, - - pub node_describe_allow_illegal_ips: bool, -} - -impl Default for TopologyCacherDebug { - fn default() -> Self { - TopologyCacherDebug { - caching_interval: DEFAULT_TOPOLOGY_CACHE_INTERVAL, - node_describe_caching_interval: DEFAULT_NODE_DESCRIBE_CACHE_INTERVAL, - node_describe_batch_size: DEFAULT_NODE_DESCRIBE_BATCH_SIZE, - node_describe_allow_illegal_ips: false, - } - } -} + #[serde(alias = "node_describe_batch_size")] + pub batch_size: usize, -#[derive(Debug, Deserialize, PartialEq, Eq, Serialize)] -#[serde(default)] -pub struct CirculatingSupplyCacher { - pub enabled: bool, - - // pub paths: CirculatingSupplyCacherPathfinder, - #[serde(default)] - pub debug: CirculatingSupplyCacherDebug, -} - -impl Default for CirculatingSupplyCacher { - fn default() -> Self { - CirculatingSupplyCacher { - enabled: true, - debug: CirculatingSupplyCacherDebug::default(), - } - } -} - -#[derive(Debug, Deserialize, PartialEq, Eq, Serialize)] -#[serde(default)] -pub struct CirculatingSupplyCacherDebug { - #[serde(with = "humantime_serde")] - pub caching_interval: Duration, + #[serde(alias = "node_describe_allow_illegal_ips")] + pub allow_illegal_ips: bool, } -impl Default for CirculatingSupplyCacherDebug { +impl Default for DescribeCacheDebug { fn default() -> Self { - CirculatingSupplyCacherDebug { - caching_interval: DEFAULT_CIRCULATING_SUPPLY_CACHE_INTERVAL, + DescribeCacheDebug { + caching_interval: DEFAULT_NODE_DESCRIBE_CACHE_INTERVAL, + batch_size: DEFAULT_NODE_DESCRIBE_BATCH_SIZE, + allow_illegal_ips: false, } } } diff --git a/nym-api/src/support/http/router.rs b/nym-api/src/support/http/router.rs index 8925b3beee2..b04956eed8b 100644 --- a/nym-api/src/support/http/router.rs +++ b/nym-api/src/support/http/router.rs @@ -3,9 +3,9 @@ use crate::circulating_supply_api::handlers::circulating_supply_routes; use crate::ecash::api_routes::handlers::ecash_routes; +use crate::mixnet_contract_cache::handlers::nym_contract_cache_routes; use crate::network::handlers::nym_network_routes; use crate::node_status_api::handlers::status_routes; -use crate::nym_contract_cache::handlers::nym_contract_cache_routes; use crate::nym_nodes::handlers::legacy::legacy_nym_node_routes; use crate::nym_nodes::handlers::nym_node_routes; use crate::status; diff --git a/nym-api/src/support/http/state/contract_details.rs b/nym-api/src/support/http/state/contract_details.rs new file mode 100644 index 00000000000..5380a928099 --- /dev/null +++ b/nym-api/src/support/http/state/contract_details.rs @@ -0,0 +1,182 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node_status_api::models::AxumErrorResponse; +use crate::support::nyxd::Client; +use nym_contracts_common::ContractBuildInformation; +use nym_validator_client::nyxd::contract_traits::{ + MixnetQueryClient, NymContractsProvider, VestingQueryClient, +}; +use nym_validator_client::nyxd::error::NyxdError; +use nym_validator_client::nyxd::AccountId; +use std::collections::HashMap; +use std::sync::Arc; +use std::time::Duration; +use time::OffsetDateTime; +use tokio::sync::RwLock; + +type ContractAddress = String; + +pub type CachedContractsInfo = HashMap; + +#[derive(Clone)] +pub struct CachedContractInfo { + pub(crate) address: Option, + pub(crate) base: Option, + pub(crate) detailed: Option, +} + +impl CachedContractInfo { + pub fn new( + address: Option<&AccountId>, + base: Option, + detailed: Option, + ) -> Self { + Self { + address: address.cloned(), + base, + detailed, + } + } +} + +#[derive(Clone)] +pub(crate) struct ContractDetailsCache { + cache_ttl: Duration, + inner: Arc>, +} + +impl ContractDetailsCache { + pub(crate) fn new(cache_ttl: Duration) -> Self { + ContractDetailsCache { + cache_ttl, + inner: Arc::new(RwLock::new(ContractDetailsCacheInner::new())), + } + } +} + +struct ContractDetailsCacheInner { + last_refreshed_at: OffsetDateTime, + cache_value: CachedContractsInfo, +} + +impl ContractDetailsCacheInner { + pub(crate) fn new() -> Self { + ContractDetailsCacheInner { + last_refreshed_at: OffsetDateTime::UNIX_EPOCH, + cache_value: Default::default(), + } + } + + fn is_valid(&self, ttl: Duration) -> bool { + if self.last_refreshed_at + ttl > OffsetDateTime::now_utc() { + return true; + } + false + } + + async fn retrieve_nym_contracts_info( + &self, + nyxd_client: &Client, + ) -> Result { + use crate::query_guard; + + let mut updated = HashMap::new(); + + let client_guard = nyxd_client.read().await; + + let mixnet = query_guard!(client_guard, mixnet_contract_address()); + let vesting = query_guard!(client_guard, vesting_contract_address()); + let coconut_dkg = query_guard!(client_guard, dkg_contract_address()); + let group = query_guard!(client_guard, group_contract_address()); + let multisig = query_guard!(client_guard, multisig_contract_address()); + let ecash = query_guard!(client_guard, ecash_contract_address()); + let performance = query_guard!(client_guard, performance_contract_address()); + + for (address, name) in [ + (mixnet, "nym-mixnet-contract"), + (vesting, "nym-vesting-contract"), + (coconut_dkg, "nym-coconut-dkg-contract"), + (group, "nym-cw4-group-contract"), + (multisig, "nym-cw3-multisig-contract"), + (ecash, "nym-ecash-contract"), + (performance, "nym-performance-contract"), + ] { + let (cw2, build_info) = if let Some(address) = address { + let cw2 = query_guard!(client_guard, try_get_cw2_contract_version(address).await); + let mut build_info = query_guard!( + client_guard, + try_get_contract_build_information(address).await + ); + + // for backwards compatibility until we migrate the contracts + if build_info.is_none() { + match name { + "nym-mixnet-contract" => { + build_info = Some(query_guard!( + client_guard, + get_mixnet_contract_version().await + )?) + } + "nym-vesting-contract" => { + build_info = Some(query_guard!( + client_guard, + get_vesting_contract_version().await + )?) + } + _ => (), + } + } + + (cw2, build_info) + } else { + (None, None) + }; + + updated.insert( + name.to_string(), + CachedContractInfo::new(address, cw2, build_info), + ); + } + + Ok(updated) + } +} + +impl ContractDetailsCache { + pub(crate) async fn get_or_refresh( + &self, + client: &Client, + ) -> Result { + if let Some(cached) = self.check_cache().await { + return Ok(cached); + } + + self.refresh(client).await + } + + async fn check_cache(&self) -> Option { + let guard = self.inner.read().await; + if guard.is_valid(self.cache_ttl) { + return Some(guard.cache_value.clone()); + } + None + } + + async fn refresh(&self, client: &Client) -> Result { + // 1. attempt to get write lock permit + let mut guard = self.inner.write().await; + + // 2. check if another task hasn't already updated the cache whilst we were waiting for the permit + if guard.is_valid(self.cache_ttl) { + return Ok(guard.cache_value.clone()); + } + + // 3. attempt to query the chain for the contracts data + let updated_values = guard.retrieve_nym_contracts_info(client).await?; + guard.last_refreshed_at = OffsetDateTime::now_utc(); + guard.cache_value = updated_values.clone(); + + Ok(updated_values) + } +} diff --git a/nym-api/src/support/http/state/mod.rs b/nym-api/src/support/http/state/mod.rs index b3bcf49216e..ec0d2ca9bb4 100644 --- a/nym-api/src/support/http/state/mod.rs +++ b/nym-api/src/support/http/state/mod.rs @@ -1,18 +1,18 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::circulating_supply_api::cache::CirculatingSupplyCache; use crate::ecash::state::EcashState; +use crate::mixnet_contract_cache::cache::MixnetContractCache; use crate::network::models::NetworkDetails; use crate::node_describe_cache::cache::DescribedNodes; use crate::node_status_api::handlers::unstable; use crate::node_status_api::models::AxumErrorResponse; use crate::node_status_api::NodeStatusCache; -use crate::nym_contract_cache::cache::NymContractCache; use crate::status::ApiStatusState; use crate::support::caching::cache::SharedCache; use crate::support::caching::Cache; use crate::support::http::state::chain_status::ChainStatusCache; +use crate::support::http::state::contract_details::ContractDetailsCache; use crate::support::http::state::force_refresh::ForcedRefresh; use crate::support::nyxd::Client; use crate::support::storage; @@ -27,26 +27,58 @@ use std::sync::Arc; use tokio::sync::RwLockReadGuard; pub(crate) mod chain_status; +pub(crate) mod contract_details; pub(crate) mod force_refresh; #[derive(Clone)] pub(crate) struct AppState { // ideally this would have been made generic to make tests easier, // however, it'd be a way bigger change (I tried) + /// Instance of a client used for interacting with the nyx chain. pub(crate) nyxd_client: Client, + + /// Holds information about the latest chain block it has queried. + /// Note, it is not updated on every request. It follows the embedded ttl. pub(crate) chain_status_cache: ChainStatusCache, + /// Holds mapping between a nyx address and tokens/delegations it holds pub(crate) address_info_cache: AddressInfoCache, + + /// Holds information on when nym-nodes requested an explicit request of their self-described data. + /// It is used to prevent DoS by nodes constantly requesting the refresh. pub(crate) forced_refresh: ForcedRefresh, - pub(crate) nym_contract_cache: NymContractCache, + + /// Holds cached state of the Nym Mixnet contract, e.g. bonded nym-nodes, rewarded set, current interval. + pub(crate) mixnet_contract_cache: MixnetContractCache, + + /// Holds processed information on network nodes, i.e. performance, config scores, etc. + // TODO: also perhaps redundant? pub(crate) node_status_cache: NodeStatusCache, - pub(crate) circulating_supply_cache: CirculatingSupplyCache, + + /// Holds reference to the persistent storage of this nym-api. pub(crate) storage: storage::NymApiStorage, + + /// Holds information on the self-reported information of nodes, e.g. auxiliary keys they use, + /// ports they announce, etc. pub(crate) described_nodes_cache: SharedCache, + + /// Information about the current network this nym-api is connected to, e.g. contract addresses, + /// endpoints, denominations. pub(crate) network_details: NetworkDetails, + + /// A simple in-memory cache of node information mapping their database id to their node-ids + /// and public keys. Useful (I guess?) for returning information about test routes. + // TODO: do we need it? pub(crate) node_info_cache: unstable::NodeInfoCache, + + /// Cache containing data (build info, versions, etc.) on all nym smart contracts on the network + pub(crate) contract_info_cache: ContractDetailsCache, + + /// Information about this nym-api, i.e. its public key, startup time, etc. pub(crate) api_status: ApiStatusState, + // todo: refactor it into inner: Arc + /// Cache holding data required by the ecash credentials - static signatures, merkle trees, etc. pub(crate) ecash_state: Arc, } @@ -62,19 +94,21 @@ impl FromRef for Arc { } } +impl FromRef for MixnetContractCache { + fn from_ref(app_state: &AppState) -> Self { + app_state.mixnet_contract_cache.clone() + } +} + impl AppState { - pub(crate) fn nym_contract_cache(&self) -> &NymContractCache { - &self.nym_contract_cache + pub(crate) fn nym_contract_cache(&self) -> &MixnetContractCache { + &self.mixnet_contract_cache } pub(crate) fn node_status_cache(&self) -> &NodeStatusCache { &self.node_status_cache } - pub(crate) fn circulating_supply_cache(&self) -> &CirculatingSupplyCache { - &self.circulating_supply_cache - } - pub(crate) fn network_details(&self) -> &NetworkDetails { &self.network_details } @@ -153,7 +187,7 @@ impl AppState { .address_info_cache .collect_balances( self.nyxd_client.clone(), - self.nym_contract_cache.clone(), + self.mixnet_contract_cache.clone(), self.network_details() .network .chain_details diff --git a/nym-api/src/support/nyxd/mod.rs b/nym-api/src/support/nyxd/mod.rs index 41ef7d2bc44..6de33c47384 100644 --- a/nym-api/src/support/nyxd/mod.rs +++ b/nym-api/src/support/nyxd/mod.rs @@ -17,10 +17,10 @@ use nym_coconut_dkg_common::msg::QueryMsg as DkgQueryMsg; use nym_coconut_dkg_common::types::{ChunkIndex, DealingIndex, PartialContractDealingData, State}; use nym_coconut_dkg_common::{ dealer::{DealerDetails, DealerDetailsResponse}, - types::{EncodedBTEPublicKeyWithProof, Epoch, EpochId}, + types::{EncodedBTEPublicKeyWithProof, Epoch}, verification_key::{ContractVKShare, VerificationKeyShare}, }; -use nym_config::defaults::{ChainDetails, NymNetworkDetails}; +use nym_config::defaults::NymNetworkDetails; use nym_dkg::Threshold; use nym_ecash_contract_common::blacklist::BlacklistedAccountResponse; use nym_ecash_contract_common::deposit::{DepositId, DepositResponse}; @@ -35,14 +35,19 @@ 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::PagedDkgQueryClient; +use nym_validator_client::nyxd::contract_traits::performance_query_client::{ + LastSubmission, NodePerformance, +}; +use nym_validator_client::nyxd::contract_traits::{ + PagedDkgQueryClient, PagedPerformanceQueryClient, PerformanceQueryClient, +}; use nym_validator_client::nyxd::error::NyxdError; use nym_validator_client::nyxd::Coin; use nym_validator_client::nyxd::{ contract_traits::{ DkgQueryClient, DkgSigningClient, EcashQueryClient, GroupQueryClient, MixnetQueryClient, MixnetSigningClient, MultisigQueryClient, MultisigSigningClient, NymContractsProvider, - PagedMixnetQueryClient, PagedMultisigQueryClient, PagedVestingQueryClient, + PagedMixnetQueryClient, PagedMultisigQueryClient, }, cosmwasm_client::types::ExecuteResult, BlockResponse, CosmWasmClient, Fee, TendermintRpcClient, @@ -54,7 +59,6 @@ use nym_validator_client::nyxd::{ use nym_validator_client::{ nyxd, DirectSigningHttpRpcNyxdClient, EcashApiClient, QueryHttpRpcNyxdClient, }; -use nym_vesting_contract_common::AccountVestingCoins; use serde::Deserialize; use std::sync::Arc; use tendermint::abci::response::Info; @@ -163,10 +167,6 @@ impl Client { } } - pub(crate) async fn chain_details(&self) -> ChainDetails { - nyxd_query!(self, current_chain_details().clone()) - } - pub(crate) async fn get_ecash_contract_address(&self) -> Result { nyxd_query!( self, @@ -258,6 +258,12 @@ impl Client { nyxd_query!(self, get_current_interval_details().await) } + pub(crate) async fn get_mixnet_contract_state( + &self, + ) -> Result { + nyxd_query!(self, get_mixnet_contract_state().await) + } + pub(crate) async fn get_current_epoch_status(&self) -> Result { nyxd_query!(self, get_current_epoch_status().await) } @@ -272,31 +278,6 @@ impl Client { nyxd_query!(self, get_rewarded_set().await) } - pub(crate) async fn get_current_vesting_account_storage_key(&self) -> Result { - let guard = self.inner.read().await; - - // the expect is fine as we always construct the client with the vesting contract explicitly set - let vesting_contract = query_guard!( - guard, - vesting_contract_address().expect("vesting contract address is not available") - ); - // TODO: I don't like the usage of the hardcoded value here - let res = query_guard!( - guard, - query_contract_raw(vesting_contract, b"key".to_vec()).await? - ); - if res.is_empty() { - return Ok(0); - } - - serde_json::from_slice(&res).map_err(NyxdError::from) - } - - pub(crate) async fn get_all_vesting_coins( - &self, - ) -> Result, NyxdError> { - nyxd_query!(self, get_all_accounts_vesting_coins().await) - } pub(crate) async fn get_pending_events_count(&self) -> Result { let pending = nyxd_query!(self, get_number_of_pending_events().await?); Ok(pending.epoch_events + pending.interval_events) @@ -423,6 +404,19 @@ impl Client { ) -> Result, NyxdError> { nyxd_query!(self, get_balance(&address, denom.into()).await) } + + pub(crate) async fn get_last_performance_contract_submission( + &self, + ) -> Result { + nyxd_query!(self, get_last_submission().await) + } + + pub(crate) async fn get_full_epoch_performance( + &self, + epoch_id: nym_mixnet_contract_common::EpochId, + ) -> Result, NyxdError> { + nyxd_query!(self, get_all_epoch_performance(epoch_id).await) + } } #[async_trait] @@ -508,7 +502,7 @@ impl crate::ecash::client::Client for Client { async fn get_epoch_threshold( &self, - epoch_id: EpochId, + epoch_id: nym_coconut_dkg_common::types::EpochId, ) -> crate::ecash::error::Result> { Ok(nyxd_query!(self, get_epoch_threshold(epoch_id).await?)) } @@ -522,7 +516,7 @@ impl crate::ecash::client::Client for Client { async fn get_registered_dealer_details( &self, - epoch_id: EpochId, + epoch_id: nym_coconut_dkg_common::types::EpochId, dealer: String, ) -> crate::ecash::error::Result { let dealer = dealer @@ -537,7 +531,7 @@ impl crate::ecash::client::Client for Client { async fn get_dealer_dealings_status( &self, - epoch_id: EpochId, + epoch_id: nym_coconut_dkg_common::types::EpochId, dealer: String, ) -> crate::ecash::error::Result { Ok(nyxd_query!( @@ -548,7 +542,7 @@ impl crate::ecash::client::Client for Client { async fn get_dealing_status( &self, - epoch_id: EpochId, + epoch_id: nym_coconut_dkg_common::types::EpochId, dealer: String, dealing_index: DealingIndex, ) -> crate::ecash::error::Result { @@ -564,7 +558,7 @@ impl crate::ecash::client::Client for Client { async fn get_dealing_metadata( &self, - epoch_id: EpochId, + epoch_id: nym_coconut_dkg_common::types::EpochId, dealer: String, dealing_index: DealingIndex, ) -> crate::ecash::error::Result> { @@ -578,7 +572,7 @@ impl crate::ecash::client::Client for Client { async fn get_dealing_chunk( &self, - epoch_id: EpochId, + epoch_id: nym_coconut_dkg_common::types::EpochId, dealer: &str, dealing_index: DealingIndex, chunk_index: ChunkIndex, @@ -593,7 +587,7 @@ impl crate::ecash::client::Client for Client { async fn get_verification_key_share( &self, - epoch_id: EpochId, + epoch_id: nym_coconut_dkg_common::types::EpochId, dealer: String, ) -> Result, EcashError> { Ok(nyxd_query!(self, get_vk_share(epoch_id, dealer).await?).share) @@ -601,7 +595,7 @@ impl crate::ecash::client::Client for Client { async fn get_verification_key_shares( &self, - epoch_id: EpochId, + epoch_id: nym_coconut_dkg_common::types::EpochId, ) -> Result, EcashError> { Ok(nyxd_query!( self, @@ -611,7 +605,7 @@ impl crate::ecash::client::Client for Client { async fn get_registered_ecash_clients( &self, - epoch_id: EpochId, + epoch_id: nym_coconut_dkg_common::types::EpochId, ) -> Result, EcashError> { Ok(self .get_verification_key_shares(epoch_id) diff --git a/nym-api/src/support/storage/manager.rs b/nym-api/src/support/storage/manager.rs index b1f294d6d75..710481d41eb 100644 --- a/nym-api/src/support/storage/manager.rs +++ b/nym-api/src/support/storage/manager.rs @@ -81,21 +81,6 @@ impl StorageManager { Ok(node_id) } - pub(super) async fn get_gateway_identity_key( - &self, - node_id: NodeId, - ) -> Result, sqlx::Error> { - let identity_key = sqlx::query!( - "SELECT identity FROM gateway_details WHERE node_id = ?", - node_id - ) - .fetch_optional(&self.connection_pool) - .await? - .map(|row| row.identity); - - Ok(identity_key) - } - /// Tries to obtain identity value of given mixnode given its mix_id /// /// # Arguments @@ -116,62 +101,6 @@ impl StorageManager { Ok(identity_key) } - /// Gets all reliability statuses for mixnode with particular identity that were inserted - /// into the database after the specified unix timestamp. - /// - /// # Arguments - /// - /// * `mix_id`: mix-id (as assigned by the smart contract) of the mixnode. - /// * `timestamp`: unix timestamp of the lower bound of the selection. - pub(super) async fn get_mixnode_statuses_since( - &self, - mix_id: NodeId, - timestamp: i64, - ) -> Result, sqlx::Error> { - sqlx::query_as!( - NodeStatus, - r#" - SELECT timestamp, reliability as "reliability: u8" - FROM mixnode_status - JOIN mixnode_details - ON mixnode_status.mixnode_details_id = mixnode_details.id - WHERE mixnode_details.mix_id=? AND mixnode_status.timestamp > ?; - "#, - mix_id, - timestamp, - ) - .fetch_all(&self.connection_pool) - .await - } - - /// Gets all reliability statuses for gateway with particular identity that were inserted - /// into the database after the specified unix timestamp. - /// - /// # Arguments - /// - /// * `identity`: identity (base58-encoded public key) of the gateway. - /// * `timestamp`: unix timestamp of the lower bound of the selection. - pub(super) async fn get_gateway_statuses_since( - &self, - node_id: NodeId, - timestamp: i64, - ) -> Result, sqlx::Error> { - sqlx::query_as!( - NodeStatus, - r#" - SELECT timestamp, reliability as "reliability: u8" - FROM gateway_status - JOIN gateway_details - ON gateway_status.gateway_details_id = gateway_details.id - WHERE gateway_details.node_id=? AND gateway_status.timestamp > ?; - "#, - node_id, - timestamp, - ) - .fetch_all(&self.connection_pool) - .await - } - /// Gets the historical daily uptime associated with the particular mixnode /// /// # Arguments diff --git a/nym-api/src/support/storage/mod.rs b/nym-api/src/support/storage/mod.rs index d059ad304df..8bb14a697d3 100644 --- a/nym-api/src/support/storage/mod.rs +++ b/nym-api/src/support/storage/mod.rs @@ -9,7 +9,7 @@ use crate::node_status_api::models::{ }; use crate::node_status_api::{ONE_DAY, ONE_HOUR}; use crate::storage::manager::StorageManager; -use crate::storage::models::{NodeStatus, TestingRoute}; +use crate::storage::models::TestingRoute; use crate::support::storage::models::{ GatewayDetails, HistoricalUptime, MixnodeDetails, MonitorRunReport, MonitorRunScore, TestedGatewayStatus, TestedMixnodeStatus, @@ -133,124 +133,6 @@ impl NymApiStorage { Ok(None) } - /// Gets all statuses for particular mixnode that were inserted - /// since the provided timestamp. - /// - /// # Arguments - /// - /// * `mix_id`: mix-id (as assigned by the smart contract) of the mixnode to query. - /// * `since`: unix timestamp indicating the lower bound interval of the selection. - async fn get_mixnode_statuses( - &self, - mix_id: NodeId, - since: i64, - ) -> Result, NymApiStorageError> { - let statuses = self - .manager - .get_mixnode_statuses_since(mix_id, since) - .await?; - - Ok(statuses) - } - - /// Gets all statuses for particular gateway that were inserted - /// since the provided timestamp. - /// - /// # Arguments - /// - /// * `since`: unix timestamp indicating the lower bound interval of the selection. - async fn get_gateway_statuses( - &self, - node_id: NodeId, - since: i64, - ) -> Result, NymApiStorageError> { - let statuses = self - .manager - .get_gateway_statuses_since(node_id, since) - .await?; - - Ok(statuses) - } - - /// Tries to construct a status report for mixnode with the specified mix_id. - /// - /// # Arguments - /// - /// * `mix_id`: mix-id (as assigned by the smart contract) of the mixnode. - pub(crate) async fn construct_mixnode_report( - &self, - mix_id: NodeId, - ) -> Result { - let now = OffsetDateTime::now_utc(); - let day_ago = (now - ONE_DAY).unix_timestamp(); - let hour_ago = (now - ONE_HOUR).unix_timestamp(); - - let statuses = self.get_mixnode_statuses(mix_id, day_ago).await?; - - // if we have no statuses, the node doesn't exist (or monitor is down), but either way, we can't make a report - if statuses.is_empty() { - return Err(NymApiStorageError::MixnodeReportNotFound { mix_id }); - } - - // determine the number of runs the mixnode should have been online for - let last_hour_runs_count = self - .get_monitor_runs_count(hour_ago, now.unix_timestamp()) - .await?; - let last_day_runs_count = self - .get_monitor_runs_count(day_ago, now.unix_timestamp()) - .await?; - - let Some(mixnode_identity) = self.manager.get_mixnode_identity_key(mix_id).await? else { - return Err(NymApiStorageError::DatabaseInconsistency { reason: format!("The node {mix_id} doesn't have an identity even though we have status information on it!") }); - }; - - Ok(MixnodeStatusReport::construct_from_last_day_reports( - now, - mix_id, - mixnode_identity, - statuses, - last_hour_runs_count, - last_day_runs_count, - )) - } - - pub(crate) async fn construct_gateway_report( - &self, - node_id: NodeId, - ) -> Result { - let now = OffsetDateTime::now_utc(); - let day_ago = (now - ONE_DAY).unix_timestamp(); - let hour_ago = (now - ONE_HOUR).unix_timestamp(); - - let statuses = self.get_gateway_statuses(node_id, day_ago).await?; - - // if we have no statuses, the node doesn't exist (or monitor is down), but either way, we can't make a report - if statuses.is_empty() { - return Err(NymApiStorageError::GatewayReportNotFound { node_id }); - } - - // determine the number of runs the gateway should have been online for - let last_hour_runs_count = self - .get_monitor_runs_count(hour_ago, now.unix_timestamp()) - .await?; - let last_day_runs_count = self - .get_monitor_runs_count(day_ago, now.unix_timestamp()) - .await?; - - let Some(gateway_identity) = self.manager.get_gateway_identity_key(node_id).await? else { - return Err(NymApiStorageError::DatabaseInconsistency { reason: format!("The node {node_id} doesn't have an identity even though we have status information on it!") }); - }; - - Ok(GatewayStatusReport::construct_from_last_day_reports( - now, - node_id, - gateway_identity, - statuses, - last_hour_runs_count, - last_day_runs_count, - )) - } - pub(crate) async fn get_mixnode_uptime_history( &self, mix_id: NodeId, diff --git a/nym-api/src/unstable_routes/v1/account/cache.rs b/nym-api/src/unstable_routes/v1/account/cache.rs index 75fe83a1fb3..23623b2bf38 100644 --- a/nym-api/src/unstable_routes/v1/account/cache.rs +++ b/nym-api/src/unstable_routes/v1/account/cache.rs @@ -1,6 +1,8 @@ use crate::unstable_routes::v1::account::data_collector::AddressDataCollector; use crate::unstable_routes::v1::account::models::{NyxAccountDelegationDetails, NyxAccountDetails}; -use crate::{node_status_api::models::AxumResult, nym_contract_cache::cache::NymContractCache}; +use crate::{ + mixnet_contract_cache::cache::MixnetContractCache, node_status_api::models::AxumResult, +}; use moka::{future::Cache, Entry}; use nym_validator_client::nyxd::AccountId; use std::{sync::Arc, time::Duration}; @@ -48,7 +50,7 @@ impl AddressInfoCache { pub(crate) async fn collect_balances( &self, nyxd_client: crate::nyxd::Client, - nym_contract_cache: NymContractCache, + nym_contract_cache: MixnetContractCache, base_denom: String, address: &str, account_id: AccountId, diff --git a/nym-api/src/unstable_routes/v1/account/data_collector.rs b/nym-api/src/unstable_routes/v1/account/data_collector.rs index 7d130e9f94f..4d29f6dc29e 100644 --- a/nym-api/src/unstable_routes/v1/account/data_collector.rs +++ b/nym-api/src/unstable_routes/v1/account/data_collector.rs @@ -3,8 +3,8 @@ use crate::unstable_routes::v1::account::models::NyxAccountDelegationRewardDetails; use crate::{ + mixnet_contract_cache::cache::MixnetContractCache, node_status_api::models::{AxumErrorResponse, AxumResult}, - nym_contract_cache::cache::NymContractCache, }; use cosmwasm_std::{Coin, Decimal}; use nym_mixnet_contract_common::NodeRewarding; @@ -14,7 +14,7 @@ use tracing::error; pub(crate) struct AddressDataCollector { nyxd_client: crate::nyxd::Client, - nym_contract_cache: NymContractCache, + nym_contract_cache: MixnetContractCache, account_id: AccountId, total_value: u128, operator_rewards: u128, @@ -26,7 +26,7 @@ pub(crate) struct AddressDataCollector { impl AddressDataCollector { pub(crate) fn new( nyxd_client: crate::nyxd::Client, - nym_contract_cache: NymContractCache, + nym_contract_cache: MixnetContractCache, base_denom: String, account_id: AccountId, ) -> Self {