added basic key rotation information to mixnet contract

This commit is contained in:
Jędrzej Stuczyński
2025-05-01 14:25:12 +01:00
parent a0c6ab8f83
commit 8eb45c2f7a
18 changed files with 200 additions and 41 deletions
@@ -28,12 +28,12 @@ use nym_mixnet_contract_common::{
ContractBuildInformation, ContractState, ContractStateParams, CurrentIntervalResponse,
CurrentNymNodeVersionResponse, Delegation, EpochEventId, EpochRewardedSet, EpochStatus,
GatewayBond, GatewayBondResponse, GatewayOwnershipResponse, HistoricalNymNodeVersionEntry,
IdentityKey, IdentityKeyRef, IntervalEventId, MixNodeBond, MixNodeDetails,
MixOwnershipResponse, MixnodeDetailsByIdentityResponse, MixnodeDetailsResponse, NodeId,
NumberOfPendingEventsResponse, NymNodeBond, NymNodeDetails, NymNodeVersionHistoryResponse,
PagedAllDelegationsResponse, PagedDelegatorDelegationsResponse, PagedGatewayResponse,
PagedMixnodeBondsResponse, PagedNodeDelegationsResponse, PendingEpochEvent,
PendingEpochEventResponse, PendingEpochEventsResponse, PendingIntervalEvent,
IdentityKey, IdentityKeyRef, IntervalEventId, KeyRotationIdResponse, KeyRotationState,
MixNodeBond, MixNodeDetails, MixOwnershipResponse, MixnodeDetailsByIdentityResponse,
MixnodeDetailsResponse, NodeId, NumberOfPendingEventsResponse, NymNodeBond, NymNodeDetails,
NymNodeVersionHistoryResponse, PagedAllDelegationsResponse, PagedDelegatorDelegationsResponse,
PagedGatewayResponse, PagedMixnodeBondsResponse, PagedNodeDelegationsResponse,
PendingEpochEvent, PendingEpochEventResponse, PendingEpochEventsResponse, PendingIntervalEvent,
PendingIntervalEventResponse, PendingIntervalEventsResponse, QueryMsg as MixnetQueryMsg,
RewardedSet, UnbondedMixnode,
};
@@ -546,6 +546,16 @@ pub trait MixnetQueryClient {
})
.await
}
async fn get_key_rotation_state(&self) -> Result<KeyRotationState, NyxdError> {
self.query_mixnet_contract(MixnetQueryMsg::GetKeyRotationState {})
.await
}
async fn get_key_rotation_id(&self) -> Result<KeyRotationIdResponse, NyxdError> {
self.query_mixnet_contract(MixnetQueryMsg::GetKeyRotationId {})
.await
}
}
// extension trait to the query client to deal with the paged queries
@@ -40,3 +40,6 @@ contract-testing = []
utoipa = ["dep:utoipa"]
schema = ["cw2"]
generate-ts = ['ts-rs']
[lints]
workspace = true
@@ -0,0 +1,71 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::EpochId;
use cosmwasm_schema::cw_serde;
pub type KeyRotationId = u32;
#[cw_serde]
pub struct KeyRotationState {
/// Defines how long each key rotation is valid for (in terms of epochs)
pub validity_epochs: u32,
/// Records the initial epoch_id when the key rotation has been introduced (0 for fresh contracts).
/// It is used for determining when rotation is meant to advance.
pub initial_epoch_id: EpochId,
}
impl KeyRotationState {
pub fn key_rotation_id(&self, current_epoch_id: EpochId) -> KeyRotationId {
let diff = current_epoch_id.saturating_sub(self.initial_epoch_id);
let full_rots = diff / self.validity_epochs;
full_rots
}
}
#[cw_serde]
pub struct KeyRotationIdResponse {
pub rotation_id: KeyRotationId,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn key_rotation_id() {
let state = KeyRotationState {
validity_epochs: 24,
initial_epoch_id: 0,
};
assert_eq!(0, state.key_rotation_id(0));
assert_eq!(0, state.key_rotation_id(23));
assert_eq!(1, state.key_rotation_id(24));
assert_eq!(1, state.key_rotation_id(47));
assert_eq!(2, state.key_rotation_id(48));
let state = KeyRotationState {
validity_epochs: 12,
initial_epoch_id: 0,
};
assert_eq!(0, state.key_rotation_id(0));
assert_eq!(0, state.key_rotation_id(11));
assert_eq!(1, state.key_rotation_id(12));
assert_eq!(1, state.key_rotation_id(23));
assert_eq!(2, state.key_rotation_id(24));
let state = KeyRotationState {
validity_epochs: 24,
initial_epoch_id: 10000,
};
assert_eq!(0, state.key_rotation_id(123));
assert_eq!(0, state.key_rotation_id(10000));
assert_eq!(0, state.key_rotation_id(10001));
assert_eq!(0, state.key_rotation_id(10023));
assert_eq!(1, state.key_rotation_id(10024));
assert_eq!(1, state.key_rotation_id(10047));
assert_eq!(2, state.key_rotation_id(10048));
assert_eq!(2, state.key_rotation_id(10060));
}
}
@@ -1,10 +1,6 @@
// Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
#![warn(clippy::expect_used)]
#![warn(clippy::unwrap_used)]
#![warn(clippy::todo)]
mod config_score;
pub mod constants;
pub mod delegation;
@@ -13,6 +9,7 @@ pub mod events;
pub mod gateway;
pub mod helpers;
pub mod interval;
pub mod key_rotation;
pub mod mixnode;
pub mod msg;
pub mod nym_node;
@@ -37,6 +34,7 @@ pub use gateway::{
pub use interval::{
CurrentIntervalResponse, EpochId, EpochState, EpochStatus, Interval, IntervalId,
};
pub use key_rotation::*;
pub use mixnode::{
LegacyMixLayer, MixNode, MixNodeBond, MixNodeConfigUpdate, MixNodeDetails,
MixOwnershipResponse, MixnodeDetailsByIdentityResponse, MixnodeDetailsResponse, NodeCostParams,
@@ -170,6 +170,11 @@ impl NodeRewarding {
}
}
// we panic here as opposed to returning an error as this is undefined behaviour,
// because the pledge amount has decreased (i.e. slashing has occurred) which
// should not be possible under any situation. at this point we don't know how many other things
// might have failed so we have to bail
#[allow(clippy::panic)]
pub fn pending_detailed_operator_reward(&self, original_pledge: &Coin) -> StdResult<Decimal> {
let initial_dec = original_pledge.amount.into_base_decimal()?;
if initial_dec > self.operator {
@@ -189,6 +194,11 @@ impl NodeRewarding {
Ok(truncate_reward(delegator_reward, &delegation.amount.denom))
}
// we panic here as opposed to returning an error as this is undefined behaviour,
// because the pledge amount has decreased (i.e. slashing has occurred) which
// should not be possible under any situation. at this point we don't know how many other things
// might have failed so we have to bail
#[allow(clippy::panic)]
pub fn withdraw_operator_reward(
&mut self,
original_pledge: &Coin,
@@ -35,6 +35,7 @@ use crate::{
PreassignedGatewayIdsResponse,
},
interval::{CurrentIntervalResponse, EpochStatus},
key_rotation::{KeyRotationIdResponse, KeyRotationState},
mixnode::{
MixOwnershipResponse, MixStakeSaturationResponse, MixnodeDetailsByIdentityResponse,
MixnodeDetailsResponse, MixnodeRewardingDetailsResponse, PagedMixnodeBondsResponse,
@@ -857,6 +858,15 @@ pub enum QueryMsg {
/// Cosmos address used for the query of the signing nonce.
address: String,
},
// sphinx key rotation-related
#[cfg_attr(feature = "schema", returns(KeyRotationState))]
/// Gets the current state config of the key rotation (i.e. starting epoch id and validity duration)
GetKeyRotationState {},
/// Gets the current key rotation id
#[cfg_attr(feature = "schema", returns(KeyRotationIdResponse))]
GetKeyRotationId {},
}
#[cw_serde]
@@ -151,6 +151,9 @@ impl Simulator {
}
}
// this code is not meant to be used in production systems, only in tests
// so a panic due to inconsistent arguments is fine
#[allow(clippy::panic)]
pub fn simulate_epoch(
&mut self,
node_params: &BTreeMap<NodeId, NodeRewardingParameters>,
@@ -21,7 +21,7 @@ nym-sphinx-types = { path = "../types" }
nym-topology = { path = "../../topology" }
[target."cfg(target_arch = \"wasm32\")".dependencies.wasm-bindgen]
version = "0.2.95"
workspace = true
[dev-dependencies]
rand_chacha = { workspace = true }
-3
View File
@@ -1193,7 +1193,6 @@ version = "1.5.1"
dependencies = [
"anyhow",
"bs58",
"cosmwasm-derive",
"cosmwasm-schema",
"cosmwasm-std",
"cw-controllers",
@@ -1208,8 +1207,6 @@ dependencies = [
"rand_chacha",
"semver",
"serde",
"thiserror 2.0.12",
"time",
]
[[package]]
+10
View File
@@ -55,3 +55,13 @@ sylvia = "1.3.3"
schemars = "0.8.16"
thiserror = "2.0.11"
[workspace.lints.clippy]
unwrap_used = "deny"
expect_used = "deny"
todo = "deny"
dbg_macro = "deny"
exit = "deny"
panic = "deny"
unimplemented = "deny"
unreachable = "deny"
+3 -3
View File
@@ -33,7 +33,6 @@ nym-contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts
cosmwasm-schema = { workspace = true, optional = true }
cosmwasm-std = { workspace = true }
cosmwasm-derive = { workspace = true }
cw-controllers = { workspace = true }
cw2 = { workspace = true }
cw-storage-plus = { workspace = true }
@@ -41,8 +40,6 @@ cw-storage-plus = { workspace = true }
bs58 = { workspace = true }
serde = { workspace = true, default-features = false, features = ["derive"] }
semver = { workspace = true }
thiserror = { workspace = true }
time = { version = "0.3", features = ["macros"] }
[dev-dependencies]
anyhow.workspace = true
@@ -55,3 +52,6 @@ easy-addr = { path = "../../common/cosmwasm-smart-contracts/easy_addr" }
default = []
contract-testing = ["mixnet-contract-common/contract-testing"]
schema-gen = ["mixnet-contract-common/schema", "cosmwasm-schema"]
[lints]
workspace = true
+1
View File
@@ -78,6 +78,7 @@ pub const NYMNODE_ROLES_ASSIGNMENT_NAMESPACE: &str = "roles";
pub const NYMNODE_REWARDED_SET_METADATA_NAMESPACE: &str = "roles_metadata";
pub const NYMNODE_ACTIVE_ROLE_ASSIGNMENT_KEY: &str = "active_roles";
pub const KEY_ROTATION_STATE_KEY: &str = "key_rot_state";
pub const NODE_ID_COUNTER_KEY: &str = "nic";
pub const PENDING_MIXNODE_CHANGES_NAMESPACE: &str = "pmc";
pub const MIXNODES_PK_NAMESPACE: &str = "mnn";
+15 -6
View File
@@ -5,6 +5,7 @@ use crate::constants::INITIAL_PLEDGE_AMOUNT;
use crate::interval::storage as interval_storage;
use crate::mixnet_contract_settings::storage as mixnet_params_storage;
use crate::nodes::storage as nymnodes_storage;
use crate::queued_migrations::introduce_key_rotation_id;
use crate::rewards::storage::RewardingStorage;
use cosmwasm_std::{
entry_point, to_json_binary, Addr, Coin, Deps, DepsMut, Env, MessageInfo, QueryResponse,
@@ -598,6 +599,14 @@ pub fn query(
QueryMsg::GetSigningNonce { address } => to_json_binary(
&crate::signing::queries::query_current_signing_nonce(deps, address)?,
),
// sphinx key rotation-related
QueryMsg::GetKeyRotationState {} => {
to_json_binary(&crate::nodes::queries::query_key_rotation_state(deps)?)
}
QueryMsg::GetKeyRotationId {} => {
to_json_binary(&crate::nodes::queries::query_key_rotation_id(deps)?)
}
};
Ok(query_res?)
@@ -605,18 +614,18 @@ pub fn query(
#[entry_point]
pub fn migrate(
deps: DepsMut<'_>,
mut deps: DepsMut<'_>,
_env: Env,
msg: MigrateMsg,
) -> Result<Response, MixnetContractError> {
set_build_information!(deps.storage)?;
cw2::ensure_from_older_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
// let skip_state_updates = msg.unsafe_skip_state_updates.unwrap_or(false);
//
// if !skip_state_updates {
//
// }
let skip_state_updates = msg.unsafe_skip_state_updates.unwrap_or(false);
if !skip_state_updates {
introduce_key_rotation_id(deps.branch())?;
}
// due to circular dependency on contract addresses (i.e. mixnet contract requiring vesting contract address
// and vesting contract requiring the mixnet contract address), if we ever want to deploy any new fresh
+15 -1
View File
@@ -6,6 +6,7 @@ use crate::constants::{
NYM_NODE_DETAILS_DEFAULT_RETRIEVAL_LIMIT, NYM_NODE_DETAILS_MAX_RETRIEVAL_LIMIT,
UNBONDED_NYM_NODES_DEFAULT_RETRIEVAL_LIMIT, UNBONDED_NYM_NODES_MAX_RETRIEVAL_LIMIT,
};
use crate::interval::storage as interval_storage;
use crate::nodes::helpers::{
attach_nym_node_details, get_node_details_by_id, get_node_details_by_identity,
get_node_details_by_owner,
@@ -21,7 +22,9 @@ use mixnet_contract_common::nym_node::{
PagedNymNodeDetailsResponse, PagedUnbondedNymNodesResponse, Role, RolesMetadataResponse,
StakeSaturationResponse, UnbondedNodeResponse,
};
use mixnet_contract_common::{NodeId, NymNodeBond, NymNodeDetails};
use mixnet_contract_common::{
KeyRotationIdResponse, KeyRotationState, NodeId, NymNodeBond, NymNodeDetails,
};
use nym_contracts_common::IdentityKey;
pub(crate) fn query_nymnode_bonds_paged(
@@ -257,3 +260,14 @@ pub fn query_stake_saturation(
uncapped_saturation: Some(node_rewarding.uncapped_bond_saturation(&rewarding_params)),
})
}
pub fn query_key_rotation_state(deps: Deps<'_>) -> StdResult<KeyRotationState> {
storage::KEY_ROTATION_STATE.load(deps.storage)
}
pub fn query_key_rotation_id(deps: Deps<'_>) -> StdResult<KeyRotationIdResponse> {
let interval = interval_storage::current_interval(deps.storage)?;
let rotation_state = storage::KEY_ROTATION_STATE.load(deps.storage)?;
let rotation_id = rotation_state.key_rotation_id(interval.current_epoch_absolute_id());
Ok(KeyRotationIdResponse { rotation_id })
}
+11 -2
View File
@@ -2,11 +2,11 @@
// SPDX-License-Identifier: Apache-2.0
use crate::nodes::storage::rewarded_set::{ACTIVE_ROLES_BUCKET, ROLES, ROLES_METADATA};
use crate::nodes::storage::{nym_nodes, NYMNODE_ID_COUNTER};
use crate::nodes::storage::{nym_nodes, KEY_ROTATION_STATE, NYMNODE_ID_COUNTER};
use cosmwasm_std::{StdResult, Storage};
use mixnet_contract_common::error::MixnetContractError;
use mixnet_contract_common::nym_node::{RewardedSetMetadata, Role};
use mixnet_contract_common::{EpochId, NodeId, NymNodeBond, RoleAssignment};
use mixnet_contract_common::{EpochId, KeyRotationState, NodeId, NymNodeBond, RoleAssignment};
use serde::{Deserialize, Serialize};
#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, Eq, PartialEq)]
@@ -124,6 +124,15 @@ pub(crate) fn initialise_storage(storage: &mut dyn Storage) -> Result<(), Mixnet
ROLES_METADATA.save(storage, active_bucket as u8, &Default::default())?;
ROLES_METADATA.save(storage, inactive_bucket as u8, &Default::default())?;
// since we're initialising fresh storage, the current epoch_id is 0
KEY_ROTATION_STATE.save(
storage,
&KeyRotationState {
validity_epochs: 24,
initial_epoch_id: 0,
},
)?;
Ok(())
}
+9 -6
View File
@@ -2,23 +2,26 @@
// SPDX-License-Identifier: Apache-2.0
use crate::constants::{
NODE_ID_COUNTER_KEY, NYMNODE_ACTIVE_ROLE_ASSIGNMENT_KEY, NYMNODE_IDENTITY_IDX_NAMESPACE,
NYMNODE_OWNER_IDX_NAMESPACE, NYMNODE_PK_NAMESPACE, NYMNODE_REWARDED_SET_METADATA_NAMESPACE,
NYMNODE_ROLES_ASSIGNMENT_NAMESPACE, PENDING_NYMNODE_CHANGES_NAMESPACE,
UNBONDED_NYMNODE_IDENTITY_IDX_NAMESPACE, UNBONDED_NYMNODE_OWNER_IDX_NAMESPACE,
UNBONDED_NYMNODE_PK_NAMESPACE,
KEY_ROTATION_STATE_KEY, NODE_ID_COUNTER_KEY, NYMNODE_ACTIVE_ROLE_ASSIGNMENT_KEY,
NYMNODE_IDENTITY_IDX_NAMESPACE, NYMNODE_OWNER_IDX_NAMESPACE, NYMNODE_PK_NAMESPACE,
NYMNODE_REWARDED_SET_METADATA_NAMESPACE, NYMNODE_ROLES_ASSIGNMENT_NAMESPACE,
PENDING_NYMNODE_CHANGES_NAMESPACE, UNBONDED_NYMNODE_IDENTITY_IDX_NAMESPACE,
UNBONDED_NYMNODE_OWNER_IDX_NAMESPACE, UNBONDED_NYMNODE_PK_NAMESPACE,
};
use crate::nodes::storage::helpers::RoleStorageBucket;
use cosmwasm_std::Addr;
use cw_storage_plus::{Index, IndexList, IndexedMap, Item, Map, MultiIndex, UniqueIndex};
use mixnet_contract_common::nym_node::{NymNodeBond, RewardedSetMetadata, Role, UnbondedNymNode};
use mixnet_contract_common::{NodeId, PendingNodeChanges};
use mixnet_contract_common::{KeyRotationState, NodeId, PendingNodeChanges};
use nym_contracts_common::IdentityKey;
pub(crate) mod helpers;
pub(crate) use helpers::*;
/// Item recording the current state of the key rotation setup
pub const KEY_ROTATION_STATE: Item<KeyRotationState> = Item::new(KEY_ROTATION_STATE_KEY);
// IMPORTANT NOTE: we're using the same storage key as we had for MIXNODE_ID_COUNTER,
// so that we could start from the old values
pub const NYMNODE_ID_COUNTER: Item<NodeId> = Item::new(NODE_ID_COUNTER_KEY);
+20 -1
View File
@@ -1,2 +1,21 @@
// Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net>
// Copyright 2022-2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::interval::storage as interval_storage;
use crate::nodes::storage as nymnodes_storage;
use cosmwasm_std::DepsMut;
use mixnet_contract_common::error::MixnetContractError;
use mixnet_contract_common::KeyRotationState;
pub fn introduce_key_rotation_id(deps: DepsMut) -> Result<(), MixnetContractError> {
let current_epoch_id =
interval_storage::current_interval(deps.storage)?.current_epoch_absolute_id();
nymnodes_storage::KEY_ROTATION_STATE.save(
deps.storage,
&KeyRotationState {
validity_epochs: 24,
initial_epoch_id: current_epoch_id,
},
)?;
Ok(())
}
-8
View File
@@ -56,14 +56,6 @@ impl AsRef<x25519::PrivateKey> for SphinxPrivateKey {
}
}
// impl Deref for SphinxPrivateKey {
// type Target = x25519::PrivateKey;
//
// fn deref(&self) -> &Self::Target {
// &self.inner
// }
// }
pub(crate) struct SphinxPublicKey {
pub(crate) rotation_id: u32,
pub(crate) inner: x25519::PublicKey,