diff --git a/Cargo.lock b/Cargo.lock index 58a817f3dd..0a069937e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3210,6 +3210,7 @@ dependencies = [ "nym-coconut-bandwidth-contract-common", "nym-coconut-dkg-common", "nym-contracts-common", + "nym-crypto", "nym-mixnet-contract-common", "nym-multisig-contract-common", "nym-network-defaults", diff --git a/common/client-libs/validator-client/src/nyxd/traits/mixnet_signing_client.rs b/common/client-libs/validator-client/src/nyxd/traits/mixnet_signing_client.rs index 159835f5a7..2edb8cdbd6 100644 --- a/common/client-libs/validator-client/src/nyxd/traits/mixnet_signing_client.rs +++ b/common/client-libs/validator-client/src/nyxd/traits/mixnet_signing_client.rs @@ -1,4 +1,4 @@ -// Copyright 2022 - Nym Technologies SA +// Copyright 2022-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use crate::nyxd::coin::Coin; @@ -9,6 +9,7 @@ use crate::nyxd::{Fee, NyxdClient, SigningCosmWasmClient}; use async_trait::async_trait; use cosmrs::AccountId; use nym_contracts_common::signing::MessageSignature; +use nym_mixnet_contract_common::families::FamilyHead; use nym_mixnet_contract_common::gateway::GatewayConfigUpdate; use nym_mixnet_contract_common::mixnode::{MixNodeConfigUpdate, MixNodeCostParams}; use nym_mixnet_contract_common::reward_params::{IntervalRewardingParamsUpdate, Performance}; @@ -146,25 +147,16 @@ pub trait MixnetSigningClient { // family related async fn create_family( &self, - owner_signature: String, label: String, fee: Option, ) -> Result { - self.execute_mixnet_contract( - fee, - MixnetExecuteMsg::CreateFamily { - owner_signature, - label, - }, - vec![], - ) - .await + self.execute_mixnet_contract(fee, MixnetExecuteMsg::CreateFamily { label }, vec![]) + .await } async fn create_family_on_behalf( &self, owner_address: String, - owner_signature: String, label: String, fee: Option, ) -> Result { @@ -172,7 +164,6 @@ pub trait MixnetSigningClient { fee, MixnetExecuteMsg::CreateFamilyOnBehalf { owner_address, - owner_signature, label, }, vec![], @@ -182,14 +173,14 @@ pub trait MixnetSigningClient { async fn join_family( &self, - signature: String, - family_head: String, + join_permit: MessageSignature, + family_head: FamilyHead, fee: Option, ) -> Result { self.execute_mixnet_contract( fee, MixnetExecuteMsg::JoinFamily { - signature, + join_permit, family_head, }, vec![], @@ -200,17 +191,15 @@ pub trait MixnetSigningClient { async fn join_family_on_behalf( &self, member_address: String, - node_identity_signature: String, - family_signature: String, - family_head: String, + join_permit: MessageSignature, + family_head: FamilyHead, fee: Option, ) -> Result { self.execute_mixnet_contract( fee, MixnetExecuteMsg::JoinFamilyOnBehalf { member_address, - node_identity_signature, - family_signature, + join_permit, family_head, }, vec![], @@ -220,33 +209,23 @@ pub trait MixnetSigningClient { async fn leave_family( &self, - signature: String, - family_head: String, + family_head: FamilyHead, fee: Option, ) -> Result { - self.execute_mixnet_contract( - fee, - MixnetExecuteMsg::LeaveFamily { - signature, - family_head, - }, - vec![], - ) - .await + self.execute_mixnet_contract(fee, MixnetExecuteMsg::LeaveFamily { family_head }, vec![]) + .await } async fn leave_family_on_behalf( &self, member_address: String, - node_identity_signature: String, - family_head: String, + family_head: FamilyHead, fee: Option, ) -> Result { self.execute_mixnet_contract( fee, MixnetExecuteMsg::LeaveFamilyOnBehalf { member_address, - node_identity_signature, family_head, }, vec![], @@ -256,22 +235,16 @@ pub trait MixnetSigningClient { async fn kick_family_member( &self, - signature: String, member: String, fee: Option, ) -> Result { - self.execute_mixnet_contract( - fee, - MixnetExecuteMsg::KickFamilyMember { signature, member }, - vec![], - ) - .await + self.execute_mixnet_contract(fee, MixnetExecuteMsg::KickFamilyMember { member }, vec![]) + .await } async fn kick_family_member_on_behalf( &self, head_address: String, - signature: String, member: String, fee: Option, ) -> Result { @@ -279,7 +252,6 @@ pub trait MixnetSigningClient { fee, MixnetExecuteMsg::KickFamilyMemberOnBehalf { head_address, - signature, member, }, vec![], diff --git a/common/client-libs/validator-client/src/nyxd/traits/vesting_signing_client.rs b/common/client-libs/validator-client/src/nyxd/traits/vesting_signing_client.rs index 296b3219dc..078667b042 100644 --- a/common/client-libs/validator-client/src/nyxd/traits/vesting_signing_client.rs +++ b/common/client-libs/validator-client/src/nyxd/traits/vesting_signing_client.rs @@ -7,6 +7,7 @@ use crate::nyxd::error::NyxdError; use crate::nyxd::{Coin, Fee, NyxdClient}; use async_trait::async_trait; use nym_contracts_common::signing::MessageSignature; +use nym_mixnet_contract_common::families::FamilyHead; use nym_mixnet_contract_common::gateway::GatewayConfigUpdate; use nym_mixnet_contract_common::mixnode::{MixNodeConfigUpdate, MixNodeCostParams}; use nym_mixnet_contract_common::{Gateway, MixId, MixNode}; @@ -136,6 +137,50 @@ pub trait VestingSigningClient { cap: Option, fee: Option, ) -> Result; + + async fn vesting_create_family( + &self, + label: String, + fee: Option, + ) -> Result { + self.execute_vesting_contract(fee, VestingExecuteMsg::CreateFamily { label }, vec![]) + .await + } + + async fn vesting_join_family( + &self, + join_permit: MessageSignature, + family_head: FamilyHead, + fee: Option, + ) -> Result { + self.execute_vesting_contract( + fee, + VestingExecuteMsg::JoinFamily { + join_permit, + family_head, + }, + vec![], + ) + .await + } + + async fn vesting_leave_family( + &self, + family_head: FamilyHead, + fee: Option, + ) -> Result { + self.execute_vesting_contract(fee, VestingExecuteMsg::LeaveFamily { family_head }, vec![]) + .await + } + + async fn vesting_kick_family_member( + &self, + member: String, + fee: Option, + ) -> Result { + self.execute_vesting_contract(fee, VestingExecuteMsg::KickFamilyMember { member }, vec![]) + .await + } } #[async_trait] diff --git a/common/commands/Cargo.toml b/common/commands/Cargo.toml index fcbc84a80e..86f8fcc54a 100644 --- a/common/commands/Cargo.toml +++ b/common/commands/Cargo.toml @@ -29,6 +29,7 @@ cosmrs = { git = "https://github.com/neacsu/cosmos-rust", branch = "neacsu/feegr cosmwasm-std = { workspace = true } validator-client = { path = "../client-libs/validator-client", features = ["nyxd-client"] } +nym-crypto = { path = "../../common/crypto", features = ["asymmetric"] } nym-network-defaults = { path = "../network-defaults" } nym-contracts-common = { path = "../cosmwasm-smart-contracts/contracts-common" } nym-mixnet-contract-common = { path = "../cosmwasm-smart-contracts/mixnet-contract" } diff --git a/common/commands/src/validator/mixnet/operators/mixnode/families/create_family.rs b/common/commands/src/validator/mixnet/operators/mixnode/families/create_family.rs new file mode 100644 index 0000000000..dd0d24a0e3 --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/mixnode/families/create_family.rs @@ -0,0 +1,37 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::context::SigningClient; +use clap::Parser; +use log::info; +use validator_client::nyxd::traits::MixnetSigningClient; +use validator_client::nyxd::VestingSigningClient; + +#[derive(Debug, Parser)] +pub struct Args { + /// Label that is going to be used for creating the family + #[arg(long)] + pub family_label: String, + + /// Indicates whether the family is going to get created via a vesting account + #[arg(long)] + pub with_vesting_account: bool, +} + +pub async fn create_family(args: Args, client: SigningClient) { + info!("Create family"); + + let res = if args.with_vesting_account { + client + .vesting_create_family(args.family_label, None) + .await + .expect("failed to create family with vesting account") + } else { + client + .create_family(args.family_label, None) + .await + .expect("failed to create family") + }; + + info!("Family creation result: {:?}", res); +} diff --git a/common/commands/src/validator/mixnet/operators/mixnode/families/create_family_join_permit_sign_payload.rs b/common/commands/src/validator/mixnet/operators/mixnode/families/create_family_join_permit_sign_payload.rs new file mode 100644 index 0000000000..2654a337e4 --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/mixnode/families/create_family_join_permit_sign_payload.rs @@ -0,0 +1,72 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::context::QueryClient; +use crate::utils::account_id_to_cw_addr; +use clap::Parser; +use cosmrs::AccountId; +use log::info; +use nym_crypto::asymmetric::identity; +use nym_mixnet_contract_common::construct_family_join_permit; +use nym_mixnet_contract_common::families::FamilyHead; +use validator_client::nyxd::traits::MixnetQueryClient; + +#[derive(Debug, Parser)] +pub struct Args { + /// Account address (i.e. owner of the family head) which will be used for issuing the permit + #[arg(long)] + pub address: AccountId, + + /// Indicates whether the member joining the family is going to use the vesting account for joining. + #[arg(long)] + pub with_vesting_account: bool, + + // might as well validate the value when parsing the arguments + /// Identity of the member for whom we're issuing the permit + #[arg(long)] + pub member: identity::PublicKey, +} + +pub async fn create_family_join_permit_sign_payload(args: Args, client: QueryClient) { + info!("Create family join permit sign payload"); + + // get the address of our mixnode to recover the family head information + let Some(mixnode) = client.get_owned_mixnode(&args.address).await.unwrap().mixnode_details else { + eprintln!("{} does not seem to even own a mixnode!", args.address); + return + }; + + // make sure this mixnode is actually a family head + if client + .get_node_family_by_head(mixnode.bond_information.identity()) + .await + .unwrap() + .is_none() + { + eprintln!("{} does not even seem to own a family!", args.address); + return; + } + + let nonce = match client.get_signing_nonce(&args.address).await { + Ok(nonce) => nonce, + Err(err) => { + eprint!( + "failed to query for the signing nonce of {}: {err}", + args.address + ); + return; + } + }; + + // let address = account_id_to_cw_addr(&args.address); + let proxy = if args.with_vesting_account { + Some(account_id_to_cw_addr(client.vesting_contract_address())) + } else { + None + }; + + let head = FamilyHead::new(mixnode.bond_information.identity()); + + let payload = construct_family_join_permit(nonce, head, proxy, args.member.to_base58_string()); + println!("{}", payload.to_base58_string().unwrap()) +} diff --git a/common/commands/src/validator/mixnet/operators/mixnode/families/join_family.rs b/common/commands/src/validator/mixnet/operators/mixnode/families/join_family.rs new file mode 100644 index 0000000000..84d3866904 --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/mixnode/families/join_family.rs @@ -0,0 +1,46 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::context::SigningClient; +use clap::Parser; +use log::info; +use nym_contracts_common::signing::MessageSignature; +use nym_crypto::asymmetric::identity; +use nym_mixnet_contract_common::families::FamilyHead; +use validator_client::nyxd::traits::MixnetSigningClient; +use validator_client::nyxd::VestingSigningClient; + +#[derive(Debug, Parser)] +pub struct Args { + /// The head of the family that we intend to join + #[arg(long)] + pub family_head: identity::PublicKey, + + /// Indicates whether the member joining the family is going to do so via the vesting contract + #[arg(long)] + pub with_vesting_account: bool, + + /// Permission, as provided by the family head, for joining the family + #[arg(long)] + pub join_permit: MessageSignature, +} + +pub async fn join_family(args: Args, client: SigningClient) { + info!("Join family"); + + let family_head = FamilyHead::new(&args.family_head.to_base58_string()); + + let res = if args.with_vesting_account { + client + .vesting_join_family(args.join_permit, family_head, None) + .await + .expect("failed to join family with vesting account") + } else { + client + .join_family(args.join_permit, family_head, None) + .await + .expect("failed to join family") + }; + + info!("Family join result: {:?}", res); +} diff --git a/common/commands/src/validator/mixnet/operators/mixnode/families/kick_family_member.rs b/common/commands/src/validator/mixnet/operators/mixnode/families/kick_family_member.rs new file mode 100644 index 0000000000..29777dcb9e --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/mixnode/families/kick_family_member.rs @@ -0,0 +1,40 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::context::SigningClient; +use clap::Parser; +use log::info; +use nym_crypto::asymmetric::identity; +use validator_client::nyxd::traits::MixnetSigningClient; +use validator_client::nyxd::VestingSigningClient; + +#[derive(Debug, Parser)] +pub struct Args { + /// The member of the family that we intend to kick + #[arg(long)] + pub member: identity::PublicKey, + + /// Indicates whether the family was created (and managed) via the vesting contract + #[arg(long)] + pub with_vesting_account: bool, +} + +pub async fn kick_family_member(args: Args, client: SigningClient) { + info!("Leave family"); + + let member = args.member.to_base58_string(); + + let res = if args.with_vesting_account { + client + .vesting_kick_family_member(member, None) + .await + .expect("failed to kick family member with vesting account") + } else { + client + .kick_family_member(member, None) + .await + .expect("failed to kick family member") + }; + + info!("Family leave result: {:?}", res); +} diff --git a/common/commands/src/validator/mixnet/operators/mixnode/families/leave_family.rs b/common/commands/src/validator/mixnet/operators/mixnode/families/leave_family.rs new file mode 100644 index 0000000000..34d0842e16 --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/mixnode/families/leave_family.rs @@ -0,0 +1,41 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::context::SigningClient; +use clap::Parser; +use log::info; +use nym_crypto::asymmetric::identity; +use nym_mixnet_contract_common::families::FamilyHead; +use validator_client::nyxd::traits::MixnetSigningClient; +use validator_client::nyxd::VestingSigningClient; + +#[derive(Debug, Parser)] +pub struct Args { + /// The head of the family that we intend to leave + #[arg(long)] + pub family_head: identity::PublicKey, + + /// Indicates whether we joined the family via the vesting contract + #[arg(long)] + pub with_vesting_account: bool, +} + +pub async fn leave_family(args: Args, client: SigningClient) { + info!("Leave family"); + + let family_head = FamilyHead::new(&args.family_head.to_base58_string()); + + let res = if args.with_vesting_account { + client + .vesting_leave_family(family_head, None) + .await + .expect("failed to leave family with vesting account") + } else { + client + .leave_family(family_head, None) + .await + .expect("failed to leave family") + }; + + info!("Family leave result: {:?}", res); +} diff --git a/common/commands/src/validator/mixnet/operators/mixnode/families/mod.rs b/common/commands/src/validator/mixnet/operators/mixnode/families/mod.rs new file mode 100644 index 0000000000..c343ef977f --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/mixnode/families/mod.rs @@ -0,0 +1,35 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use clap::{Args, Subcommand}; + +pub mod create_family; +pub mod create_family_join_permit_sign_payload; +pub mod join_family; +pub mod kick_family_member; +pub mod leave_family; + +#[derive(Debug, Args)] +#[clap(args_conflicts_with_subcommands = true, subcommand_required = true)] +pub struct MixnetOperatorsMixnodeFamilies { + #[clap(subcommand)] + pub command: MixnetOperatorsMixnodeFamiliesCommands, +} + +#[derive(Debug, Subcommand)] +pub enum MixnetOperatorsMixnodeFamiliesCommands { + /// Create family + CreateFamily(create_family::Args), + + /// Join family + JoinFamily(join_family::Args), + + /// Leave family, + LeaveFamily(leave_family::Args), + + /// Kick family member + KickFamilyMember(kick_family_member::Args), + + /// Create a message payload that is required to get signed in order to obtain a permit for joining family + CreateFamilyJoinPermitSignPayload(create_family_join_permit_sign_payload::Args), +} diff --git a/common/commands/src/validator/mixnet/operators/mixnode/mod.rs b/common/commands/src/validator/mixnet/operators/mixnode/mod.rs index 5ba93b4e29..c3440c6e67 100644 --- a/common/commands/src/validator/mixnet/operators/mixnode/mod.rs +++ b/common/commands/src/validator/mixnet/operators/mixnode/mod.rs @@ -4,6 +4,7 @@ use clap::{Args, Subcommand}; pub mod bond_mixnode; +pub mod families; pub mod keys; pub mod mixnode_bonding_sign_payload; pub mod rewards; @@ -27,6 +28,8 @@ pub enum MixnetOperatorsMixnodeCommands { Rewards(rewards::MixnetOperatorsMixnodeRewards), /// Manage your mixnode settings stored in the directory Settings(settings::MixnetOperatorsMixnodeSettings), + /// Operations for mixnode families + Families(families::MixnetOperatorsMixnodeFamilies), /// Bond to a mixnode Bond(bond_mixnode::Args), /// Unbond from a mixnode diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/families.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/families.rs index 0ab73f6085..122e1690f6 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/families.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/families.rs @@ -1,7 +1,9 @@ use crate::{IdentityKey, IdentityKeyRef}; use cosmwasm_std::Addr; use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use std::fmt::{Display, Formatter}; +use std::str::FromStr; #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( @@ -20,9 +22,45 @@ pub struct Family { feature = "generate-ts", ts(export_to = "ts-packages/types/src/types/rust/NodeFamilyHead.ts") )] -#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq, JsonSchema)] +#[derive(Debug, Clone, Eq, PartialEq, JsonSchema)] pub struct FamilyHead(IdentityKey); +impl Serialize for FamilyHead { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + self.0.serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for FamilyHead { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let inner = IdentityKey::deserialize(deserializer)?; + Ok(FamilyHead(inner)) + } +} + +impl FromStr for FamilyHead { + type Err = ::Err; + + fn from_str(s: &str) -> Result { + // theoretically we should be verifying whether it's a valid base58 value + // (or even better, whether it's a valid ed25519 public key), but definition of + // `FamilyHead` might change later + Ok(FamilyHead(IdentityKey::from_str(s)?)) + } +} + +impl Display for FamilyHead { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + impl FamilyHead { pub fn new(identity: IdentityKeyRef<'_>) -> Self { FamilyHead(identity.to_string()) @@ -34,11 +72,11 @@ impl FamilyHead { } impl Family { - pub fn new(head: FamilyHead, proxy: Option, label: &str) -> Self { + pub fn new(head: FamilyHead, proxy: Option, label: String) -> Self { Family { head, proxy: proxy.map(|p| p.to_string()), - label: label.to_string(), + label, } } @@ -60,3 +98,21 @@ impl Family { &self.label } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn family_head_serde() { + let dummy = FamilyHead::new("foomp"); + + let ser_str = serde_json_wasm::to_string(&dummy).unwrap(); + let de_str: FamilyHead = serde_json_wasm::from_str(&ser_str).unwrap(); + assert_eq!(dummy, de_str); + + let ser_bytes = serde_json_wasm::to_vec(&dummy).unwrap(); + let de_bytes: FamilyHead = serde_json_wasm::from_slice(&ser_bytes).unwrap(); + assert_eq!(dummy, de_bytes); + } +} diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs index 83fa00be2f..015d750900 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs @@ -3,6 +3,7 @@ use crate::delegation::OwnerProxySubKey; use crate::error::MixnetContractError; +use crate::families::FamilyHead; use crate::gateway::GatewayConfigUpdate; use crate::helpers::IntoBaseDecimal; use crate::mixnode::{MixNodeConfigUpdate, MixNodeCostParams}; @@ -82,42 +83,35 @@ pub enum ExecuteMsg { // Families /// Only owner of the node can crate the family with node as head CreateFamily { - owner_signature: String, label: String, }, /// Family head needs to sign the joining node IdentityKey JoinFamily { - signature: String, - family_head: IdentityKey, + join_permit: MessageSignature, + family_head: FamilyHead, }, LeaveFamily { - signature: String, - family_head: IdentityKey, + family_head: FamilyHead, }, KickFamilyMember { - signature: String, member: IdentityKey, }, CreateFamilyOnBehalf { owner_address: String, - owner_signature: String, label: String, }, /// Family head needs to sign the joining node IdentityKey, MixNode needs to provide its signature proving that it wants to join the family JoinFamilyOnBehalf { member_address: String, - node_identity_signature: String, - family_signature: String, - family_head: IdentityKey, + join_permit: MessageSignature, + family_head: FamilyHead, }, LeaveFamilyOnBehalf { member_address: String, - node_identity_signature: String, - family_head: IdentityKey, + family_head: FamilyHead, }, KickFamilyMemberOnBehalf { head_address: String, - signature: String, member: IdentityKey, }, diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/signing_types.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/signing_types.rs index da4de4a9a9..57ad49dc96 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/signing_types.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/signing_types.rs @@ -1,7 +1,8 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::{Gateway, MixNode, MixNodeCostParams}; +use crate::families::FamilyHead; +use crate::{Gateway, IdentityKey, MixNode, MixNodeCostParams}; use contracts_common::signing::{ ContractMessageContent, MessageType, Nonce, SignableMessage, SigningPurpose, }; @@ -10,6 +11,7 @@ use serde::Serialize; pub type SignableMixNodeBondingMsg = SignableMessage>; pub type SignableGatewayBondingMsg = SignableMessage>; +pub type SignableFamilyJoinPermitMsg = SignableMessage; #[derive(Serialize)] pub struct MixnodeBondingPayload { @@ -77,75 +79,43 @@ pub fn construct_gateway_bonding_sign_payload( } #[derive(Serialize)] -pub struct FamilyCreationSignature { - label: String, - // TODO: add any extra fields? +pub struct FamilyJoinPermit { + // the granter of this permit + family_head: FamilyHead, + // whether the **member** will want to join via the proxy (i.e. vesting contract) + proxy: Option, + // the actual member we want to permit to join + member_node: IdentityKey, } -impl FamilyCreationSignature { - pub fn new(label: String) -> Self { - Self { label } +impl FamilyJoinPermit { + pub fn new(family_head: FamilyHead, proxy: Option, member_node: IdentityKey) -> Self { + Self { + family_head, + proxy, + member_node, + } } } -impl SigningPurpose for FamilyCreationSignature { +impl SigningPurpose for FamilyJoinPermit { fn message_type() -> MessageType { - MessageType::new("family-creation") + MessageType::new("family-join-permit") } } -#[derive(Serialize)] -pub struct FamilyJoinSignature { - family_head: String, - // TODO: add any extra fields? -} +pub fn construct_family_join_permit( + nonce: Nonce, + family_head: FamilyHead, + proxy: Option, + member_node: IdentityKey, +) -> SignableFamilyJoinPermitMsg { + let payload = FamilyJoinPermit::new(family_head, proxy, member_node); -impl FamilyJoinSignature { - pub fn new(family_head: String) -> Self { - Self { family_head } - } -} - -impl SigningPurpose for FamilyJoinSignature { - fn message_type() -> MessageType { - MessageType::new("family-join") - } -} - -#[derive(Serialize)] -pub struct FamilyLeaveSignature { - family_head: String, - // TODO: add any extra fields? -} - -impl FamilyLeaveSignature { - pub fn new(family_head: String) -> Self { - Self { family_head } - } -} - -impl SigningPurpose for FamilyLeaveSignature { - fn message_type() -> MessageType { - MessageType::new("family-leave") - } -} - -#[derive(Serialize)] -pub struct FamilyKickSignature { - member: String, - // TODO: add any extra fields? -} - -impl FamilyKickSignature { - pub fn new(member: String) -> Self { - Self { member } - } -} - -impl SigningPurpose for FamilyKickSignature { - fn message_type() -> MessageType { - MessageType::new("family-member-removal") - } + // note: we're NOT wrapping it in `ContractMessageContent` because the family head is not going to be the one + // sending the message to the contract + SignableMessage::new(nonce, payload) } // TODO: depending on our threat model, we should perhaps extend it to include all _on_behalf methods +// (update: but we trust our vesting contract since its compromise would be even more devastating so there's no need) diff --git a/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs b/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs index d55e9a0fa7..ed605dd64a 100644 --- a/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs +++ b/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs @@ -1,5 +1,6 @@ use contracts_common::signing::MessageSignature; use cosmwasm_std::{Coin, Timestamp}; +use mixnet_contract_common::families::FamilyHead; use mixnet_contract_common::{ gateway::GatewayConfigUpdate, mixnode::{MixNodeConfigUpdate, MixNodeCostParams}, @@ -60,21 +61,17 @@ pub enum ExecuteMsg { // Families /// Only owner of the node can crate the family with node as head CreateFamily { - owner_signature: String, label: String, }, /// Family head needs to sign the joining node IdentityKey, the Node provides its signature signaling consent to join the family JoinFamily { - node_identity_signature: String, - family_signature: String, - family_head: IdentityKey, + join_permit: MessageSignature, + family_head: FamilyHead, }, LeaveFamily { - node_identity_signature: String, - family_head: IdentityKey, + family_head: FamilyHead, }, KickFamilyMember { - signature: String, member: IdentityKey, }, TrackReward { diff --git a/contracts/mixnet/src/contract.rs b/contracts/mixnet/src/contract.rs index 0f3748b0d2..7275bacb7e 100644 --- a/contracts/mixnet/src/contract.rs +++ b/contracts/mixnet/src/contract.rs @@ -104,68 +104,56 @@ pub fn execute( crate::mixnodes::transactions::assign_mixnode_layer(deps, info, mix_id, layer) } // families - ExecuteMsg::CreateFamily { - owner_signature, - label, - } => crate::families::transactions::try_create_family(deps, info, owner_signature, &label), - ExecuteMsg::JoinFamily { - signature, - family_head, - } => { - crate::families::transactions::try_join_family(deps, info, None, signature, family_head) + ExecuteMsg::CreateFamily { label } => { + crate::families::transactions::try_create_family(deps, info, label) } - ExecuteMsg::LeaveFamily { - signature, + ExecuteMsg::JoinFamily { + join_permit, family_head, - } => crate::families::transactions::try_leave_family(deps, info, signature, family_head), - ExecuteMsg::KickFamilyMember { signature, member } => { - crate::families::transactions::try_head_kick_member(deps, info, signature, &member) + } => crate::families::transactions::try_join_family(deps, info, join_permit, family_head), + ExecuteMsg::LeaveFamily { family_head } => { + crate::families::transactions::try_leave_family(deps, info, family_head) + } + ExecuteMsg::KickFamilyMember { member } => { + crate::families::transactions::try_head_kick_member(deps, info, member) } ExecuteMsg::CreateFamilyOnBehalf { owner_address, - owner_signature, label, } => crate::families::transactions::try_create_family_on_behalf( deps, info, owner_address, - owner_signature, - &label, + label, ), ExecuteMsg::JoinFamilyOnBehalf { member_address, - node_identity_signature, - family_signature, + join_permit, family_head, } => crate::families::transactions::try_join_family_on_behalf( deps, info, member_address, - Some(node_identity_signature), - family_signature, + join_permit, family_head, ), ExecuteMsg::LeaveFamilyOnBehalf { member_address, - node_identity_signature, family_head, } => crate::families::transactions::try_leave_family_on_behalf( deps, info, member_address, - node_identity_signature, family_head, ), ExecuteMsg::KickFamilyMemberOnBehalf { head_address, - signature, member, } => crate::families::transactions::try_head_kick_member_on_behalf( deps, info, head_address, - signature, - &member, + member, ), // state/sys-params-related ExecuteMsg::UpdateRewardingValidatorAddress { address } => { @@ -392,13 +380,13 @@ pub fn query( &crate::families::queries::get_family_by_head(&head, deps.storage)?, ), QueryMsg::GetFamilyByLabel { label } => to_binary( - &crate::families::queries::get_family_by_label(&label, deps.storage)?, + &crate::families::queries::get_family_by_label(label, deps.storage)?, ), QueryMsg::GetFamilyMembersByHead { head } => to_binary( &crate::families::queries::get_family_members_by_head(&head, deps.storage)?, ), QueryMsg::GetFamilyMembersByLabel { label } => to_binary( - &crate::families::queries::get_family_members_by_label(&label, deps.storage)?, + &crate::families::queries::get_family_members_by_label(label, deps.storage)?, ), QueryMsg::GetContractVersion {} => { to_binary(&crate::mixnet_contract_settings::queries::query_contract_version()) diff --git a/contracts/mixnet/src/families/mod.rs b/contracts/mixnet/src/families/mod.rs index f355b587a7..f3c80cce37 100644 --- a/contracts/mixnet/src/families/mod.rs +++ b/contracts/mixnet/src/families/mod.rs @@ -1,3 +1,7 @@ +// Copyright 2022-2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + pub mod queries; +pub mod signature_helpers; pub mod storage; pub mod transactions; diff --git a/contracts/mixnet/src/families/queries.rs b/contracts/mixnet/src/families/queries.rs index 238c38aaa7..dc4799f0b9 100644 --- a/contracts/mixnet/src/families/queries.rs +++ b/contracts/mixnet/src/families/queries.rs @@ -1,31 +1,27 @@ -use std::collections::HashSet; - -use crate::constants::{FAMILIES_DEFAULT_RETRIEVAL_LIMIT, FAMILIES_MAX_RETRIEVAL_LIMIT}; +// Copyright 2022-2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 use super::storage::{families, get_family, get_members, MEMBERS}; +use crate::constants::{FAMILIES_DEFAULT_RETRIEVAL_LIMIT, FAMILIES_MAX_RETRIEVAL_LIMIT}; use cosmwasm_std::{Order, Storage}; use cw_storage_plus::Bound; use mixnet_contract_common::families::{Family, FamilyHead}; use mixnet_contract_common::{error::MixnetContractError, IdentityKeyRef}; use mixnet_contract_common::{IdentityKey, PagedFamiliesResponse, PagedMembersResponse}; +use std::collections::HashSet; pub fn get_family_by_label( - label: &str, + label: String, storage: &dyn Storage, ) -> Result, MixnetContractError> { - Ok(families() - .idx - .label - .item(storage, label.to_string())? - .map(|o| o.1)) + Ok(families().idx.label.item(storage, label)?.map(|o| o.1)) } pub fn get_family_by_head( head: IdentityKeyRef<'_>, storage: &dyn Storage, -) -> Result { - let family_head = FamilyHead::new(head); - get_family(&family_head, storage) +) -> Result, MixnetContractError> { + Ok(families().may_load(storage, head.to_string())?) } pub fn get_family_members_by_head( @@ -38,15 +34,10 @@ pub fn get_family_members_by_head( } pub fn get_family_members_by_label( - label: &str, + label: String, storage: &dyn Storage, ) -> Result>, MixnetContractError> { - if let Some(family) = families() - .idx - .label - .item(storage, label.to_string())? - .map(|o| o.1) - { + if let Some(family) = families().idx.label.item(storage, label)?.map(|o| o.1) { Ok(Some(get_members(&family, storage)?)) } else { Ok(None) diff --git a/contracts/mixnet/src/families/signature_helpers.rs b/contracts/mixnet/src/families/signature_helpers.rs new file mode 100644 index 0000000000..186bbc19bc --- /dev/null +++ b/contracts/mixnet/src/families/signature_helpers.rs @@ -0,0 +1,38 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::mixnodes::storage as mixnodes_storage; +use crate::signing::storage as signing_storage; +use crate::support::helpers::decode_ed25519_identity_key; +use cosmwasm_std::{Addr, Deps}; +use mixnet_contract_common::error::MixnetContractError; +use mixnet_contract_common::families::FamilyHead; +use mixnet_contract_common::{construct_family_join_permit, IdentityKeyRef}; +use nym_contracts_common::signing::{MessageSignature, Verifier}; + +pub(crate) fn verify_family_join_permit( + deps: Deps<'_>, + granter: FamilyHead, + proxy: Option, + member: IdentityKeyRef, + signature: MessageSignature, +) -> Result<(), MixnetContractError> { + // recover the public key + let public_key = decode_ed25519_identity_key(granter.identity())?; + + // that's kinda a backwards way of getting the granter's nonce, but it works, so ¯\_(ツ)_/¯ + let Some(head_mixnode) = mixnodes_storage::mixnode_bonds() + .idx + .identity_key + .item(deps.storage, granter.identity().to_owned())?.map(|record| record.1) else { + return Err(MixnetContractError::FamilyDoesNotExist { head: granter.identity().to_string() }) + }; + let nonce = signing_storage::get_signing_nonce(deps.storage, head_mixnode.owner)?; + let msg = construct_family_join_permit(nonce, granter, proxy, member.to_owned()); + + if deps.api.verify_message(msg, signature, &public_key)? { + Ok(()) + } else { + Err(MixnetContractError::InvalidEd25519Signature) + } +} diff --git a/contracts/mixnet/src/families/storage.rs b/contracts/mixnet/src/families/storage.rs index a60df7a87d..1bdd421206 100644 --- a/contracts/mixnet/src/families/storage.rs +++ b/contracts/mixnet/src/families/storage.rs @@ -1,9 +1,11 @@ -use std::collections::HashSet; +// Copyright 2022-2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 -use cosmwasm_std::{Order, StdError, Storage}; +use cosmwasm_std::{Order, Storage}; use cw_storage_plus::{Index, IndexList, IndexedMap, Map, UniqueIndex}; use mixnet_contract_common::families::{Family, FamilyHead}; use mixnet_contract_common::{error::MixnetContractError, IdentityKey, IdentityKeyRef}; +use std::collections::HashSet; use crate::constants::{FAMILIES_INDEX_NAMESPACE, FAMILIES_MAP_NAMESPACE, MEMBERS_MAP_NAMESPACE}; @@ -51,22 +53,8 @@ pub fn get_family(head: &FamilyHead, store: &dyn Storage) -> Result Result<(), MixnetContractError> { - match families().save(store, f.head_identity().to_string(), f) { - Ok(()) => Ok(()), - Err(e) => match &e { - StdError::GenericErr { msg } => { - if msg.starts_with("Violates unique constraint") { - Err(MixnetContractError::FamilyWithLabelExists( - f.label().to_string(), - )) - } else { - Err(e.into()) - } - } - _ => Err(e.into()), - }, - } +pub fn save_family(f: &Family, store: &mut dyn Storage) -> Result<(), MixnetContractError> { + Ok(families().save(store, f.head_identity().to_string(), f)?) } pub fn add_family_member( diff --git a/contracts/mixnet/src/families/transactions.rs b/contracts/mixnet/src/families/transactions.rs index c1334fdb2a..b19da4e9c7 100644 --- a/contracts/mixnet/src/families/transactions.rs +++ b/contracts/mixnet/src/families/transactions.rs @@ -1,136 +1,99 @@ -use crate::support::helpers::{ - ensure_bonded, ensure_sent_by_vesting_contract, validate_family_signature, - validate_node_identity_signature, -}; - -use cosmwasm_std::{Addr, DepsMut, MessageInfo, Response}; -use mixnet_contract_common::families::{Family, FamilyHead}; -use mixnet_contract_common::{error::MixnetContractError, IdentityKey, IdentityKeyRef}; +// Copyright 2022-2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 use super::storage::{ - add_family_member, create_family, get_family, is_any_member, is_family_member, - remove_family_member, + add_family_member, get_family, is_any_member, is_family_member, remove_family_member, + save_family, }; +use crate::families::queries::get_family_by_label; +use crate::families::signature_helpers::verify_family_join_permit; +use crate::support::helpers::{ensure_bonded, ensure_sent_by_vesting_contract}; +use cosmwasm_std::{Addr, DepsMut, MessageInfo, Response}; +use mixnet_contract_common::families::{Family, FamilyHead}; +use mixnet_contract_common::{error::MixnetContractError, IdentityKey}; +use nym_contracts_common::signing::MessageSignature; /// Creates a new MixNode family with senders node as head pub fn try_create_family( deps: DepsMut, info: MessageInfo, - owner_signature: String, - label: &str, + label: String, ) -> Result { - _try_create_family(deps, &info.sender, owner_signature, label, None) + _try_create_family(deps, &info.sender, label, None) } pub fn try_create_family_on_behalf( deps: DepsMut, info: MessageInfo, owner_address: String, - owner_signature: String, - label: &str, + label: String, ) -> Result { ensure_sent_by_vesting_contract(&info, deps.storage)?; let owner_address = deps.api.addr_validate(&owner_address)?; - _try_create_family( - deps, - &owner_address, - owner_signature, - label, - Some(info.sender), - ) + _try_create_family(deps, &owner_address, label, Some(info.sender)) } fn _try_create_family( deps: DepsMut, owner: &Addr, - owner_signature: String, - label: &str, + label: String, proxy: Option, ) -> Result { - let existing_bond = crate::mixnodes::storage::mixnode_bonds() - .idx - .owner - .item(deps.storage, owner.clone())? - .ok_or(MixnetContractError::NoAssociatedMixNodeBond { - owner: owner.clone(), - })? - .1; + let existing_bond = + crate::mixnodes::helpers::must_get_mixnode_bond_by_owner(deps.storage, owner)?; ensure_bonded(&existing_bond)?; - validate_node_identity_signature( - deps.as_ref(), - owner, - &owner_signature, - existing_bond.identity(), - )?; - let family_head = FamilyHead::new(existing_bond.identity()); - if let Ok(_family) = get_family(&family_head, deps.storage) { + // can't overwrite existing family + if get_family(&family_head, deps.storage).is_ok() { return Err(MixnetContractError::FamilyCanHaveOnlyOne); } + // the label must be unique + if get_family_by_label(label.clone(), deps.storage)?.is_some() { + return Err(MixnetContractError::FamilyWithLabelExists(label)); + } + let family = Family::new(family_head, proxy, label); - create_family(&family, deps.storage)?; + save_family(&family, deps.storage)?; Ok(Response::default()) } pub fn try_join_family( deps: DepsMut, info: MessageInfo, - // Required for proxy joining - node_identity_signature: Option, - family_signature: String, - family_head: IdentityKey, + join_permit: MessageSignature, + family_head: FamilyHead, ) -> Result { - let family_head = FamilyHead::new(&family_head); - _try_join_family( - deps, - &info.sender, - node_identity_signature, - family_signature, - family_head, - ) + _try_join_family(deps, &info.sender, join_permit, family_head, None) } pub fn try_join_family_on_behalf( deps: DepsMut, info: MessageInfo, member_address: String, - node_identity_signature: Option, - family_signature: String, - family_head: IdentityKey, + join_permit: MessageSignature, + family_head: FamilyHead, ) -> Result { ensure_sent_by_vesting_contract(&info, deps.storage)?; let member_address = deps.api.addr_validate(&member_address)?; - let family_head = FamilyHead::new(&family_head); - _try_join_family( - deps, - &member_address, - node_identity_signature, - family_signature, - family_head, - ) + let proxy = Some(info.sender); + _try_join_family(deps, &member_address, join_permit, family_head, proxy) } fn _try_join_family( deps: DepsMut, owner: &Addr, - node_identity_signature: Option, - family_signature: String, + join_permit: MessageSignature, family_head: FamilyHead, + proxy: Option, ) -> Result { - let existing_bond = crate::mixnodes::storage::mixnode_bonds() - .idx - .owner - .item(deps.storage, owner.clone())? - .ok_or(MixnetContractError::NoAssociatedMixNodeBond { - owner: owner.clone(), - })? - .1; + let existing_bond = + crate::mixnodes::helpers::must_get_mixnode_bond_by_owner(deps.storage, owner)?; ensure_bonded(&existing_bond)?; @@ -147,20 +110,12 @@ fn _try_join_family( )); } - if let Some(node_identity_signature) = node_identity_signature { - validate_node_identity_signature( - deps.as_ref(), - owner, - &node_identity_signature, - existing_bond.identity(), - )?; - } - - validate_family_signature( + verify_family_join_permit( deps.as_ref(), + family_head.clone(), + proxy, existing_bond.identity(), - &family_signature, - family_head.identity(), + join_permit, )?; let family = get_family(&family_head, deps.storage)?; @@ -173,41 +128,30 @@ fn _try_join_family( pub fn try_leave_family( deps: DepsMut, info: MessageInfo, - signature: String, - family_head: IdentityKey, + family_head: FamilyHead, ) -> Result { - let family_head = FamilyHead::new(&family_head); - _try_leave_family(deps, &info.sender, signature, family_head) + _try_leave_family(deps, &info.sender, family_head) } pub fn try_leave_family_on_behalf( deps: DepsMut, info: MessageInfo, member_address: String, - node_family_signature: String, - family_head: IdentityKey, + family_head: FamilyHead, ) -> Result { ensure_sent_by_vesting_contract(&info, deps.storage)?; - let family_head = FamilyHead::new(&family_head); let member_address = deps.api.addr_validate(&member_address)?; - _try_leave_family(deps, &member_address, node_family_signature, family_head) + _try_leave_family(deps, &member_address, family_head) } fn _try_leave_family( deps: DepsMut, owner: &Addr, - node_family_signature: String, family_head: FamilyHead, ) -> Result { - let existing_bond = crate::mixnodes::storage::mixnode_bonds() - .idx - .owner - .item(deps.storage, owner.clone())? - .ok_or(MixnetContractError::NoAssociatedMixNodeBond { - owner: owner.clone(), - })? - .1; + let existing_bond = + crate::mixnodes::helpers::must_get_mixnode_bond_by_owner(deps.storage, owner)?; ensure_bonded(&existing_bond)?; @@ -226,13 +170,6 @@ fn _try_leave_family( }); } - validate_node_identity_signature( - deps.as_ref(), - owner, - &node_family_signature, - existing_bond.identity(), - )?; - remove_family_member(deps.storage, existing_bond.identity()); Ok(Response::default()) @@ -241,62 +178,56 @@ fn _try_leave_family( pub fn try_head_kick_member( deps: DepsMut, info: MessageInfo, - owner_signature: String, - member: IdentityKeyRef, + member: IdentityKey, ) -> Result { - _try_head_kick_member(deps, &info.sender, owner_signature, member) + _try_head_kick_member(deps, &info.sender, member) } pub fn try_head_kick_member_on_behalf( deps: DepsMut, info: MessageInfo, head_address: String, - owner_signature: String, - member: IdentityKeyRef, + member: IdentityKey, ) -> Result { ensure_sent_by_vesting_contract(&info, deps.storage)?; let head_address = deps.api.addr_validate(&head_address)?; - _try_head_kick_member(deps, &head_address, owner_signature, member) + _try_head_kick_member(deps, &head_address, member) } -#[allow(unused_variables)] fn _try_head_kick_member( deps: DepsMut, owner: &Addr, - owner_signature: String, - member: IdentityKeyRef<'_>, + member: IdentityKey, ) -> Result { - Err(MixnetContractError::NotImplemented) - // let existing_bond = crate::mixnodes::storage::mixnode_bonds() - // .idx - // .owner - // .item(deps.storage, owner.clone())? - // .ok_or(MixnetContractError::NoAssociatedMixNodeBond { - // owner: owner.clone(), - // })? - // .1; + let head_bond = crate::mixnodes::helpers::must_get_mixnode_bond_by_owner(deps.storage, owner)?; - // ensure_bonded(&existing_bond)?; + // make sure we're still in the mixnet + ensure_bonded(&head_bond)?; - // validate_node_identity_signature( - // deps.as_ref(), - // owner, - // &owner_signature, - // existing_bond.identity(), - // )?; + // make sure we're not trying to kick ourselves... + if member == head_bond.identity() { + return Err(MixnetContractError::CantLeaveOwnFamily { + head: head_bond.identity().to_string(), + member, + }); + } - // let family_head = FamilyHead::new(existing_bond.identity()); - // let family = get_family(&family_head, deps.storage)?; - // if !is_family_member(deps.storage, &family, member)? { - // return Err(MixnetContractError::NotAMember { - // head: family_head.identity().to_string(), - // member: existing_bond.identity().to_string(), - // }); - // } + // get the family details + let family_head = FamilyHead::new(head_bond.identity()); + let family = get_family(&family_head, deps.storage)?; - // remove_family_member(deps.storage, member); - // Ok(Response::default()) + // make sure the member we're trying to kick is an actual member + if !is_family_member(deps.storage, &family, &member)? { + return Err(MixnetContractError::NotAMember { + head: family_head.identity().to_string(), + member, + }); + } + + // finally get rid of the member + remove_family_member(deps.storage, &member); + Ok(Response::default()) } #[cfg(test)] @@ -322,9 +253,9 @@ mod test { let cost_params = fixtures::mix_node_cost_params_fixture(); let (head_mixnode, head_bond_sig, head_keypair) = test.mixnode_with_signature(head, None); - let (malicious_mixnode, malicious_bond_sig, malicious_keypair) = + let (malicious_mixnode, malicious_bond_sig, _malicious_keypair) = test.mixnode_with_signature(malicious_head, None); - let (member_mixnode, member_bond_sig, member_keypair) = + let (member_mixnode, member_bond_sig, _member_keypair) = test.mixnode_with_signature(member, None); crate::mixnodes::transactions::try_add_mixnode( @@ -357,26 +288,14 @@ mod test { ) .unwrap(); - let old_style_head_sig = head_keypair.private_key().sign_text(head); - let old_style_malicious_head_sig = - malicious_keypair.private_key().sign_text(malicious_head); - let old_style_member_sig = member_keypair.private_key().sign_text(member); - - try_create_family( - test.deps_mut(), - mock_info(head, &[]), - old_style_head_sig, - "test", - ) - .unwrap(); + try_create_family(test.deps_mut(), mock_info(head, &[]), "test".to_string()).unwrap(); let family_head = FamilyHead::new(&head_mixnode.identity_key); assert!(get_family(&family_head, test.deps().storage).is_ok()); let nope = try_create_family( test.deps_mut(), mock_info(malicious_head, &[]), - old_style_malicious_head_sig, - "test", + "test".to_string(), ); match nope { @@ -387,24 +306,23 @@ mod test { }, } - let family = get_family_by_label("test", test.deps().storage).unwrap(); + let family = get_family_by_label("test".to_string(), test.deps().storage).unwrap(); assert!(family.is_some()); assert_eq!(family.unwrap().head_identity(), family_head.identity()); - let family = get_family_by_head(family_head.identity(), test.deps().storage).unwrap(); + let family = get_family_by_head(family_head.identity(), test.deps().storage) + .unwrap() + .unwrap(); assert_eq!(family.head_identity(), family_head.identity()); - let join_signature = head_keypair - .private_key() - .sign(member_mixnode.identity_key.as_bytes()) - .to_base58_string(); + let join_permit = + test.generate_family_join_permit(&head_keypair, &member_mixnode.identity_key, false); try_join_family( test.deps_mut(), mock_info(member, &[]), - Some(old_style_member_sig.clone()), - join_signature.clone(), - head_mixnode.identity_key.clone(), + join_permit, + family_head.clone(), ) .unwrap(); @@ -414,11 +332,34 @@ mod test { is_family_member(test.deps().storage, &family, &member_mixnode.identity_key).unwrap() ); - try_leave_family( + try_leave_family(test.deps_mut(), mock_info(member, &[]), family_head.clone()).unwrap(); + + let family = get_family(&family_head, test.deps().storage).unwrap(); + assert!( + !is_family_member(test.deps().storage, &family, &member_mixnode.identity_key).unwrap() + ); + + let new_join_permit = + test.generate_family_join_permit(&head_keypair, &member_mixnode.identity_key, false); + + try_join_family( test.deps_mut(), mock_info(member, &[]), - old_style_member_sig.clone(), - head_mixnode.identity_key.clone(), + new_join_permit, + family_head.clone(), + ) + .unwrap(); + + let family = get_family(&family_head, test.deps().storage).unwrap(); + + assert!( + is_family_member(test.deps().storage, &family, &member_mixnode.identity_key).unwrap() + ); + + try_head_kick_member( + test.deps_mut(), + mock_info(head, &[]), + member_mixnode.identity_key.clone(), ) .unwrap(); @@ -426,32 +367,6 @@ mod test { assert!( !is_family_member(test.deps().storage, &family, &member_mixnode.identity_key).unwrap() ); - - try_join_family( - test.deps_mut(), - mock_info(member, &[]), - Some(old_style_member_sig), - join_signature, - head_mixnode.identity_key, - ) - .unwrap(); - - let family = get_family(&family_head, test.deps().storage).unwrap(); - - assert!( - is_family_member(test.deps().storage, &family, &member_mixnode.identity_key).unwrap() - ); - - // try_head_kick_member( - // deps.as_mut(), - // mock_info(&head, &[]), - // head_sig.clone(), - // &member_mixnode.identity_key.clone(), - // ) - // .unwrap(); - - // let family = get_family(&family_head, test.deps().storage).unwrap(); - // assert!(!is_family_member(test.deps().storage, &family, &member_mixnode.identity_key).unwrap()); } #[cfg(test)] @@ -468,15 +383,13 @@ mod test { let head = "alice"; - let (_, keypair) = test.add_dummy_mixnode_with_proxy_and_keypair(head, None); - let sig = keypair.private_key().sign_text(head); + test.add_dummy_mixnode(head, None); let res = try_create_family_on_behalf( test.deps_mut(), mock_info(illegal_proxy.as_ref(), &[]), head.to_string(), - sig, - "label", + "label".to_string(), ) .unwrap_err(); @@ -507,23 +420,22 @@ mod test { let new_member = "vin-diesel"; let (_, head_keys) = test.create_dummy_mixnode_with_new_family(head, label); - let (_, member_keys) = test.add_dummy_mixnode_with_proxy_and_keypair(new_member, None); + let (_, member_keys) = test.add_dummy_mixnode_with_keypair(new_member, None); - // TODO: those signatures are WRONG and have to be c hanged - let join_signature = head_keys - .private_key() - .sign_text(&member_keys.public_key().to_base58_string()); - - let member_sig = member_keys.private_key().sign_text(new_member); + let join_permit = test.generate_family_join_permit( + &head_keys, + &member_keys.public_key().to_base58_string(), + false, + ); let head_identity = head_keys.public_key().to_base58_string(); + let family_head = FamilyHead::new(&head_identity); let res = try_join_family_on_behalf( test.deps_mut(), mock_info(illegal_proxy.as_ref(), &[]), new_member.to_string(), - Some(member_sig), - join_signature, - head_identity, + join_permit, + family_head, ) .unwrap_err(); @@ -554,33 +466,30 @@ mod test { let new_member = "vin-diesel"; let (_, head_keys) = test.create_dummy_mixnode_with_new_family(head, label); - let (_, member_keys) = test.add_dummy_mixnode_with_proxy_and_keypair(new_member, None); + let (_, member_keys) = test.add_dummy_mixnode_with_keypair(new_member, None); - // TODO: those signatures are WRONG and have to be changed - let join_signature = head_keys - .private_key() - .sign_text(&member_keys.public_key().to_base58_string()); - - let member_sig = member_keys.private_key().sign_text(new_member); + let join_permit = test.generate_family_join_permit( + &head_keys, + &member_keys.public_key().to_base58_string(), + true, + ); let head_identity = head_keys.public_key().to_base58_string(); + let family_head = FamilyHead::new(&head_identity); try_join_family_on_behalf( test.deps_mut(), mock_info(vesting_contract.as_ref(), &[]), new_member.to_string(), - Some(member_sig.clone()), - join_signature, - head_identity, + join_permit, + family_head.clone(), ) .unwrap(); - let leave_signature = member_sig; let res = try_leave_family_on_behalf( test.deps_mut(), mock_info(illegal_proxy.as_ref(), &[]), new_member.to_string(), - leave_signature, - head_keys.public_key().to_base58_string(), + family_head.clone(), ) .unwrap_err(); @@ -611,33 +520,31 @@ mod test { let new_member = "vin-diesel"; let (_, head_keys) = test.create_dummy_mixnode_with_new_family(head, label); - let (_, member_keys) = test.add_dummy_mixnode_with_proxy_and_keypair(new_member, None); + let (_, member_keys) = test.add_dummy_mixnode_with_keypair(new_member, None); - // TODO: those signatures are WRONG and have to be c hanged - let join_signature = head_keys - .private_key() - .sign_text(&member_keys.public_key().to_base58_string()); - - let member_sig = member_keys.private_key().sign_text(new_member); + let join_permit = test.generate_family_join_permit( + &head_keys, + &member_keys.public_key().to_base58_string(), + true, + ); let head_identity = head_keys.public_key().to_base58_string(); + let family_head = FamilyHead::new(&head_identity); + try_join_family_on_behalf( test.deps_mut(), mock_info(vesting_contract.as_ref(), &[]), new_member.to_string(), - Some(member_sig), - join_signature, - head_identity, + join_permit, + family_head, ) .unwrap(); - // TODO: a completely wrong signature is being used let res = try_head_kick_member_on_behalf( test.deps_mut(), mock_info(illegal_proxy.as_ref(), &[]), head.to_string(), - head_keys.private_key().sign_text(head), - &member_keys.public_key().to_base58_string(), + member_keys.public_key().to_base58_string(), ) .unwrap_err(); diff --git a/contracts/mixnet/src/gateways/signature_helpers.rs b/contracts/mixnet/src/gateways/signature_helpers.rs index efdb231e9e..e6ce7f5ad5 100644 --- a/contracts/mixnet/src/gateways/signature_helpers.rs +++ b/contracts/mixnet/src/gateways/signature_helpers.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::signing::storage as signing_storage; +use crate::support::helpers::decode_ed25519_identity_key; use cosmwasm_std::{Addr, Coin, Deps}; use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::{construct_gateway_bonding_sign_payload, Gateway}; @@ -17,10 +18,7 @@ pub(crate) fn verify_gateway_bonding_signature( signature: MessageSignature, ) -> Result<(), MixnetContractError> { // recover the public key - let mut public_key = [0u8; 32]; - bs58::decode(&gateway.identity_key) - .into(&mut public_key) - .map_err(|err| MixnetContractError::MalformedEd25519IdentityKey(err.to_string()))?; + let public_key = decode_ed25519_identity_key(&gateway.identity_key)?; // reconstruct the payload let nonce = signing_storage::get_signing_nonce(deps.storage, sender.clone())?; diff --git a/contracts/mixnet/src/mixnodes/signature_helpers.rs b/contracts/mixnet/src/mixnodes/signature_helpers.rs index 24d87eb2e9..2ccec88d81 100644 --- a/contracts/mixnet/src/mixnodes/signature_helpers.rs +++ b/contracts/mixnet/src/mixnodes/signature_helpers.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::signing::storage as signing_storage; +use crate::support::helpers::decode_ed25519_identity_key; use cosmwasm_std::{Addr, Coin, Deps}; use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::{construct_mixnode_bonding_sign_payload, MixNode, MixNodeCostParams}; @@ -18,10 +19,7 @@ pub(crate) fn verify_mixnode_bonding_signature( signature: MessageSignature, ) -> Result<(), MixnetContractError> { // recover the public key - let mut public_key = [0u8; 32]; - bs58::decode(&mixnode.identity_key) - .into(&mut public_key) - .map_err(|err| MixnetContractError::MalformedEd25519IdentityKey(err.to_string()))?; + let public_key = decode_ed25519_identity_key(&mixnode.identity_key)?; // reconstruct the payload let nonce = signing_storage::get_signing_nonce(deps.storage, sender.clone())?; diff --git a/contracts/mixnet/src/mixnodes/transactions.rs b/contracts/mixnet/src/mixnodes/transactions.rs index f2bfa89941..378179a282 100644 --- a/contracts/mixnet/src/mixnodes/transactions.rs +++ b/contracts/mixnet/src/mixnodes/transactions.rs @@ -245,12 +245,7 @@ pub(crate) fn _try_remove_mixnode( owner: Addr, proxy: Option, ) -> Result { - let existing_bond = storage::mixnode_bonds() - .idx - .owner - .item(deps.storage, owner.clone())? - .ok_or(MixnetContractError::NoAssociatedMixNodeBond { owner })? - .1; + let existing_bond = must_get_mixnode_bond_by_owner(deps.storage, &owner)?; // unbonding is only allowed if the epoch is currently not in the process of being advanced ensure_epoch_in_progress_state(deps.storage)?; diff --git a/contracts/mixnet/src/support/helpers.rs b/contracts/mixnet/src/support/helpers.rs index e2d1c36ddd..c992e75145 100644 --- a/contracts/mixnet/src/support/helpers.rs +++ b/contracts/mixnet/src/support/helpers.rs @@ -4,9 +4,7 @@ use crate::gateways::storage as gateways_storage; use crate::mixnet_contract_settings::storage as mixnet_params_storage; use crate::mixnodes::storage as mixnodes_storage; -use cosmwasm_std::{ - wasm_execute, Addr, BankMsg, Coin, CosmosMsg, Deps, MessageInfo, Response, Storage, -}; +use cosmwasm_std::{wasm_execute, Addr, BankMsg, Coin, CosmosMsg, MessageInfo, Response, Storage}; use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::{EpochState, EpochStatus, IdentityKeyRef, MixId, MixNodeBond}; use vesting_contract_common::messages::ExecuteMsg as VestingContractExecuteMsg; @@ -371,184 +369,19 @@ pub(crate) fn ensure_no_existing_bond( Ok(()) } -pub fn validate_node_identity_signature( - deps: Deps<'_>, - owner: &Addr, - signature: &str, - identity: IdentityKeyRef<'_>, -) -> Result<(), MixnetContractError> { - validate_ed25519_signature(deps, owner.as_bytes(), signature, identity) -} - -pub fn validate_family_signature( - deps: Deps<'_>, - family_member: IdentityKeyRef<'_>, - signature: &str, - family_head: IdentityKeyRef<'_>, -) -> Result<(), MixnetContractError> { - validate_ed25519_signature(deps, family_member.as_bytes(), signature, family_head) -} - -pub(crate) fn validate_ed25519_signature( - deps: Deps<'_>, - signed_bytes: &[u8], - signature: &str, - identity: IdentityKeyRef<'_>, -) -> Result<(), MixnetContractError> { - let mut identity_bytes = [0u8; 32]; - let mut signature_bytes = [0u8; 64]; - - let identity_used_bytes = bs58::decode(identity) - .into(&mut identity_bytes) +pub(crate) fn decode_ed25519_identity_key( + encoded: IdentityKeyRef, +) -> Result<[u8; 32], MixnetContractError> { + let mut public_key = [0u8; 32]; + let used = bs58::decode(encoded) + .into(&mut public_key) .map_err(|err| MixnetContractError::MalformedEd25519IdentityKey(err.to_string()))?; - let signature_used_bytes = bs58::decode(signature) - .into(&mut signature_bytes) - .map_err(|err| MixnetContractError::MalformedEd25519Signature(err.to_string()))?; - if identity_used_bytes != 32 { + if used != 32 { return Err(MixnetContractError::MalformedEd25519IdentityKey( "Too few bytes provided for the public key".into(), )); } - if signature_used_bytes != 64 { - return Err(MixnetContractError::MalformedEd25519Signature( - "Too few bytes provided for the signature".into(), - )); - } - - let res = deps - .api - .ed25519_verify(signed_bytes, &signature_bytes, &identity_bytes) - .map_err(cosmwasm_std::StdError::verification_err)?; - if !res { - Err(MixnetContractError::InvalidEd25519Signature) - } else { - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use cosmwasm_std::testing::mock_dependencies; - use nym_crypto::asymmetric::identity; - use rand_chacha::rand_core::SeedableRng; - - #[test] - fn validating_node_signature() { - let deps = mock_dependencies(); - - // since those tests are NOT compiled to wasm, we can use rng-related dependency - let dummy_seed = [1u8; 32]; - let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); - - let short_bs58 = "2SfEgZ4aQUr3HSwqE"; - let long_bs58 = "g34PyULki9fc3FqKobj5wdVNCaWAt1M9oZowyyMFfWSCejxg7wt574piZVjqjFEN2UXsgZ56KTkKf3jnWD4DJ2Gsf7KXQAvptFfcYRrZHTjMVo3NXcBSNm3wDBKZWZURzp4Fixv"; - - let address1 = Addr::unchecked("some-dummy-address1"); - let address2 = Addr::unchecked("some-dummy-address2"); - - let keypair1 = identity::KeyPair::new(&mut rng); - let keypair2 = identity::KeyPair::new(&mut rng); - - let sig_addr1_key1 = keypair1 - .private_key() - .sign(address1.as_bytes()) - .to_base58_string(); - let sig_addr2_key1 = keypair1 - .private_key() - .sign(address2.as_bytes()) - .to_base58_string(); - let sig_addr1_key2 = keypair2 - .private_key() - .sign(address1.as_bytes()) - .to_base58_string(); - - assert_eq!( - Err(MixnetContractError::MalformedEd25519IdentityKey( - "buffer provided to decode base58 encoded string into was too small".into() - )), - validate_node_identity_signature(deps.as_ref(), &address1, &sig_addr1_key1, long_bs58,) - ); - - assert_eq!( - Err(MixnetContractError::MalformedEd25519Signature( - "buffer provided to decode base58 encoded string into was too small".into() - )), - validate_node_identity_signature( - deps.as_ref(), - &address1, - long_bs58.into(), - &keypair1.public_key().to_base58_string(), - ) - ); - - assert_eq!( - Err(MixnetContractError::MalformedEd25519IdentityKey( - "Too few bytes provided for the public key".into() - )), - validate_node_identity_signature(deps.as_ref(), &address1, &sig_addr1_key1, short_bs58,) - ); - - assert_eq!( - Err(MixnetContractError::MalformedEd25519Signature( - "Too few bytes provided for the signature".into() - )), - validate_node_identity_signature( - deps.as_ref(), - &address1, - short_bs58.into(), - &keypair1.public_key().to_base58_string(), - ) - ); - - assert_eq!( - Err(MixnetContractError::InvalidEd25519Signature), - validate_node_identity_signature( - deps.as_ref(), - &address1, - &sig_addr1_key1, - &keypair2.public_key().to_base58_string(), - ) - ); - - assert_eq!( - Err(MixnetContractError::InvalidEd25519Signature), - validate_node_identity_signature( - deps.as_ref(), - &address1, - &sig_addr2_key1, - &keypair1.public_key().to_base58_string(), - ) - ); - - assert_eq!( - Err(MixnetContractError::InvalidEd25519Signature), - validate_node_identity_signature( - deps.as_ref(), - &address2, - &sig_addr1_key1, - &keypair1.public_key().to_base58_string(), - ) - ); - - assert_eq!( - Err(MixnetContractError::InvalidEd25519Signature), - validate_node_identity_signature( - deps.as_ref(), - &address1, - &sig_addr1_key2, - &keypair1.public_key().to_base58_string(), - ) - ); - - assert!(validate_node_identity_signature( - deps.as_ref(), - &address1, - &sig_addr1_key1, - &keypair1.public_key().to_base58_string(), - ) - .is_ok()); - } + Ok(public_key) } diff --git a/contracts/mixnet/src/support/tests/mod.rs b/contracts/mixnet/src/support/tests/mod.rs index e693b6b1f4..1d66286e32 100644 --- a/contracts/mixnet/src/support/tests/mod.rs +++ b/contracts/mixnet/src/support/tests/mod.rs @@ -54,6 +54,7 @@ pub mod test_helpers { use mixnet_contract_common::events::{ may_find_attribute, MixnetEventType, DELEGATES_REWARD_KEY, OPERATOR_REWARD_KEY, }; + use mixnet_contract_common::families::FamilyHead; use mixnet_contract_common::mixnode::{MixNodeRewarding, UnbondedMixnode}; use mixnet_contract_common::pending_events::{PendingEpochEventData, PendingIntervalEventData}; use mixnet_contract_common::reward_params::{Performance, RewardingParams}; @@ -61,10 +62,10 @@ pub mod test_helpers { use mixnet_contract_common::rewarding::simulator::Simulator; use mixnet_contract_common::rewarding::RewardDistribution; use mixnet_contract_common::{ - Delegation, EpochState, EpochStatus, Gateway, GatewayBondingPayload, IdentityKey, - InitialRewardingParams, InstantiateMsg, Interval, MixId, MixNode, MixNodeBond, - MixnodeBondingPayload, Percent, RewardedSetNodeStatus, SignableGatewayBondingMsg, - SignableMixNodeBondingMsg, + construct_family_join_permit, Delegation, EpochState, EpochStatus, Gateway, + GatewayBondingPayload, IdentityKey, IdentityKeyRef, InitialRewardingParams, InstantiateMsg, + Interval, MixId, MixNode, MixNodeBond, MixnodeBondingPayload, Percent, + RewardedSetNodeStatus, SignableGatewayBondingMsg, SignableMixNodeBondingMsg, }; use nym_contracts_common::signing::{ ContractMessageContent, MessageSignature, SignableMessage, SigningAlgorithm, SigningPurpose, @@ -169,26 +170,64 @@ pub mod test_helpers { .collect::>() } + pub fn generate_family_join_permit( + &mut self, + family_owner_keys: &identity::KeyPair, + member_node: IdentityKeyRef, + vesting: bool, + ) -> MessageSignature { + let identity = family_owner_keys.public_key().to_base58_string(); + + let head_mixnode = mixnodes_storage::mixnode_bonds() + .idx + .identity_key + .item(self.deps().storage, identity.clone()) + .unwrap() + .map(|record| record.1) + .unwrap(); + + let family_head = FamilyHead::new(&identity); + let owner = head_mixnode.owner; + + let nonce = + signing_storage::get_signing_nonce(self.deps().storage, owner.clone()).unwrap(); + + let proxy = if vesting { + Some(self.vesting_contract()) + } else { + None + }; + + let msg = + construct_family_join_permit(nonce, family_head, proxy, member_node.to_owned()); + + let sig_bytes = family_owner_keys + .private_key() + .sign(&msg.to_plaintext().unwrap()) + .to_bytes(); + MessageSignature::from(sig_bytes.as_ref()) + } + #[allow(unused)] pub fn join_family( &mut self, member: &str, member_keys: &identity::KeyPair, head_keys: &identity::KeyPair, + vesting: bool, ) { - let identity_signature = member_keys.private_key().sign_text(member); - let join_signature = head_keys - .private_key() - .sign(&member_keys.public_key().to_bytes()) - .to_base58_string(); + let member_identity = member_keys.public_key().to_base58_string(); let head_identity = head_keys.public_key().to_base58_string(); + let join_permit = + self.generate_family_join_permit(head_keys, &member_identity, vesting); + let family_head = FamilyHead::new(&head_identity); + try_join_family( self.deps_mut(), mock_info(member, &[]), - Some(identity_signature), - join_signature, - head_identity, + join_permit, + family_head, ) .unwrap(); } @@ -199,9 +238,8 @@ pub mod test_helpers { label: &str, ) -> (MixId, identity::KeyPair) { let (mix_id, keys) = self.add_dummy_mixnode_with_proxy_and_keypair(head, None); - let sig = keys.private_key().sign_text(head); - try_create_family(self.deps_mut(), mock_info(head, &[]), sig, label).unwrap(); + try_create_family(self.deps_mut(), mock_info(head, &[]), label.to_string()).unwrap(); (mix_id, keys) } @@ -290,6 +328,53 @@ pub mod test_helpers { ed25519_sign_message(msg, key) } + pub fn add_dummy_mixnode_with_keypair( + &mut self, + owner: &str, + stake: Option, + ) -> (MixId, identity::KeyPair) { + let stake = self.make_mix_pledge(stake); + + let keypair = identity::KeyPair::new(&mut self.rng); + let identity_key = keypair.public_key().to_base58_string(); + let legit_sphinx_keys = nym_crypto::asymmetric::encryption::KeyPair::new(&mut self.rng); + + let mixnode = MixNode { + identity_key, + sphinx_key: legit_sphinx_keys.public_key().to_base58_string(), + ..tests::fixtures::mix_node_fixture() + }; + + let msg = mixnode_bonding_sign_payload( + self.deps(), + owner, + None, + mixnode.clone(), + stake.clone(), + ); + let owner_signature = ed25519_sign_message(msg, keypair.private_key()); + + let info = mock_info(owner, &stake); + let current_id_counter = mixnodes_storage::MIXNODE_ID_COUNTER + .may_load(self.deps().storage) + .unwrap() + .unwrap_or_default(); + + let env = self.env(); + try_add_mixnode( + self.deps_mut(), + env, + info, + mixnode, + tests::fixtures::mix_node_cost_params_fixture(), + owner_signature, + ) + .unwrap(); + + // newly added mixnode gets assigned the current counter + 1 + (current_id_counter + 1, keypair) + } + pub fn add_dummy_mixnode_with_proxy_and_keypair( &mut self, owner: &str, diff --git a/contracts/vesting/src/contract.rs b/contracts/vesting/src/contract.rs index bfac829ebd..16192fcb5e 100644 --- a/contracts/vesting/src/contract.rs +++ b/contracts/vesting/src/contract.rs @@ -14,6 +14,7 @@ use cosmwasm_std::{ QueryResponse, Response, StdResult, Timestamp, Uint128, }; use cw_storage_plus::Bound; +use mixnet_contract_common::families::FamilyHead; use mixnet_contract_common::gateway::GatewayConfigUpdate; use mixnet_contract_common::mixnode::{MixNodeConfigUpdate, MixNodeCostParams}; use mixnet_contract_common::{Gateway, MixId, MixNode}; @@ -111,28 +112,13 @@ pub fn execute( msg: ExecuteMsg, ) -> Result { match msg { - ExecuteMsg::CreateFamily { - owner_signature, - label, - } => try_create_family(info, deps, owner_signature, label), + ExecuteMsg::CreateFamily { label } => try_create_family(info, deps, label), ExecuteMsg::JoinFamily { - node_identity_signature, - family_signature, + join_permit, family_head, - } => try_join_family( - info, - deps, - node_identity_signature, - family_signature, - family_head, - ), - ExecuteMsg::LeaveFamily { - node_identity_signature, - family_head, - } => try_leave_family(info, deps, node_identity_signature, family_head), - ExecuteMsg::KickFamilyMember { signature, member } => { - try_kick_family_member(info, deps, signature, member) - } + } => try_join_family(info, deps, join_permit, family_head), + ExecuteMsg::LeaveFamily { family_head } => try_leave_family(info, deps, family_head), + ExecuteMsg::KickFamilyMember { member } => try_kick_family_member(info, deps, member), ExecuteMsg::UpdateLockedPledgeCap { address, cap } => { try_update_locked_pledge_cap(address, cap, info, deps) } @@ -226,44 +212,35 @@ pub fn execute( pub fn try_create_family( info: MessageInfo, deps: DepsMut, - owner_signature: String, label: String, ) -> Result { let account = account_from_address(info.sender.as_ref(), deps.storage, deps.api)?; - account.try_create_family(deps.storage, owner_signature, label) + account.try_create_family(deps.storage, label) } pub fn try_join_family( info: MessageInfo, deps: DepsMut, - node_identity_signature: String, - family_signature: String, - family_head: String, + join_permit: MessageSignature, + family_head: FamilyHead, ) -> Result { let account = account_from_address(info.sender.as_ref(), deps.storage, deps.api)?; - account.try_join_family( - deps.storage, - node_identity_signature, - family_signature, - &family_head, - ) + account.try_join_family(deps.storage, join_permit, family_head) } pub fn try_leave_family( info: MessageInfo, deps: DepsMut, - node_identity_signature: String, - family_head: String, + family_head: FamilyHead, ) -> Result { let account = account_from_address(info.sender.as_ref(), deps.storage, deps.api)?; - account.try_leave_family(deps.storage, node_identity_signature, &family_head) + account.try_leave_family(deps.storage, family_head) } pub fn try_kick_family_member( info: MessageInfo, deps: DepsMut, - signature: String, member: String, ) -> Result { let account = account_from_address(info.sender.as_ref(), deps.storage, deps.api)?; - account.try_head_kick_member(deps.storage, signature, &member) + account.try_head_kick_member(deps.storage, &member) } /// Update locked_pledge_cap, the hard cap for staking/bonding with unvested tokens. diff --git a/contracts/vesting/src/traits/node_families.rs b/contracts/vesting/src/traits/node_families.rs index 3e2e6e9863..163308d30a 100644 --- a/contracts/vesting/src/traits/node_families.rs +++ b/contracts/vesting/src/traits/node_families.rs @@ -1,34 +1,32 @@ use crate::errors::ContractError; +use contracts_common::signing::MessageSignature; use cosmwasm_std::{Response, Storage}; +use mixnet_contract_common::families::FamilyHead; use mixnet_contract_common::IdentityKeyRef; pub trait NodeFamilies { fn try_create_family( &self, storage: &dyn Storage, - owner_signature: String, label: String, ) -> Result; fn try_join_family( &self, storage: &dyn Storage, - node_identity_signature: String, - family_signature: String, - family_head: IdentityKeyRef, + join_permit: MessageSignature, + family_head: FamilyHead, ) -> Result; fn try_leave_family( &self, storage: &dyn Storage, - signature: String, - family_head: IdentityKeyRef, + family_head: FamilyHead, ) -> Result; fn try_head_kick_member( &self, storage: &dyn Storage, - signature: String, member: IdentityKeyRef<'_>, ) -> Result; } diff --git a/contracts/vesting/src/vesting/account/node_families.rs b/contracts/vesting/src/vesting/account/node_families.rs index edf07d27fd..2d6c4ce436 100644 --- a/contracts/vesting/src/vesting/account/node_families.rs +++ b/contracts/vesting/src/vesting/account/node_families.rs @@ -1,18 +1,18 @@ use super::Account; use crate::{errors::ContractError, storage::MIXNET_CONTRACT_ADDRESS, traits::NodeFamilies}; +use contracts_common::signing::MessageSignature; use cosmwasm_std::{wasm_execute, Response, Storage}; +use mixnet_contract_common::families::FamilyHead; use mixnet_contract_common::{ExecuteMsg as MixnetExecuteMsg, IdentityKeyRef}; impl NodeFamilies for Account { fn try_create_family( &self, storage: &dyn Storage, - owner_signature: String, label: String, ) -> Result { let msg = MixnetExecuteMsg::CreateFamilyOnBehalf { - owner_address: self.owner_address().to_string(), - owner_signature, + owner_address: self.owner_address().into_string(), label, }; @@ -24,15 +24,13 @@ impl NodeFamilies for Account { fn try_join_family( &self, storage: &dyn Storage, - node_identity_signature: String, - family_signature: String, - family_head: IdentityKeyRef, + join_permit: MessageSignature, + family_head: FamilyHead, ) -> Result { let msg = MixnetExecuteMsg::JoinFamilyOnBehalf { member_address: self.owner_address().to_string(), - node_identity_signature, - family_signature, - family_head: family_head.to_string(), + join_permit, + family_head, }; let msg = wasm_execute(MIXNET_CONTRACT_ADDRESS.load(storage)?, &msg, vec![])?; @@ -43,13 +41,11 @@ impl NodeFamilies for Account { fn try_leave_family( &self, storage: &dyn Storage, - node_identity_signature: String, - family_head: IdentityKeyRef, + family_head: FamilyHead, ) -> Result { let msg = MixnetExecuteMsg::LeaveFamilyOnBehalf { member_address: self.owner_address().to_string(), - node_identity_signature, - family_head: family_head.to_string(), + family_head, }; let msg = wasm_execute(MIXNET_CONTRACT_ADDRESS.load(storage)?, &msg, vec![])?; @@ -60,12 +56,10 @@ impl NodeFamilies for Account { fn try_head_kick_member( &self, storage: &dyn Storage, - signature: String, member: IdentityKeyRef<'_>, ) -> Result { let msg = MixnetExecuteMsg::KickFamilyMemberOnBehalf { head_address: self.owner_address().to_string(), - signature, member: member.to_string(), }; diff --git a/nym-wallet/src-tauri/src/main.rs b/nym-wallet/src-tauri/src/main.rs index d790d01861..8146bc9a18 100644 --- a/nym-wallet/src-tauri/src/main.rs +++ b/nym-wallet/src-tauri/src/main.rs @@ -74,10 +74,6 @@ fn main() { mixnet::delegate::get_all_mix_delegations, mixnet::delegate::undelegate_from_mixnode, mixnet::delegate::undelegate_all_from_mixnode, - mixnet::families::create_family, - mixnet::families::join_family, - mixnet::families::leave_family, - mixnet::families::kick_family_member, mixnet::interval::get_current_interval, mixnet::interval::get_pending_epoch_events, mixnet::interval::get_pending_interval_events, diff --git a/nym-wallet/src-tauri/src/operations/mixnet/families.rs b/nym-wallet/src-tauri/src/operations/mixnet/families.rs deleted file mode 100644 index aba66c9a58..0000000000 --- a/nym-wallet/src-tauri/src/operations/mixnet/families.rs +++ /dev/null @@ -1,81 +0,0 @@ -use crate::error::BackendError; -use crate::state::WalletState; -use nym_types::transaction::TransactionExecuteResult; -use validator_client::nyxd::traits::MixnetSigningClient; -use validator_client::nyxd::Fee; - -#[tauri::command] -pub async fn create_family( - signature: String, - label: String, - fee: Option, - state: tauri::State<'_, WalletState>, -) -> Result { - let guard = state.read().await; - let fee_amount = guard.convert_tx_fee(fee.as_ref()); - let res = guard - .current_client()? - .nyxd - .create_family(signature, label, fee) - .await?; - Ok(TransactionExecuteResult::from_execute_result( - res, fee_amount, - )?) -} - -#[tauri::command] -pub async fn join_family( - signature: String, - family_head: String, - fee: Option, - state: tauri::State<'_, WalletState>, -) -> Result { - let guard = state.read().await; - let fee_amount = guard.convert_tx_fee(fee.as_ref()); - let res = guard - .current_client()? - .nyxd - .join_family(signature, family_head, fee) - .await?; - Ok(TransactionExecuteResult::from_execute_result( - res, fee_amount, - )?) -} - -#[tauri::command] -pub async fn leave_family( - signature: String, - family_head: String, - fee: Option, - state: tauri::State<'_, WalletState>, -) -> Result { - let guard = state.read().await; - let fee_amount = guard.convert_tx_fee(fee.as_ref()); - let res = guard - .current_client()? - .nyxd - .leave_family(signature, family_head, fee) - .await?; - Ok(TransactionExecuteResult::from_execute_result( - res, fee_amount, - )?) -} - -#[tauri::command] -pub async fn kick_family_member( - signature: String, - member: String, - fee: Option, - state: tauri::State<'_, WalletState>, -) -> Result { - let guard = state.read().await; - let fee_amount = guard.convert_tx_fee(fee.as_ref()); - let res = guard - .current_client()? - .nyxd - .kick_family_member(signature, member, fee) - .await?; - Ok(TransactionExecuteResult::from_execute_result( - res, fee_amount, - )?) -} diff --git a/nym-wallet/src-tauri/src/operations/mixnet/mod.rs b/nym-wallet/src-tauri/src/operations/mixnet/mod.rs index 8b19a26dd0..cb94278ee2 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/mod.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/mod.rs @@ -2,7 +2,6 @@ pub mod account; pub mod admin; pub mod bond; pub mod delegate; -pub mod families; pub mod interval; pub mod rewards; pub mod send; diff --git a/tools/nym-cli/README.md b/tools/nym-cli/README.md index b2ad624a6d..1701577e46 100644 --- a/tools/nym-cli/README.md +++ b/tools/nym-cli/README.md @@ -37,7 +37,7 @@ There are two ways to provide this: ### Passing named arguments -You will need to pass the following with every command as an argument: +You will need to pass the following with most commands as arguments: ``` --mnemonic @@ -70,7 +70,7 @@ nym-cli --help - query for a block at a height - query for a block at a timestamp -### 🪐 `cosmwasm` +### 🪐 cosmwasm - upload a smart contract - instantiate a smart contract @@ -90,6 +90,8 @@ nym-cli --help - query for waiting rewards - withdraw rewards - manage mixnode settings +- create payload for family creation signature +- create family #### 🥩 Delegators diff --git a/tools/nym-cli/src/main.rs b/tools/nym-cli/src/main.rs index 6007a3501f..f01b96fb73 100644 --- a/tools/nym-cli/src/main.rs +++ b/tools/nym-cli/src/main.rs @@ -135,13 +135,16 @@ async fn wait_for_interrupt() { } #[tokio::main] -async fn main() { +async fn main() -> anyhow::Result<()> { setup_logging(); let cli = Cli::parse(); tokio::select! { - _ = wait_for_interrupt() => warn!("Received interrupt - the specified command might have not completed!"), - _ = execute(cli) => (), + _ = wait_for_interrupt() => { + warn!("Received interrupt - the specified command might have not completed!"); + Ok(()) + }, + res = execute(cli) => res } } diff --git a/tools/nym-cli/src/validator/mixnet/operators/mixnodes/families/mod.rs b/tools/nym-cli/src/validator/mixnet/operators/mixnodes/families/mod.rs new file mode 100644 index 0000000000..0f10e79fa3 --- /dev/null +++ b/tools/nym-cli/src/validator/mixnet/operators/mixnodes/families/mod.rs @@ -0,0 +1,47 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_cli_commands::context::{create_query_client, create_signing_client, ClientArgs}; +use nym_cli_commands::validator::mixnet::operators::mixnode::families::MixnetOperatorsMixnodeFamiliesCommands; +use nym_network_defaults::NymNetworkDetails; + +pub(crate) async fn execute( + global_args: ClientArgs, + families: nym_cli_commands::validator::mixnet::operators::mixnode::families::MixnetOperatorsMixnodeFamilies, + network_details: &NymNetworkDetails, +) -> anyhow::Result<()> { + match families.command { + MixnetOperatorsMixnodeFamiliesCommands::CreateFamily(args) => { + nym_cli_commands::validator::mixnet::operators::mixnode::families::create_family::create_family( + args, + create_signing_client(global_args, network_details)?, + ) + .await + } + MixnetOperatorsMixnodeFamiliesCommands::JoinFamily(args) => { + nym_cli_commands::validator::mixnet::operators::mixnode::families::join_family::join_family( + args, + create_signing_client(global_args, network_details)?, + ) + .await + } + MixnetOperatorsMixnodeFamiliesCommands::LeaveFamily(args) => { + nym_cli_commands::validator::mixnet::operators::mixnode::families::leave_family::leave_family( + args, + create_signing_client(global_args, network_details)?, + ) + .await + } + MixnetOperatorsMixnodeFamiliesCommands::KickFamilyMember(args) => { + nym_cli_commands::validator::mixnet::operators::mixnode::families::kick_family_member::kick_family_member( + args, + create_signing_client(global_args, network_details)?, + ) + .await + } + MixnetOperatorsMixnodeFamiliesCommands::CreateFamilyJoinPermitSignPayload(args) => { + nym_cli_commands::validator::mixnet::operators::mixnode::families::create_family_join_permit_sign_payload::create_family_join_permit_sign_payload(args, create_query_client(network_details)?).await + } + } + Ok(()) +} diff --git a/tools/nym-cli/src/validator/mixnet/operators/mixnodes/mod.rs b/tools/nym-cli/src/validator/mixnet/operators/mixnodes/mod.rs index b0823264fa..6100d54b4a 100644 --- a/tools/nym-cli/src/validator/mixnet/operators/mixnodes/mod.rs +++ b/tools/nym-cli/src/validator/mixnet/operators/mixnodes/mod.rs @@ -4,6 +4,7 @@ use nym_cli_commands::context::{create_signing_client, ClientArgs}; use nym_network_defaults::NymNetworkDetails; +pub(crate) mod families; pub(crate) mod keys; pub(crate) mod rewards; pub(crate) mod settings; @@ -23,6 +24,9 @@ pub(crate) async fn execute( nym_cli_commands::validator::mixnet::operators::mixnode::MixnetOperatorsMixnodeCommands::Settings(settings) => { settings::execute(global_args, settings, network_details).await? } + nym_cli_commands::validator::mixnet::operators::mixnode::MixnetOperatorsMixnodeCommands::Families(families) => { + families::execute(global_args, families, network_details).await? + } nym_cli_commands::validator::mixnet::operators::mixnode::MixnetOperatorsMixnodeCommands::Bond(args) => { nym_cli_commands::validator::mixnet::operators::mixnode::bond_mixnode::bond_mixnode(args, create_signing_client(global_args, network_details)?).await }