From 7ff043d8dfb4425177adbf33dcc3c74d9ecab6b6 Mon Sep 17 00:00:00 2001 From: Pierre Dommerc Date: Fri, 10 Mar 2023 15:53:21 +0100 Subject: [PATCH] Feature/bonding signature UI (#3157) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * wip * updated gateways 'sign' command * in-wallet verification of mix bonding signature * changed signature of vesting contract trait method * updated wallet bonding endpoints * tauri commands for generating signing payloads * renamed signer to sender * verifying new signatures in the contract * fixed existing mixnet unit tests * unit tests for invalid signatures * fixed other usages of MessageSignature + FromStr * using base58-encoded serialization * removed owner-signature from details response * added ability to construct bonding payloads via nym-cli * removed signature from bonding payload args * moved 'message_type' from 'ContractMessageContent' to 'SignableMessage' * refactor(wallet-rust): rename owner_signature args * feat(wallet-bonding): handle user signature * feat(wallet-bonding): fix bonding * feat(wallet-bonding): fix lint issue * feat(wallet-bonding): ui adjustment * make the location field mandatory for payload signing * feat(wallet-bonding): remove ownersignature field, remove dead code --------- Co-authored-by: Jędrzej Stuczyński Co-authored-by: Tommy Verrall --- Cargo.lock | 2 + .../validator-client/src/nyxd/mod.rs | 10 + .../src/nyxd/traits/mixnet_query_client.rs | 8 + .../src/nyxd/traits/mixnet_signing_client.rs | 9 +- .../src/nyxd/traits/vesting_signing_client.rs | 13 +- common/commands/Cargo.toml | 1 + common/commands/src/utils.rs | 13 +- .../mixnet/operators/gateway/bond_gateway.rs | 9 +- .../gateway/gateway_bonding_sign_payload.rs | 81 +++ .../validator/mixnet/operators/gateway/mod.rs | 3 + .../operators/gateway/vesting_bond_gateway.rs | 11 +- .../mixnet/operators/mixnode/bond_mixnode.rs | 3 +- .../mixnode/mixnode_bonding_sign_payload.rs | 108 ++++ .../validator/mixnet/operators/mixnode/mod.rs | 3 + .../operators/mixnode/vesting_bond_mixnode.rs | 7 +- .../contracts-common/src/lib.rs | 1 + .../contracts-common/src/signing/mod.rs | 259 ++++++++++ .../contracts-common/src/signing/verifier.rs | 81 +++ .../mixnet-contract/src/error.rs | 7 + .../mixnet-contract/src/lib.rs | 2 + .../mixnet-contract/src/msg.rs | 14 +- .../mixnet-contract/src/signing.rs | 0 .../mixnet-contract/src/signing_types.rs | 151 ++++++ .../vesting-contract/src/messages.rs | 5 +- common/types/src/gateway.rs | 2 - common/types/src/mixnode.rs | 2 - contracts/mixnet/Cargo.toml | 3 +- contracts/mixnet/src/constants.rs | 62 +-- contracts/mixnet/src/contract.rs | 3 + contracts/mixnet/src/families/transactions.rs | 112 ++-- contracts/mixnet/src/gateways/mod.rs | 1 + contracts/mixnet/src/gateways/queries.rs | 73 +-- .../mixnet/src/gateways/signature_helpers.rs | 34 ++ contracts/mixnet/src/gateways/transactions.rs | 205 +++++--- contracts/mixnet/src/lib.rs | 1 + contracts/mixnet/src/mixnodes/mod.rs | 1 + contracts/mixnet/src/mixnodes/queries.rs | 218 +++----- .../mixnet/src/mixnodes/signature_helpers.rs | 36 ++ contracts/mixnet/src/mixnodes/transactions.rs | 224 +++++--- contracts/mixnet/src/signing/mod.rs | 5 + contracts/mixnet/src/signing/queries.rs | 11 + contracts/mixnet/src/signing/storage.rs | 30 ++ contracts/mixnet/src/support/helpers.rs | 6 +- .../mixnet/src/support/tests/messages.rs | 27 +- contracts/mixnet/src/support/tests/mod.rs | 479 +++++++++++------- contracts/mixnet/src/support/tests/queries.rs | 39 -- contracts/vesting/src/contract.rs | 5 +- .../vesting/src/traits/bonding_account.rs | 5 +- .../account/gateway_bonding_account.rs | 3 +- .../account/mixnode_bonding_account.rs | 3 +- contracts/vesting/src/vesting/mod.rs | 13 +- gateway/src/commands/sign.rs | 45 +- gateway/src/config/mod.rs | 1 + gateway/src/node/mod.rs | 23 +- mixnode/Cargo.toml | 1 + mixnode/src/commands/sign.rs | 45 +- mixnode/src/node/mod.rs | 20 - nym-wallet/Cargo.lock | 256 ++++++++-- nym-wallet/src-tauri/Cargo.toml | 5 + nym-wallet/src-tauri/src/error.rs | 10 + nym-wallet/src-tauri/src/main.rs | 4 + .../src-tauri/src/operations/helpers.rs | 391 ++++++++++++++ .../src-tauri/src/operations/mixnet/bond.rs | 46 +- nym-wallet/src-tauri/src/operations/mod.rs | 4 + .../signatures/ed25519_signing_payload.rs | 100 ++++ .../src/operations/signatures/mod.rs | 1 + .../src/operations/simulate/mixnet.rs | 9 +- .../src/operations/simulate/vesting.rs | 9 +- .../src-tauri/src/operations/vesting/bond.rs | 39 +- .../Bonding/forms/BondGatewayForm.tsx | 211 +------- .../Bonding/forms/BondMixnodeForm.tsx | 249 +-------- .../Bonding/forms/GatewayAmountForm.tsx | 84 +++ .../Bonding/forms/GatewayInitForm.tsx | 116 +++++ .../Bonding/forms/GatewaySignatureForm.tsx | 87 ++++ .../Bonding/forms/MixnodeAmountForm.tsx | 123 +++++ .../Bonding/forms/MixnodeInitForm.tsx | 109 ++++ .../Bonding/forms/MixnodeSignatureForm.tsx | 88 ++++ .../Bonding/forms/gatewayValidationSchema.ts | 4 - .../Bonding/forms/mixnodeValidationSchema.ts | 4 - .../Bonding/modals/BondGatewayModal.tsx | 49 +- .../Bonding/modals/BondMixnodeModal.tsx | 65 +-- nym-wallet/src/components/Bonding/utils.ts | 37 ++ nym-wallet/src/context/bonding.tsx | 60 ++- nym-wallet/src/context/mocks/bonding.tsx | 19 + nym-wallet/src/pages/bonding/types.ts | 19 +- nym-wallet/src/requests/actions.ts | 15 +- nym-wallet/src/requests/simulate.ts | 2 +- nym-wallet/src/requests/vesting.ts | 19 +- nym-wallet/src/types/global.ts | 15 +- .../mixnet/operators/gateways/mod.rs | 3 + .../mixnet/operators/mixnodes/mod.rs | 3 + ts-packages/mock-nym-api/package.json | 4 +- 92 files changed, 3435 insertions(+), 1363 deletions(-) create mode 100644 common/commands/src/validator/mixnet/operators/gateway/gateway_bonding_sign_payload.rs create mode 100644 common/commands/src/validator/mixnet/operators/mixnode/mixnode_bonding_sign_payload.rs create mode 100644 common/cosmwasm-smart-contracts/contracts-common/src/signing/mod.rs create mode 100644 common/cosmwasm-smart-contracts/contracts-common/src/signing/verifier.rs create mode 100644 common/cosmwasm-smart-contracts/mixnet-contract/src/signing.rs create mode 100644 common/cosmwasm-smart-contracts/mixnet-contract/src/signing_types.rs create mode 100644 contracts/mixnet/src/gateways/signature_helpers.rs create mode 100644 contracts/mixnet/src/mixnodes/signature_helpers.rs create mode 100644 contracts/mixnet/src/signing/mod.rs create mode 100644 contracts/mixnet/src/signing/queries.rs create mode 100644 contracts/mixnet/src/signing/storage.rs delete mode 100644 contracts/mixnet/src/support/tests/queries.rs create mode 100644 nym-wallet/src-tauri/src/operations/helpers.rs create mode 100644 nym-wallet/src-tauri/src/operations/signatures/ed25519_signing_payload.rs create mode 100644 nym-wallet/src/components/Bonding/forms/GatewayAmountForm.tsx create mode 100644 nym-wallet/src/components/Bonding/forms/GatewayInitForm.tsx create mode 100644 nym-wallet/src/components/Bonding/forms/GatewaySignatureForm.tsx create mode 100644 nym-wallet/src/components/Bonding/forms/MixnodeAmountForm.tsx create mode 100644 nym-wallet/src/components/Bonding/forms/MixnodeInitForm.tsx create mode 100644 nym-wallet/src/components/Bonding/forms/MixnodeSignatureForm.tsx create mode 100644 nym-wallet/src/components/Bonding/utils.ts diff --git a/Cargo.lock b/Cargo.lock index 48e0705c85..e262a0b76a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3212,6 +3212,7 @@ dependencies = [ "log", "nym-coconut-bandwidth-contract-common", "nym-coconut-dkg-common", + "nym-contracts-common", "nym-mixnet-contract-common", "nym-multisig-contract-common", "nym-network-defaults", @@ -3507,6 +3508,7 @@ dependencies = [ "mixnode-common", "nym-bin-common", "nym-config", + "nym-contracts-common", "nym-crypto", "nym-nonexhaustive-delayqueue", "nym-pemstore", diff --git a/common/client-libs/validator-client/src/nyxd/mod.rs b/common/client-libs/validator-client/src/nyxd/mod.rs index 4990f1b030..b7612c5df3 100644 --- a/common/client-libs/validator-client/src/nyxd/mod.rs +++ b/common/client-libs/validator-client/src/nyxd/mod.rs @@ -43,6 +43,7 @@ pub use cosmrs::tendermint::Time as TendermintTime; pub use cosmrs::tx::{self, Gas}; pub use cosmrs::Coin as CosmosCoin; pub use cosmrs::{bip32, AccountId, Decimal, Denom}; +use cosmwasm_std::Addr; pub use cosmwasm_std::Coin as CosmWasmCoin; pub use fee::{gas_price::GasPrice, GasAdjustable, GasAdjustment}; pub use signing_client::Client as SigningNyxdClient; @@ -369,6 +370,15 @@ where &self.client_address.as_ref().unwrap()[0] } + pub fn cw_address(&self) -> Addr + where + C: SigningCosmWasmClient, + { + // the call to unchecked is fine here as we're converting directly from `AccountId` + // which must have been a valid bech32 address + Addr::unchecked(self.address().as_ref()) + } + pub fn signer(&self) -> &DirectSecp256k1HdWallet where C: SigningCosmWasmClient, diff --git a/common/client-libs/validator-client/src/nyxd/traits/mixnet_query_client.rs b/common/client-libs/validator-client/src/nyxd/traits/mixnet_query_client.rs index 2ff48d1113..fd97892420 100644 --- a/common/client-libs/validator-client/src/nyxd/traits/mixnet_query_client.rs +++ b/common/client-libs/validator-client/src/nyxd/traits/mixnet_query_client.rs @@ -6,6 +6,7 @@ use crate::nyxd::error::NyxdError; use crate::nyxd::NyxdClient; use async_trait::async_trait; use cosmrs::AccountId; +use nym_contracts_common::signing::Nonce; use nym_mixnet_contract_common::delegation::{MixNodeDelegationResponse, OwnerProxySubKey}; use nym_mixnet_contract_common::families::Family; use nym_mixnet_contract_common::mixnode::{ @@ -390,6 +391,13 @@ pub trait MixnetQueryClient { .await } + async fn get_signing_nonce(&self, address: &AccountId) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetSigningNonce { + address: address.to_string(), + }) + .await + } + async fn get_node_family_by_label(&self, label: &str) -> Result, NyxdError> { self.query_mixnet_contract(MixnetQueryMsg::GetFamilyByLabel { label: label.to_string(), 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 dafad09536..0f49dcc622 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 @@ -8,6 +8,7 @@ use crate::nyxd::error::NyxdError; 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::mixnode::{MixNodeConfigUpdate, MixNodeCostParams}; use nym_mixnet_contract_common::reward_params::{IntervalRewardingParamsUpdate, Performance}; use nym_mixnet_contract_common::{ @@ -290,7 +291,7 @@ pub trait MixnetSigningClient { &self, mix_node: MixNode, cost_params: MixNodeCostParams, - owner_signature: String, + owner_signature: MessageSignature, pledge: Coin, fee: Option, ) -> Result { @@ -311,7 +312,7 @@ pub trait MixnetSigningClient { owner: AccountId, mix_node: MixNode, cost_params: MixNodeCostParams, - owner_signature: String, + owner_signature: MessageSignature, pledge: Coin, fee: Option, ) -> Result { @@ -442,7 +443,7 @@ pub trait MixnetSigningClient { async fn bond_gateway( &self, gateway: Gateway, - owner_signature: String, + owner_signature: MessageSignature, pledge: Coin, fee: Option, ) -> Result { @@ -461,7 +462,7 @@ pub trait MixnetSigningClient { &self, owner: AccountId, gateway: Gateway, - owner_signature: String, + owner_signature: MessageSignature, pledge: Coin, fee: Option, ) -> Result { 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 e2d622d12d..fec9c19c04 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 @@ -6,6 +6,7 @@ use crate::nyxd::cosmwasm_client::types::ExecuteResult; 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::mixnode::{MixNodeConfigUpdate, MixNodeCostParams}; use nym_mixnet_contract_common::{Gateway, MixId, MixNode}; use nym_vesting_contract_common::messages::{ @@ -43,7 +44,7 @@ pub trait VestingSigningClient { async fn vesting_bond_gateway( &self, gateway: Gateway, - owner_signature: &str, + owner_signature: MessageSignature, pledge: Coin, fee: Option, ) -> Result; @@ -61,7 +62,7 @@ pub trait VestingSigningClient { &self, mix_node: MixNode, cost_params: MixNodeCostParams, - owner_signature: &str, + owner_signature: MessageSignature, pledge: Coin, fee: Option, ) -> Result; @@ -208,14 +209,14 @@ impl VestingSigningClient for Ny async fn vesting_bond_gateway( &self, gateway: Gateway, - owner_signature: &str, + owner_signature: MessageSignature, pledge: Coin, fee: Option, ) -> Result { let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); let req = VestingExecuteMsg::BondGateway { gateway, - owner_signature: owner_signature.to_string(), + owner_signature, amount: pledge.into(), }; self.client @@ -272,7 +273,7 @@ impl VestingSigningClient for Ny &self, mix_node: MixNode, cost_params: MixNodeCostParams, - owner_signature: &str, + owner_signature: MessageSignature, pledge: Coin, fee: Option, ) -> Result { @@ -281,7 +282,7 @@ impl VestingSigningClient for Ny VestingExecuteMsg::BondMixnode { mix_node, cost_params, - owner_signature: owner_signature.to_string(), + owner_signature, amount: pledge.into(), }, vec![], diff --git a/common/commands/Cargo.toml b/common/commands/Cargo.toml index 5a872b0ec1..12ea1b5176 100644 --- a/common/commands/Cargo.toml +++ b/common/commands/Cargo.toml @@ -30,6 +30,7 @@ cosmwasm-std = { version = "1.0.0" } validator-client = { path = "../client-libs/validator-client", features = ["nyxd-client"] } 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" } nym-vesting-contract-common = { path = "../cosmwasm-smart-contracts/vesting-contract" } nym-coconut-bandwidth-contract-common = { path = "../cosmwasm-smart-contracts/coconut-bandwidth-contract" } diff --git a/common/commands/src/utils.rs b/common/commands/src/utils.rs index 682465b010..05c395924d 100644 --- a/common/commands/src/utils.rs +++ b/common/commands/src/utils.rs @@ -1,13 +1,20 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use cosmrs::AccountId; +use cosmwasm_std::{Addr, Coin as CosmWasmCoin, Decimal}; +use log::error; use std::error::Error; use std::fmt::Display; - -use cosmwasm_std::{Coin as CosmWasmCoin, Decimal}; -use log::error; use validator_client::nyxd::Coin; +// TODO: perhaps it should be moved to some global common crate? +pub fn account_id_to_cw_addr(account_id: &AccountId) -> Addr { + // the call to unchecked is fine here as we're converting directly from `AccountId` + // which must have been a valid bech32 address + Addr::unchecked(account_id.as_ref()) +} + pub fn pretty_coin(coin: &Coin) -> String { let amount = Decimal::from_ratio(coin.amount, 1_000_000u128); let denom = if coin.denom.starts_with('u') { diff --git a/common/commands/src/validator/mixnet/operators/gateway/bond_gateway.rs b/common/commands/src/validator/mixnet/operators/gateway/bond_gateway.rs index e565364609..d5a8ab3c47 100644 --- a/common/commands/src/validator/mixnet/operators/gateway/bond_gateway.rs +++ b/common/commands/src/validator/mixnet/operators/gateway/bond_gateway.rs @@ -4,6 +4,7 @@ use crate::context::SigningClient; use clap::Parser; use log::{info, warn}; +use nym_contracts_common::signing::MessageSignature; use nym_mixnet_contract_common::Coin; use nym_network_defaults::{DEFAULT_CLIENT_LISTENING_PORT, DEFAULT_MIX_LISTENING_PORT}; use validator_client::nyxd::traits::MixnetSigningClient; @@ -14,7 +15,7 @@ pub struct Args { pub host: String, #[clap(long)] - pub signature: String, + pub signature: MessageSignature, #[clap(long)] pub mix_port: Option, @@ -23,7 +24,7 @@ pub struct Args { pub clients_port: Option, #[clap(long)] - pub location: Option, + pub location: String, #[clap(long)] pub sphinx_key: String, @@ -59,9 +60,7 @@ pub async fn bond_gateway(args: Args, client: SigningClient) { host: args.host, mix_port: args.mix_port.unwrap_or(DEFAULT_MIX_LISTENING_PORT), clients_port: args.clients_port.unwrap_or(DEFAULT_CLIENT_LISTENING_PORT), - location: args - .location - .unwrap_or_else(|| "secret gateway location".to_owned()), + location: args.location, sphinx_key: args.sphinx_key, identity_key: args.identity_key, version: args.version, diff --git a/common/commands/src/validator/mixnet/operators/gateway/gateway_bonding_sign_payload.rs b/common/commands/src/validator/mixnet/operators/gateway/gateway_bonding_sign_payload.rs new file mode 100644 index 0000000000..470c25bb98 --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/gateway/gateway_bonding_sign_payload.rs @@ -0,0 +1,81 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::context::SigningClient; +use crate::utils::account_id_to_cw_addr; +use clap::Parser; +use cosmwasm_std::Coin; +use nym_mixnet_contract_common::construct_gateway_bonding_sign_payload; +use nym_network_defaults::{DEFAULT_CLIENT_LISTENING_PORT, DEFAULT_MIX_LISTENING_PORT}; +use validator_client::nyxd::traits::MixnetQueryClient; + +#[derive(Debug, Parser)] +pub struct Args { + #[clap(long)] + pub host: String, + + #[clap(long)] + pub mix_port: Option, + + #[clap(long)] + pub clients_port: Option, + + #[clap(long)] + pub location: String, + + #[clap(long)] + pub sphinx_key: String, + + #[clap(long)] + pub identity_key: String, + + #[clap(long)] + pub version: String, + + #[clap( + long, + help = "bonding amount in current DENOMINATION (so it would be 'unym', rather than 'nym')" + )] + pub amount: u128, + + /// Indicates whether the gateway is going to get bonded via a vesting account + #[arg(long)] + pub with_vesting_account: bool, +} + +pub async fn create_payload(args: Args, client: SigningClient) { + let denom = client.current_chain_details().mix_denom.base.as_str(); + + let gateway = nym_mixnet_contract_common::Gateway { + host: args.host, + mix_port: args.mix_port.unwrap_or(DEFAULT_MIX_LISTENING_PORT), + clients_port: args.clients_port.unwrap_or(DEFAULT_CLIENT_LISTENING_PORT), + location: args.location, + sphinx_key: args.sphinx_key, + identity_key: args.identity_key, + version: args.version, + }; + + let coin = Coin::new(args.amount, denom); + + let nonce = match client.get_signing_nonce(client.address()).await { + Ok(nonce) => nonce, + Err(err) => { + eprint!( + "failed to query for the signing nonce of {}: {err}", + client.address() + ); + return; + } + }; + + let address = account_id_to_cw_addr(client.address()); + let proxy = if args.with_vesting_account { + Some(account_id_to_cw_addr(client.vesting_contract_address())) + } else { + None + }; + + let payload = construct_gateway_bonding_sign_payload(nonce, address, proxy, coin, gateway); + println!("{}", payload.to_base58_string().unwrap()) +} diff --git a/common/commands/src/validator/mixnet/operators/gateway/mod.rs b/common/commands/src/validator/mixnet/operators/gateway/mod.rs index af658a7876..59cbea3f1b 100644 --- a/common/commands/src/validator/mixnet/operators/gateway/mod.rs +++ b/common/commands/src/validator/mixnet/operators/gateway/mod.rs @@ -4,6 +4,7 @@ use clap::{Args, Subcommand}; pub mod bond_gateway; +pub mod gateway_bonding_sign_payload; pub mod unbond_gateway; pub mod vesting_bond_gateway; pub mod vesting_unbond_gateway; @@ -25,4 +26,6 @@ pub enum MixnetOperatorsGatewayCommands { VestingBond(vesting_bond_gateway::Args), /// Unbound from a gateway (when originally using locked tokens) VestingUnbound(vesting_unbond_gateway::Args), + /// Create base58-encoded payload required for producing valid bonding signature. + CreateGatewayBondingSignPayload(gateway_bonding_sign_payload::Args), } diff --git a/common/commands/src/validator/mixnet/operators/gateway/vesting_bond_gateway.rs b/common/commands/src/validator/mixnet/operators/gateway/vesting_bond_gateway.rs index 9cb4b338d4..77aa8323d7 100644 --- a/common/commands/src/validator/mixnet/operators/gateway/vesting_bond_gateway.rs +++ b/common/commands/src/validator/mixnet/operators/gateway/vesting_bond_gateway.rs @@ -4,6 +4,7 @@ use crate::context::SigningClient; use clap::Parser; use log::{info, warn}; +use nym_contracts_common::signing::MessageSignature; use nym_mixnet_contract_common::{Coin, Gateway}; use nym_network_defaults::{DEFAULT_CLIENT_LISTENING_PORT, DEFAULT_MIX_LISTENING_PORT}; use validator_client::nyxd::VestingSigningClient; @@ -14,7 +15,7 @@ pub struct Args { pub host: String, #[clap(long)] - pub signature: String, + pub signature: MessageSignature, #[clap(long)] pub mix_port: Option, @@ -23,7 +24,7 @@ pub struct Args { pub clients_port: Option, #[clap(long)] - pub location: Option, + pub location: String, #[clap(long)] pub sphinx_key: String, @@ -57,9 +58,7 @@ pub async fn vesting_bond_gateway(client: SigningClient, args: Args, denom: &str host: args.host, mix_port: args.mix_port.unwrap_or(DEFAULT_MIX_LISTENING_PORT), clients_port: args.clients_port.unwrap_or(DEFAULT_CLIENT_LISTENING_PORT), - location: args - .location - .unwrap_or_else(|| "secret gateway location".to_owned()), + location: args.location, sphinx_key: args.sphinx_key, identity_key: args.identity_key, version: args.version, @@ -68,7 +67,7 @@ pub async fn vesting_bond_gateway(client: SigningClient, args: Args, denom: &str let coin = Coin::new(args.amount, denom); let res = client - .vesting_bond_gateway(gateway, &args.signature, coin.into(), None) + .vesting_bond_gateway(gateway, args.signature, coin.into(), None) .await .expect("failed to bond gateway!"); diff --git a/common/commands/src/validator/mixnet/operators/mixnode/bond_mixnode.rs b/common/commands/src/validator/mixnet/operators/mixnode/bond_mixnode.rs index f6650a9ac9..14538c5288 100644 --- a/common/commands/src/validator/mixnet/operators/mixnode/bond_mixnode.rs +++ b/common/commands/src/validator/mixnet/operators/mixnode/bond_mixnode.rs @@ -5,6 +5,7 @@ use clap::Parser; use cosmwasm_std::Uint128; use log::{info, warn}; +use nym_contracts_common::signing::MessageSignature; use nym_mixnet_contract_common::{Coin, MixNodeCostParams, Percent}; use nym_network_defaults::{ DEFAULT_HTTP_API_LISTENING_PORT, DEFAULT_MIX_LISTENING_PORT, DEFAULT_VERLOC_LISTENING_PORT, @@ -20,7 +21,7 @@ pub struct Args { pub host: String, #[clap(long)] - pub signature: String, + pub signature: MessageSignature, #[clap(long)] pub mix_port: Option, diff --git a/common/commands/src/validator/mixnet/operators/mixnode/mixnode_bonding_sign_payload.rs b/common/commands/src/validator/mixnet/operators/mixnode/mixnode_bonding_sign_payload.rs new file mode 100644 index 0000000000..a4b41b03f2 --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/mixnode/mixnode_bonding_sign_payload.rs @@ -0,0 +1,108 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::context::SigningClient; +use crate::utils::account_id_to_cw_addr; +use clap::Parser; +use cosmwasm_std::{Coin, Uint128}; +use nym_contracts_common::Percent; +use nym_mixnet_contract_common::{construct_mixnode_bonding_sign_payload, MixNodeCostParams}; +use nym_network_defaults::{ + DEFAULT_HTTP_API_LISTENING_PORT, DEFAULT_MIX_LISTENING_PORT, DEFAULT_VERLOC_LISTENING_PORT, +}; +use validator_client::nyxd::traits::MixnetQueryClient; +use validator_client::nyxd::CosmWasmCoin; + +#[derive(Debug, Parser)] +pub struct Args { + #[clap(long)] + pub host: String, + + #[clap(long)] + pub mix_port: Option, + + #[clap(long)] + pub verloc_port: Option, + + #[clap(long)] + pub http_api_port: Option, + + #[clap(long)] + pub sphinx_key: String, + + #[clap(long)] + pub identity_key: String, + + #[clap(long)] + pub version: String, + + #[clap(long)] + pub profit_margin_percent: Option, + + #[clap( + long, + help = "operating cost in current DENOMINATION (so it would be 'unym', rather than 'nym')" + )] + pub interval_operating_cost: Option, + + #[clap( + long, + help = "bonding amount in current DENOMINATION (so it would be 'unym', rather than 'nym')" + )] + pub amount: u128, + + /// Indicates whether the mixnode is going to get bonded via a vesting account + #[arg(long)] + pub with_vesting_account: bool, +} + +pub async fn create_payload(args: Args, client: SigningClient) { + let denom = client.current_chain_details().mix_denom.base.as_str(); + + let mixnode = nym_mixnet_contract_common::MixNode { + host: args.host, + mix_port: args.mix_port.unwrap_or(DEFAULT_MIX_LISTENING_PORT), + verloc_port: args.verloc_port.unwrap_or(DEFAULT_VERLOC_LISTENING_PORT), + http_api_port: args + .http_api_port + .unwrap_or(DEFAULT_HTTP_API_LISTENING_PORT), + sphinx_key: args.sphinx_key, + identity_key: args.identity_key, + version: args.version, + }; + + let coin = Coin::new(args.amount, denom); + + let cost_params = MixNodeCostParams { + profit_margin_percent: Percent::from_percentage_value( + args.profit_margin_percent.unwrap_or(10) as u64, + ) + .unwrap(), + interval_operating_cost: CosmWasmCoin { + denom: denom.into(), + amount: Uint128::new(args.interval_operating_cost.unwrap_or(40_000_000)), + }, + }; + + let nonce = match client.get_signing_nonce(client.address()).await { + Ok(nonce) => nonce, + Err(err) => { + eprint!( + "failed to query for the signing nonce of {}: {err}", + client.address() + ); + return; + } + }; + + let address = account_id_to_cw_addr(client.address()); + let proxy = if args.with_vesting_account { + Some(account_id_to_cw_addr(client.vesting_contract_address())) + } else { + None + }; + + let payload = + construct_mixnode_bonding_sign_payload(nonce, address, proxy, coin, mixnode, cost_params); + println!("{}", payload.to_base58_string().unwrap()) +} diff --git a/common/commands/src/validator/mixnet/operators/mixnode/mod.rs b/common/commands/src/validator/mixnet/operators/mixnode/mod.rs index e608c4c36f..5d7f0cd6f1 100644 --- a/common/commands/src/validator/mixnet/operators/mixnode/mod.rs +++ b/common/commands/src/validator/mixnet/operators/mixnode/mod.rs @@ -5,6 +5,7 @@ use clap::{Args, Subcommand}; pub mod bond_mixnode; pub mod keys; +pub mod mixnode_bonding_sign_payload; pub mod rewards; pub mod settings; pub mod unbond_mixnode; @@ -34,4 +35,6 @@ pub enum MixnetOperatorsMixnodeCommands { BondVesting(vesting_bond_mixnode::Args), /// Unbound from a mixnode (when originally using locked tokens) UnboundVesting(vesting_unbond_mixnode::Args), + /// Create base58-encoded payload required for producing valid bonding signature. + CreateMixnodeBondingSignPayload(mixnode_bonding_sign_payload::Args), } diff --git a/common/commands/src/validator/mixnet/operators/mixnode/vesting_bond_mixnode.rs b/common/commands/src/validator/mixnet/operators/mixnode/vesting_bond_mixnode.rs index a42d3b02de..03bcf8dc7a 100644 --- a/common/commands/src/validator/mixnet/operators/mixnode/vesting_bond_mixnode.rs +++ b/common/commands/src/validator/mixnet/operators/mixnode/vesting_bond_mixnode.rs @@ -1,10 +1,11 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use crate::context::SigningClient; use clap::Parser; use cosmwasm_std::Uint128; use log::{info, warn}; +use nym_contracts_common::signing::MessageSignature; use nym_mixnet_contract_common::{Coin, MixNodeCostParams}; use nym_mixnet_contract_common::{MixNode, Percent}; use nym_network_defaults::{ @@ -18,7 +19,7 @@ pub struct Args { pub host: String, #[clap(long)] - pub signature: String, + pub signature: MessageSignature, #[clap(long)] pub mix_port: Option, @@ -95,7 +96,7 @@ pub async fn vesting_bond_mixnode(client: SigningClient, args: Args, denom: &str }; let res = client - .vesting_bond_mixnode(mixnode, cost_params, &args.signature, coin.into(), None) + .vesting_bond_mixnode(mixnode, cost_params, args.signature, coin.into(), None) .await .expect("failed to bond vesting mixnode!"); diff --git a/common/cosmwasm-smart-contracts/contracts-common/src/lib.rs b/common/cosmwasm-smart-contracts/contracts-common/src/lib.rs index 4c351a0ccf..9f6cd8098f 100644 --- a/common/cosmwasm-smart-contracts/contracts-common/src/lib.rs +++ b/common/cosmwasm-smart-contracts/contracts-common/src/lib.rs @@ -6,6 +6,7 @@ pub mod dealings; pub mod events; +pub mod signing; pub mod types; pub use types::*; diff --git a/common/cosmwasm-smart-contracts/contracts-common/src/signing/mod.rs b/common/cosmwasm-smart-contracts/contracts-common/src/signing/mod.rs new file mode 100644 index 0000000000..206152957b --- /dev/null +++ b/common/cosmwasm-smart-contracts/contracts-common/src/signing/mod.rs @@ -0,0 +1,259 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_std::{from_slice, to_vec, Addr, Coin, MessageInfo, StdResult}; +use schemars::JsonSchema; +use serde::de::DeserializeOwned; +use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; +use std::fmt::{Display, Formatter}; +use std::str::FromStr; +pub use verifier::Verifier; + +pub mod verifier; + +pub type Nonce = u32; + +// define this type explicitly for [hopefully] better usability +// (so you wouldn't need to worry about whether you should use bytes, bs58, etc.) +#[derive(Clone, Debug, PartialEq, JsonSchema)] +pub struct MessageSignature(Vec); + +impl MessageSignature { + pub fn as_bs58_string(&self) -> String { + bs58::encode(&self.0).into_string() + } +} + +impl<'a> From<&'a [u8]> for MessageSignature { + fn from(value: &'a [u8]) -> Self { + MessageSignature(value.to_vec()) + } +} + +impl From> for MessageSignature { + fn from(value: Vec) -> Self { + MessageSignature(value) + } +} + +impl<'a> TryFrom<&'a str> for MessageSignature { + type Error = bs58::decode::Error; + + fn try_from(value: &'a str) -> Result { + Ok(MessageSignature(bs58::decode(value).into_vec()?)) + } +} + +impl TryFrom for MessageSignature { + type Error = bs58::decode::Error; + + fn try_from(value: String) -> Result { + Self::try_from(value.as_str()) + } +} + +impl AsRef<[u8]> for MessageSignature { + #[inline] + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +impl<'de> Deserialize<'de> for MessageSignature { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let inner = String::deserialize(deserializer)?; + let bytes = bs58::decode(inner).into_vec().map_err(de::Error::custom)?; + Ok(MessageSignature(bytes)) + } +} + +impl Serialize for MessageSignature { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let bs58_encoded = self.as_bs58_string(); + bs58_encoded.serialize(serializer) + } +} + +impl Display for MessageSignature { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.as_bs58_string()) + } +} + +impl FromStr for MessageSignature { + type Err = bs58::decode::Error; + + fn from_str(s: &str) -> Result { + Self::try_from(s) + } +} + +pub trait SigningPurpose { + fn message_type() -> MessageType; +} + +#[derive(Serialize, Deserialize)] +#[serde(transparent)] +pub struct MessageType(String); + +impl MessageType { + pub fn new>(typ: S) -> Self { + MessageType(typ.into()) + } +} + +impl From for MessageType +where + T: ToString, +{ + fn from(value: T) -> Self { + MessageType(value.to_string()) + } +} + +#[derive(Default, Debug, Serialize, Deserialize, Copy, Clone)] +#[serde(rename_all = "lowercase")] +pub enum SigningAlgorithm { + #[default] + Ed25519, + Secp256k1, +} + +impl SigningAlgorithm { + pub fn is_ed25519(&self) -> bool { + matches!(self, SigningAlgorithm::Ed25519) + } +} + +// TODO: maybe move this one to repo-wide common? +// TODO: should it perhaps also include the public key itself? +#[derive(Serialize, Deserialize)] +pub struct SignableMessage { + pub nonce: u32, + pub algorithm: SigningAlgorithm, + pub message_type: MessageType, + + pub content: T, +} + +impl SignableMessage +where + T: SigningPurpose, +{ + pub fn new(nonce: u32, content: T) -> Self { + SignableMessage { + nonce, + algorithm: SigningAlgorithm::Ed25519, + message_type: T::message_type(), + content, + } + } + + pub fn with_signing_algorithm(mut self, algorithm: SigningAlgorithm) -> Self { + self.algorithm = algorithm; + self + } + + pub fn to_plaintext(&self) -> StdResult> + where + T: Serialize, + { + to_vec(self) + } + + pub fn to_sha256_plaintext_digest(&self) -> StdResult> + where + T: Serialize, + { + unimplemented!() + } + + pub fn to_json_string(&self) -> StdResult + where + T: Serialize, + { + // if you look into implementation of `serde_json_wasm::to_string` this [i.e. the String conversion] + // CAN'T fail, but let's avoid this unnecessary unwrap either way + self.to_plaintext() + .map(|s| String::from_utf8(s).unwrap_or(String::from("SERIALIZATION FAILURE"))) + } + + pub fn to_base58_string(&self) -> StdResult + where + T: Serialize, + { + self.to_plaintext().map(|s| bs58::encode(s).into_string()) + } + + pub fn try_from_bytes(bytes: &[u8]) -> StdResult> + where + T: DeserializeOwned, + { + from_slice(bytes) + } + + pub fn try_from_string(raw: &str) -> StdResult> + where + T: DeserializeOwned, + { + Self::try_from_bytes(raw.as_bytes()) + } + + pub fn try_from_base58_string(raw: &str) -> bs58::decode::Result>> + where + T: DeserializeOwned, + { + bs58::decode(raw) + .into_vec() + .map(|d| Self::try_from_bytes(&d)) + } +} + +#[derive(Serialize)] +pub struct ContractMessageContent { + pub sender: Addr, + pub proxy: Option, + pub funds: Vec, + pub data: T, +} + +impl SigningPurpose for ContractMessageContent +where + T: SigningPurpose, +{ + fn message_type() -> MessageType { + T::message_type() + } +} + +impl ContractMessageContent { + pub fn new(sender: Addr, proxy: Option, funds: Vec, data: T) -> Self { + ContractMessageContent { + sender, + proxy, + funds, + data, + } + } + + pub fn new_with_info(info: MessageInfo, signer: Addr, data: T) -> Self { + let proxy = if info.sender == signer { + None + } else { + Some(info.sender) + }; + + ContractMessageContent { + sender: signer, + proxy, + funds: info.funds, + data, + } + } +} diff --git a/common/cosmwasm-smart-contracts/contracts-common/src/signing/verifier.rs b/common/cosmwasm-smart-contracts/contracts-common/src/signing/verifier.rs new file mode 100644 index 0000000000..6663703d42 --- /dev/null +++ b/common/cosmwasm-smart-contracts/contracts-common/src/signing/verifier.rs @@ -0,0 +1,81 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::signing::{MessageSignature, SignableMessage, SigningAlgorithm, SigningPurpose}; +use cosmwasm_std::{Api, StdError, VerificationError}; +use serde::Serialize; +use thiserror::Error; + +pub trait Verifier { + type Error: From; + + fn verify_message( + &self, + message: SignableMessage, + signature: MessageSignature, + public_key: &[u8], + ) -> Result { + match message.algorithm { + SigningAlgorithm::Ed25519 => { + let plaintext = message.to_plaintext()?; + self.verify_ed25519(&plaintext, signature.as_ref(), public_key) + } + SigningAlgorithm::Secp256k1 => { + let plaintext = message.to_sha256_plaintext_digest()?; + self.verify_secp256k1(&plaintext, signature.as_ref(), public_key) + } + } + } + + fn verify_ed25519( + &self, + _message: &[u8], + _signature: &[u8], + _public_key: &[u8], + ) -> Result { + unimplemented!() + } + + fn verify_secp256k1( + &self, + _message_hash: &[u8], + _signature: &[u8], + _public_key: &[u8], + ) -> Result { + unimplemented!() + } +} + +#[derive(Debug, Error, PartialEq)] +pub enum ApiVerifierError { + #[error(transparent)] + Verification(#[from] VerificationError), + + #[error(transparent)] + Std(#[from] StdError), +} + +impl Verifier for T +where + T: Api + ?Sized, +{ + type Error = ApiVerifierError; + + fn verify_ed25519( + &self, + message: &[u8], + signature: &[u8], + public_key: &[u8], + ) -> Result { + Ok(self.ed25519_verify(message, signature, public_key)?) + } + + fn verify_secp256k1( + &self, + message_hash: &[u8], + signature: &[u8], + public_key: &[u8], + ) -> Result { + Ok(self.secp256k1_verify(message_hash, signature, public_key)?) + } +} diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs index 7a68f81d90..d6b8ba07d5 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::{EpochState, IdentityKey, MixId}; +use contracts_common::signing::verifier::ApiVerifierError; use cosmwasm_std::{Addr, Coin, Decimal}; use thiserror::Error; @@ -218,4 +219,10 @@ pub enum MixnetContractError { value: String, error_message: String, }, + + #[error("failed to verify message signature: {source}")] + SignatureVerificationFailure { + #[from] + source: ApiVerifierError, + }, } diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/lib.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/lib.rs index 4adc351471..738aeb6daa 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/lib.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/lib.rs @@ -17,6 +17,7 @@ mod msg; pub mod pending_events; pub mod reward_params; pub mod rewarding; +pub mod signing_types; mod types; pub use contracts_common::types::*; @@ -43,4 +44,5 @@ pub use pending_events::{ PendingIntervalEventData, PendingIntervalEventKind, }; pub use reward_params::{IntervalRewardParams, IntervalRewardingParamsUpdate, RewardingParams}; +pub use signing_types::*; pub use types::*; diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs index 1af718afde..8376259bfc 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs @@ -10,6 +10,7 @@ use crate::reward_params::{ }; use crate::{delegation, ContractStateParams, Layer, LayerAssignment, MixId, Percent}; use crate::{Gateway, IdentityKey, MixNode}; +use contracts_common::signing::MessageSignature; use cosmwasm_std::Decimal; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -153,12 +154,12 @@ pub enum ExecuteMsg { BondMixnode { mix_node: MixNode, cost_params: MixNodeCostParams, - owner_signature: String, + owner_signature: MessageSignature, }, BondMixnodeOnBehalf { mix_node: MixNode, cost_params: MixNodeCostParams, - owner_signature: String, + owner_signature: MessageSignature, owner: String, }, PledgeMore {}, @@ -187,12 +188,12 @@ pub enum ExecuteMsg { // gateway-related: BondGateway { gateway: Gateway, - owner_signature: String, + owner_signature: MessageSignature, }, BondGatewayOnBehalf { gateway: Gateway, owner: String, - owner_signature: String, + owner_signature: MessageSignature, }, UnbondGateway {}, UnbondGatewayOnBehalf { @@ -500,6 +501,11 @@ pub enum QueryMsg { start_after: Option, }, GetNumberOfPendingEvents {}, + + // signing-related + GetSigningNonce { + address: String, + }, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/signing.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/signing.rs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/signing_types.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/signing_types.rs new file mode 100644 index 0000000000..da4de4a9a9 --- /dev/null +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/signing_types.rs @@ -0,0 +1,151 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::{Gateway, MixNode, MixNodeCostParams}; +use contracts_common::signing::{ + ContractMessageContent, MessageType, Nonce, SignableMessage, SigningPurpose, +}; +use cosmwasm_std::{Addr, Coin}; +use serde::Serialize; + +pub type SignableMixNodeBondingMsg = SignableMessage>; +pub type SignableGatewayBondingMsg = SignableMessage>; + +#[derive(Serialize)] +pub struct MixnodeBondingPayload { + mix_node: MixNode, + cost_params: MixNodeCostParams, +} + +impl MixnodeBondingPayload { + pub fn new(mix_node: MixNode, cost_params: MixNodeCostParams) -> Self { + Self { + mix_node, + cost_params, + } + } +} + +impl SigningPurpose for MixnodeBondingPayload { + fn message_type() -> MessageType { + MessageType::new("mixnode-bonding") + } +} + +pub fn construct_mixnode_bonding_sign_payload( + nonce: Nonce, + sender: Addr, + proxy: Option, + pledge: Coin, + mix_node: MixNode, + cost_params: MixNodeCostParams, +) -> SignableMixNodeBondingMsg { + let payload = MixnodeBondingPayload::new(mix_node, cost_params); + let content = ContractMessageContent::new(sender, proxy, vec![pledge], payload); + + SignableMessage::new(nonce, content) +} + +#[derive(Serialize)] +pub struct GatewayBondingPayload { + gateway: Gateway, +} + +impl GatewayBondingPayload { + pub fn new(gateway: Gateway) -> Self { + Self { gateway } + } +} + +impl SigningPurpose for GatewayBondingPayload { + fn message_type() -> MessageType { + MessageType::new("gateway-bonding") + } +} + +pub fn construct_gateway_bonding_sign_payload( + nonce: Nonce, + sender: Addr, + proxy: Option, + pledge: Coin, + gateway: Gateway, +) -> SignableGatewayBondingMsg { + let payload = GatewayBondingPayload::new(gateway); + let content = ContractMessageContent::new(sender, proxy, vec![pledge], payload); + + SignableMessage::new(nonce, content) +} + +#[derive(Serialize)] +pub struct FamilyCreationSignature { + label: String, + // TODO: add any extra fields? +} + +impl FamilyCreationSignature { + pub fn new(label: String) -> Self { + Self { label } + } +} + +impl SigningPurpose for FamilyCreationSignature { + fn message_type() -> MessageType { + MessageType::new("family-creation") + } +} + +#[derive(Serialize)] +pub struct FamilyJoinSignature { + family_head: String, + // TODO: add any extra fields? +} + +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") + } +} + +// TODO: depending on our threat model, we should perhaps extend it to include all _on_behalf methods diff --git a/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs b/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs index 8df999f2a6..39b5e74187 100644 --- a/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs +++ b/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs @@ -1,3 +1,4 @@ +use contracts_common::signing::MessageSignature; use cosmwasm_std::{Coin, Timestamp}; use mixnet_contract_common::{ mixnode::{MixNodeConfigUpdate, MixNodeCostParams}, @@ -118,7 +119,7 @@ pub enum ExecuteMsg { BondMixnode { mix_node: MixNode, cost_params: MixNodeCostParams, - owner_signature: String, + owner_signature: MessageSignature, amount: Coin, }, PledgeMore { @@ -131,7 +132,7 @@ pub enum ExecuteMsg { }, BondGateway { gateway: Gateway, - owner_signature: String, + owner_signature: MessageSignature, amount: Coin, }, UnbondGateway {}, diff --git a/common/types/src/gateway.rs b/common/types/src/gateway.rs index d512ef5f30..f2a90d3e60 100644 --- a/common/types/src/gateway.rs +++ b/common/types/src/gateway.rs @@ -81,7 +81,6 @@ impl GatewayBond { pub struct GatewayNodeDetailsResponse { pub identity_key: String, pub sphinx_key: String, - pub owner_signature: String, pub announce_address: String, pub bind_address: String, pub version: String, @@ -94,7 +93,6 @@ impl fmt::Display for GatewayNodeDetailsResponse { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { writeln!(f, "Identity Key: {}", self.identity_key)?; writeln!(f, "Sphinx Key: {}", self.sphinx_key)?; - writeln!(f, "Owner Signature: {}", self.owner_signature)?; writeln!( f, "Host: {} (bind address: {})", diff --git a/common/types/src/mixnode.rs b/common/types/src/mixnode.rs index 4518700449..44ae234eac 100644 --- a/common/types/src/mixnode.rs +++ b/common/types/src/mixnode.rs @@ -167,7 +167,6 @@ impl MixNodeCostParams { pub struct MixnodeNodeDetailsResponse { pub identity_key: String, pub sphinx_key: String, - pub owner_signature: String, pub announce_address: String, pub bind_address: String, pub version: String, @@ -182,7 +181,6 @@ impl fmt::Display for MixnodeNodeDetailsResponse { let wallet_address = self.wallet_address.clone().unwrap_or_default(); writeln!(f, "Identity Key: {}", self.identity_key)?; writeln!(f, "Sphinx Key: {}", self.sphinx_key)?; - writeln!(f, "Owner Signature: {}", self.owner_signature)?; writeln!( f, "Host: {} (bind address: {})", diff --git a/contracts/mixnet/Cargo.toml b/contracts/mixnet/Cargo.toml index b75700efd1..c4c47c5d75 100644 --- a/contracts/mixnet/Cargo.toml +++ b/contracts/mixnet/Cargo.toml @@ -24,7 +24,7 @@ crate-type = ["cdylib", "rlib"] [dependencies] mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract", package = "nym-mixnet-contract-common", version = "0.2.0" } vesting-contract-common = { path = "../../common/cosmwasm-smart-contracts/vesting-contract", package = "nym-vesting-contract-common", version = "0.2.0" } -#nym-config = { path = "../../common/config"} +nym-contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common", version = "0.2.0" } cosmwasm-std = "1.0.0" cosmwasm-storage = "1.0.0" @@ -41,7 +41,6 @@ semver = { version = "1.0.16", default-features = false } [dev-dependencies] cosmwasm-schema = "1.0.0" rand_chacha = "0.2" -#rand = "0.7" nym-crypto = { path = "../../common/crypto", features = ["asymmetric", "rand"] } [build-dependencies] diff --git a/contracts/mixnet/src/constants.rs b/contracts/mixnet/src/constants.rs index 504544acaa..96698f7084 100644 --- a/contracts/mixnet/src/constants.rs +++ b/contracts/mixnet/src/constants.rs @@ -39,41 +39,43 @@ pub const FAMILIES_DEFAULT_RETRIEVAL_LIMIT: u32 = 10; pub const FAMILIES_MAX_RETRIEVAL_LIMIT: u32 = 20; // storage keys -pub(crate) const DELEGATION_PK_NAMESPACE: &str = "dl"; -pub(crate) const DELEGATION_OWNER_IDX_NAMESPACE: &str = "dlo"; -pub(crate) const DELEGATION_MIXNODE_IDX_NAMESPACE: &str = "dlm"; +pub const DELEGATION_PK_NAMESPACE: &str = "dl"; +pub const DELEGATION_OWNER_IDX_NAMESPACE: &str = "dlo"; +pub const DELEGATION_MIXNODE_IDX_NAMESPACE: &str = "dlm"; -pub(crate) const GATEWAYS_PK_NAMESPACE: &str = "gt"; -pub(crate) const GATEWAYS_OWNER_IDX_NAMESPACE: &str = "gto"; +pub const GATEWAYS_PK_NAMESPACE: &str = "gt"; +pub const GATEWAYS_OWNER_IDX_NAMESPACE: &str = "gto"; -pub(crate) const REWARDED_SET_KEY: &str = "rs"; -pub(crate) const CURRENT_EPOCH_STATUS_KEY: &str = "ces"; -pub(crate) const CURRENT_INTERVAL_KEY: &str = "ci"; -pub(crate) const EPOCH_EVENT_ID_COUNTER_KEY: &str = "eic"; -pub(crate) const INTERVAL_EVENT_ID_COUNTER_KEY: &str = "iic"; -pub(crate) const PENDING_EPOCH_EVENTS_NAMESPACE: &str = "pee"; -pub(crate) const PENDING_INTERVAL_EVENTS_NAMESPACE: &str = "pie"; +pub const REWARDED_SET_KEY: &str = "rs"; +pub const CURRENT_EPOCH_STATUS_KEY: &str = "ces"; +pub const CURRENT_INTERVAL_KEY: &str = "ci"; +pub const EPOCH_EVENT_ID_COUNTER_KEY: &str = "eic"; +pub const INTERVAL_EVENT_ID_COUNTER_KEY: &str = "iic"; +pub const PENDING_EPOCH_EVENTS_NAMESPACE: &str = "pee"; +pub const PENDING_INTERVAL_EVENTS_NAMESPACE: &str = "pie"; -pub(crate) const LAST_EPOCH_EVENT_ID_KEY: &str = "lee"; -pub(crate) const LAST_INTERVAL_EVENT_ID_KEY: &str = "lie"; +pub const LAST_EPOCH_EVENT_ID_KEY: &str = "lee"; +pub const LAST_INTERVAL_EVENT_ID_KEY: &str = "lie"; -pub(crate) const CONTRACT_STATE_KEY: &str = "state"; +pub const CONTRACT_STATE_KEY: &str = "state"; -pub(crate) const LAYER_DISTRIBUTION_KEY: &str = "layers"; -pub(crate) const NODE_ID_COUNTER_KEY: &str = "nic"; -pub(crate) const MIXNODES_PK_NAMESPACE: &str = "mnn"; -pub(crate) const MIXNODES_OWNER_IDX_NAMESPACE: &str = "mno"; -pub(crate) const MIXNODES_IDENTITY_IDX_NAMESPACE: &str = "mni"; -pub(crate) const MIXNODES_SPHINX_IDX_NAMESPACE: &str = "mns"; +pub const LAYER_DISTRIBUTION_KEY: &str = "layers"; +pub const NODE_ID_COUNTER_KEY: &str = "nic"; +pub const MIXNODES_PK_NAMESPACE: &str = "mnn"; +pub const MIXNODES_OWNER_IDX_NAMESPACE: &str = "mno"; +pub const MIXNODES_IDENTITY_IDX_NAMESPACE: &str = "mni"; +pub const MIXNODES_SPHINX_IDX_NAMESPACE: &str = "mns"; -pub(crate) const UNBONDED_MIXNODES_PK_NAMESPACE: &str = "ubm"; -pub(crate) const UNBONDED_MIXNODES_OWNER_IDX_NAMESPACE: &str = "umo"; -pub(crate) const UNBONDED_MIXNODES_IDENTITY_IDX_NAMESPACE: &str = "umi"; +pub const UNBONDED_MIXNODES_PK_NAMESPACE: &str = "ubm"; +pub const UNBONDED_MIXNODES_OWNER_IDX_NAMESPACE: &str = "umo"; +pub const UNBONDED_MIXNODES_IDENTITY_IDX_NAMESPACE: &str = "umi"; -pub(crate) const REWARDING_PARAMS_KEY: &str = "rparams"; -pub(crate) const PENDING_REWARD_POOL_KEY: &str = "prp"; -pub(crate) const MIXNODES_REWARDING_PK_NAMESPACE: &str = "mnr"; +pub const REWARDING_PARAMS_KEY: &str = "rparams"; +pub const PENDING_REWARD_POOL_KEY: &str = "prp"; +pub const MIXNODES_REWARDING_PK_NAMESPACE: &str = "mnr"; -pub(crate) const FAMILIES_INDEX_NAMESPACE: &str = "faml2"; -pub(crate) const FAMILIES_MAP_NAMESPACE: &str = "fam2"; -pub(crate) const MEMBERS_MAP_NAMESPACE: &str = "memb2"; +pub const FAMILIES_INDEX_NAMESPACE: &str = "faml2"; +pub const FAMILIES_MAP_NAMESPACE: &str = "fam2"; +pub const MEMBERS_MAP_NAMESPACE: &str = "memb2"; + +pub const SIGNING_NONCES_NAMESPACE: &str = "sn"; diff --git a/contracts/mixnet/src/contract.rs b/contracts/mixnet/src/contract.rs index 923b7614b5..623e9c0a1c 100644 --- a/contracts/mixnet/src/contract.rs +++ b/contracts/mixnet/src/contract.rs @@ -581,6 +581,9 @@ pub fn query( QueryMsg::GetNumberOfPendingEvents {} => to_binary( &crate::interval::queries::query_number_of_pending_events(deps)?, ), + QueryMsg::GetSigningNonce { address } => to_binary( + &crate::signing::queries::query_current_signing_nonce(deps, address)?, + ), }; Ok(query_res?) diff --git a/contracts/mixnet/src/families/transactions.rs b/contracts/mixnet/src/families/transactions.rs index e392b62dea..c1334fdb2a 100644 --- a/contracts/mixnet/src/families/transactions.rs +++ b/contracts/mixnet/src/families/transactions.rs @@ -305,71 +305,77 @@ mod test { use crate::families::queries::{get_family_by_head, get_family_by_label}; use crate::families::storage::is_family_member; use crate::mixnet_contract_settings::storage::minimum_mixnode_pledge; - use crate::support::tests::{fixtures, test_helpers}; - use cosmwasm_std::testing::{mock_env, mock_info}; + use crate::support::tests::fixtures; + use crate::support::tests::test_helpers::TestSetup; + use cosmwasm_std::testing::mock_info; #[test] fn test_family_crud() { - let mut deps = test_helpers::init_contract(); - let env = mock_env(); - let mut rng = test_helpers::test_rng(); + let mut test = TestSetup::new(); + let env = test.env(); let head = "alice"; let malicious_head = "timmy"; + let member = "bob"; - let minimum_pledge = minimum_mixnode_pledge(deps.as_ref().storage).unwrap(); - - let (head_mixnode, head_sig, head_keypair) = - test_helpers::mixnode_with_signature(&mut rng, head); - - let (malicious_mixnode, malicious_sig, _malicious_keypair) = - test_helpers::mixnode_with_signature(&mut rng, malicious_head); - + let minimum_pledge = minimum_mixnode_pledge(test.deps().storage).unwrap(); let cost_params = fixtures::mix_node_cost_params_fixture(); - let member = "bob"; - let (member_mixnode, member_sig, _) = - test_helpers::mixnode_with_signature(&mut rng, member); + let (head_mixnode, head_bond_sig, head_keypair) = test.mixnode_with_signature(head, None); + let (malicious_mixnode, malicious_bond_sig, malicious_keypair) = + test.mixnode_with_signature(malicious_head, None); + let (member_mixnode, member_bond_sig, member_keypair) = + test.mixnode_with_signature(member, None); - // we are informed that we didn't send enough funds crate::mixnodes::transactions::try_add_mixnode( - deps.as_mut(), + test.deps_mut(), env.clone(), mock_info(head, &[minimum_pledge.clone()]), head_mixnode.clone(), cost_params.clone(), - head_sig.clone(), + head_bond_sig, ) .unwrap(); crate::mixnodes::transactions::try_add_mixnode( - deps.as_mut(), + test.deps_mut(), env.clone(), mock_info(malicious_head, &[minimum_pledge.clone()]), malicious_mixnode, cost_params.clone(), - malicious_sig.clone(), + malicious_bond_sig, ) .unwrap(); crate::mixnodes::transactions::try_add_mixnode( - deps.as_mut(), + test.deps_mut(), env, mock_info(member, &[minimum_pledge]), member_mixnode.clone(), cost_params, - member_sig.clone(), + member_bond_sig, ) .unwrap(); - try_create_family(deps.as_mut(), mock_info(head, &[]), head_sig, "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(); let family_head = FamilyHead::new(&head_mixnode.identity_key); - assert!(get_family(&family_head, &deps.storage).is_ok()); + assert!(get_family(&family_head, test.deps().storage).is_ok()); let nope = try_create_family( - deps.as_mut(), + test.deps_mut(), mock_info(malicious_head, &[]), - malicious_sig, + old_style_malicious_head_sig, "test", ); @@ -381,11 +387,11 @@ mod test { }, } - let family = get_family_by_label("test", &deps.storage).unwrap(); + let family = get_family_by_label("test", 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(), &deps.storage).unwrap(); + let family = get_family_by_head(family_head.identity(), test.deps().storage).unwrap(); assert_eq!(family.head_identity(), family_head.identity()); let join_signature = head_keypair @@ -394,41 +400,47 @@ mod test { .to_base58_string(); try_join_family( - deps.as_mut(), + test.deps_mut(), mock_info(member, &[]), - Some(member_sig.clone()), + Some(old_style_member_sig.clone()), join_signature.clone(), head_mixnode.identity_key.clone(), ) .unwrap(); - let family = get_family(&family_head, &deps.storage).unwrap(); + let family = get_family(&family_head, test.deps().storage).unwrap(); - assert!(is_family_member(&deps.storage, &family, &member_mixnode.identity_key).unwrap()); + assert!( + is_family_member(test.deps().storage, &family, &member_mixnode.identity_key).unwrap() + ); try_leave_family( - deps.as_mut(), + test.deps_mut(), mock_info(member, &[]), - member_sig.clone(), + old_style_member_sig.clone(), head_mixnode.identity_key.clone(), ) .unwrap(); - let family = get_family(&family_head, &deps.storage).unwrap(); - assert!(!is_family_member(&deps.storage, &family, &member_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_join_family( - deps.as_mut(), + test.deps_mut(), mock_info(member, &[]), - Some(member_sig.clone()), - join_signature.clone(), - head_mixnode.identity_key.clone(), + Some(old_style_member_sig), + join_signature, + head_mixnode.identity_key, ) .unwrap(); - let family = get_family(&family_head, &deps.storage).unwrap(); + let family = get_family(&family_head, test.deps().storage).unwrap(); - assert!(is_family_member(&deps.storage, &family, &member_mixnode.identity_key).unwrap()); + assert!( + is_family_member(test.deps().storage, &family, &member_mixnode.identity_key).unwrap() + ); // try_head_kick_member( // deps.as_mut(), @@ -438,8 +450,8 @@ mod test { // ) // .unwrap(); - // let family = get_family(&family_head, &deps.storage).unwrap(); - // assert!(!is_family_member(&deps.storage, &family, &member_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()); } #[cfg(test)] @@ -456,7 +468,7 @@ mod test { let head = "alice"; - let (_, keypair) = test.add_dummy_mixnode_with_keypair(head, None); + let (_, keypair) = test.add_dummy_mixnode_with_proxy_and_keypair(head, None); let sig = keypair.private_key().sign_text(head); let res = try_create_family_on_behalf( @@ -495,7 +507,7 @@ 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_keypair(new_member, None); + let (_, member_keys) = test.add_dummy_mixnode_with_proxy_and_keypair(new_member, None); // TODO: those signatures are WRONG and have to be c hanged let join_signature = head_keys @@ -542,7 +554,7 @@ 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_keypair(new_member, None); + let (_, member_keys) = test.add_dummy_mixnode_with_proxy_and_keypair(new_member, None); // TODO: those signatures are WRONG and have to be changed let join_signature = head_keys @@ -599,7 +611,7 @@ 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_keypair(new_member, None); + let (_, member_keys) = test.add_dummy_mixnode_with_proxy_and_keypair(new_member, None); // TODO: those signatures are WRONG and have to be c hanged let join_signature = head_keys @@ -613,7 +625,7 @@ mod test { test.deps_mut(), mock_info(vesting_contract.as_ref(), &[]), new_member.to_string(), - Some(member_sig.clone()), + Some(member_sig), join_signature, head_identity, ) diff --git a/contracts/mixnet/src/gateways/mod.rs b/contracts/mixnet/src/gateways/mod.rs index 838867c264..d76ed62ced 100644 --- a/contracts/mixnet/src/gateways/mod.rs +++ b/contracts/mixnet/src/gateways/mod.rs @@ -2,5 +2,6 @@ // 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/gateways/queries.rs b/contracts/mixnet/src/gateways/queries.rs index cc58b58944..ab40766e57 100644 --- a/contracts/mixnet/src/gateways/queries.rs +++ b/contracts/mixnet/src/gateways/queries.rs @@ -62,6 +62,7 @@ pub(crate) mod tests { use crate::contract::execute; use crate::support::tests; use crate::support::tests::test_helpers; + use crate::support::tests::test_helpers::TestSetup; use cosmwasm_std::testing::{mock_env, mock_info}; #[test] @@ -73,27 +74,22 @@ pub(crate) mod tests { #[test] fn gateways_paged_retrieval_obeys_limits() { - let mut deps = test_helpers::init_contract(); - let env = mock_env(); - let mut rng = test_helpers::test_rng(); + let mut test = TestSetup::new(); + test.add_dummy_gateways(1000); let limit = 2; - test_helpers::add_dummy_gateways(&mut rng, deps.as_mut(), env, 1000); - let page1 = query_gateways_paged(deps.as_ref(), None, Option::from(limit)).unwrap(); + let page1 = query_gateways_paged(test.deps(), None, Option::from(limit)).unwrap(); assert_eq!(limit, page1.nodes.len() as u32); } #[test] fn gateways_paged_retrieval_has_default_limit() { - let mut deps = test_helpers::init_contract(); - let env = mock_env(); - let mut rng = test_helpers::test_rng(); - - test_helpers::add_dummy_gateways(&mut rng, deps.as_mut(), env, 1000); + let mut test = TestSetup::new(); + test.add_dummy_gateways(1000); // query without explicitly setting a limit - let page1 = query_gateways_paged(deps.as_ref(), None, None).unwrap(); + let page1 = query_gateways_paged(test.deps(), None, None).unwrap(); assert_eq!( GATEWAY_BOND_DEFAULT_RETRIEVAL_LIMIT, @@ -103,15 +99,12 @@ pub(crate) mod tests { #[test] fn gateways_paged_retrieval_has_max_limit() { - let mut deps = test_helpers::init_contract(); - let env = mock_env(); - let mut rng = test_helpers::test_rng(); - - test_helpers::add_dummy_gateways(&mut rng, deps.as_mut(), env, 1000); + let mut test = TestSetup::new(); + test.add_dummy_gateways(1000); // query with a crazily high limit in an attempt to use too many resources let crazy_limit = 1000 * GATEWAY_BOND_DEFAULT_RETRIEVAL_LIMIT; - let page1 = query_gateways_paged(deps.as_ref(), None, Option::from(crazy_limit)).unwrap(); + let page1 = query_gateways_paged(test.deps(), None, Option::from(crazy_limit)).unwrap(); // we default to a decent sized upper bound instead let expected_limit = GATEWAY_BOND_MAX_RETRIEVAL_LIMIT; @@ -121,25 +114,27 @@ pub(crate) mod tests { #[test] fn gateway_pagination_works() { let mut deps = test_helpers::init_contract(); - let _env = mock_env(); let mut rng = test_helpers::test_rng(); + let stake = tests::fixtures::good_gateway_pledge(); // prepare 4 messages and identities that are sorted by the generated identities // (because we query them in an ascended manner) let mut exec_data = (0..4) .map(|i| { let sender = format!("nym-addr{}", i); - let (msg, identity) = tests::messages::valid_bond_gateway_msg(&mut rng, &sender); + let (msg, identity) = tests::messages::valid_bond_gateway_msg( + &mut rng, + deps.as_ref(), + stake.clone(), + &sender, + ); (msg, (sender, identity)) }) .collect::>(); exec_data.sort_by(|(_, (_, id1)), (_, (_, id2))| id1.cmp(id2)); let (messages, sender_identities): (Vec<_>, Vec<_>) = exec_data.into_iter().unzip(); - let info = mock_info( - &sender_identities[0].0.clone(), - &tests::fixtures::good_gateway_pledge(), - ); + let info = mock_info(&sender_identities[0].0.clone(), &stake); execute(deps.as_mut(), mock_env(), info, messages[0].clone()).unwrap(); let per_page = 2; @@ -200,43 +195,29 @@ pub(crate) mod tests { #[test] fn query_for_gateway_owner_works() { - let mut deps = test_helpers::init_contract(); - let env = mock_env(); - let mut rng = test_helpers::test_rng(); + let mut test = TestSetup::new(); // "fred" does not own a mixnode if there are no mixnodes - let res = query_owned_gateway(deps.as_ref(), "fred".to_string()).unwrap(); + let res = query_owned_gateway(test.deps(), "fred".to_string()).unwrap(); assert!(res.gateway.is_none()); - // mixnode was added to "bob", "fred" still does not own one - test_helpers::add_gateway( - &mut rng, - deps.as_mut(), - env.clone(), - "bob", - tests::fixtures::good_gateway_pledge(), - ); + // gateway was added to "bob", "fred" still does not own one + test.add_dummy_gateway("bob", None); - let res = query_owned_gateway(deps.as_ref(), "fred".to_string()).unwrap(); + let res = query_owned_gateway(test.deps(), "fred".to_string()).unwrap(); assert!(res.gateway.is_none()); // "fred" now owns a gateway! - test_helpers::add_gateway( - &mut rng, - deps.as_mut(), - env, - "fred", - tests::fixtures::good_gateway_pledge(), - ); + test.add_dummy_gateway("fred", None); - let res = query_owned_gateway(deps.as_ref(), "fred".to_string()).unwrap(); + let res = query_owned_gateway(test.deps(), "fred".to_string()).unwrap(); assert!(res.gateway.is_some()); // but after unbonding it, he doesn't own one anymore - crate::gateways::transactions::try_remove_gateway(deps.as_mut(), mock_info("fred", &[])) + crate::gateways::transactions::try_remove_gateway(test.deps_mut(), mock_info("fred", &[])) .unwrap(); - let res = query_owned_gateway(deps.as_ref(), "fred".to_string()).unwrap(); + let res = query_owned_gateway(test.deps(), "fred".to_string()).unwrap(); assert!(res.gateway.is_none()); } } diff --git a/contracts/mixnet/src/gateways/signature_helpers.rs b/contracts/mixnet/src/gateways/signature_helpers.rs new file mode 100644 index 0000000000..efdb231e9e --- /dev/null +++ b/contracts/mixnet/src/gateways/signature_helpers.rs @@ -0,0 +1,34 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::signing::storage as signing_storage; +use cosmwasm_std::{Addr, Coin, Deps}; +use mixnet_contract_common::error::MixnetContractError; +use mixnet_contract_common::{construct_gateway_bonding_sign_payload, Gateway}; +use nym_contracts_common::signing::MessageSignature; +use nym_contracts_common::signing::Verifier; + +pub(crate) fn verify_gateway_bonding_signature( + deps: Deps<'_>, + sender: Addr, + proxy: Option, + pledge: Coin, + gateway: Gateway, + 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()))?; + + // reconstruct the payload + let nonce = signing_storage::get_signing_nonce(deps.storage, sender.clone())?; + let msg = construct_gateway_bonding_sign_payload(nonce, sender, proxy, pledge, gateway); + + if deps.api.verify_message(msg, signature, &public_key)? { + Ok(()) + } else { + Err(MixnetContractError::InvalidEd25519Signature) + } +} diff --git a/contracts/mixnet/src/gateways/transactions.rs b/contracts/mixnet/src/gateways/transactions.rs index 2078b0a180..dc32d6ce4d 100644 --- a/contracts/mixnet/src/gateways/transactions.rs +++ b/contracts/mixnet/src/gateways/transactions.rs @@ -2,15 +2,17 @@ // SPDX-License-Identifier: Apache-2.0 use super::storage; +use crate::gateways::signature_helpers::verify_gateway_bonding_signature; use crate::mixnet_contract_settings::storage as mixnet_params_storage; +use crate::signing::storage as signing_storage; use crate::support::helpers::{ - ensure_no_existing_bond, ensure_sent_by_vesting_contract, validate_node_identity_signature, - validate_pledge, + ensure_no_existing_bond, ensure_sent_by_vesting_contract, validate_pledge, }; use cosmwasm_std::{wasm_execute, Addr, BankMsg, Coin, DepsMut, Env, MessageInfo, Response}; use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::events::{new_gateway_bonding_event, new_gateway_unbonding_event}; use mixnet_contract_common::{Gateway, GatewayBond}; +use nym_contracts_common::signing::MessageSignature; use vesting_contract_common::messages::ExecuteMsg as VestingContractExecuteMsg; pub fn try_add_gateway( @@ -18,7 +20,7 @@ pub fn try_add_gateway( env: Env, info: MessageInfo, gateway: Gateway, - owner_signature: String, + owner_signature: MessageSignature, ) -> Result { _try_add_gateway( deps, @@ -37,7 +39,7 @@ pub fn try_add_gateway_on_behalf( info: MessageInfo, gateway: Gateway, owner: String, - owner_signature: String, + owner_signature: MessageSignature, ) -> Result { ensure_sent_by_vesting_contract(&info, deps.storage)?; @@ -54,13 +56,15 @@ pub fn try_add_gateway_on_behalf( ) } +// TODO: perhaps also require the user to explicitly provide what it thinks is the current nonce +// so that we could return a better error message if it doesn't match? pub(crate) fn _try_add_gateway( deps: DepsMut<'_>, env: Env, gateway: Gateway, pledge: Vec, owner: Addr, - owner_signature: String, + owner_signature: MessageSignature, proxy: Option, ) -> Result { // check if the pledge contains any funds of the appropriate denomination @@ -82,13 +86,18 @@ pub(crate) fn _try_add_gateway( } // check if this sender actually owns the gateway by checking the signature - validate_node_identity_signature( + verify_gateway_bonding_signature( deps.as_ref(), - &owner, - &owner_signature, - &gateway.identity_key, + owner.clone(), + proxy.clone(), + pledge.clone(), + gateway.clone(), + owner_signature, )?; + // update the signing nonce associated with this sender so that the future signature would be made on the new value + signing_storage::increment_signing_nonce(deps.storage, owner.clone())?; + let gateway_identity = gateway.identity_key.clone(); let bond = GatewayBond::new( pledge.clone(), @@ -182,39 +191,41 @@ pub(crate) fn _try_remove_gateway( #[cfg(test)] pub mod tests { + use super::*; use crate::contract::execute; + use crate::gateways::queries; use crate::gateways::transactions::{ try_add_gateway, try_add_gateway_on_behalf, try_remove_gateway_on_behalf, }; use crate::interval::pending_events; use crate::mixnet_contract_settings::storage::minimum_gateway_pledge; use crate::support::tests; - use crate::support::tests::fixtures::{good_gateway_pledge, TEST_COIN_DENOM}; + use crate::support::tests::fixtures; + use crate::support::tests::fixtures::{good_gateway_pledge, good_mixnode_pledge}; use crate::support::tests::test_helpers::TestSetup; - use crate::support::tests::{fixtures, test_helpers}; - use cosmwasm_std::testing::{mock_env, mock_info}; - use cosmwasm_std::{coin, Addr, BankMsg, Response, Uint128}; + use cosmwasm_std::testing::mock_info; + use cosmwasm_std::{Addr, BankMsg, Response, Uint128}; use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::events::new_gateway_unbonding_event; use mixnet_contract_common::ExecuteMsg; #[test] fn gateway_add() { - let mut deps = test_helpers::init_contract(); - let env = mock_env(); - let mut rng = test_helpers::test_rng(); + let mut test = TestSetup::new(); // if we fail validation (by say not sending enough funds let sender = "alice"; - let minimum_pledge = minimum_gateway_pledge(deps.as_ref().storage).unwrap(); + let minimum_pledge = minimum_gateway_pledge(test.deps().storage).unwrap(); let mut insufficient_pledge = minimum_pledge.clone(); insufficient_pledge.amount -= Uint128::new(1000); let info = mock_info(sender, &[insufficient_pledge.clone()]); - let (gateway, sig) = test_helpers::gateway_with_signature(&mut rng, sender); + let (gateway, sig) = + test.gateway_with_signature(sender, Some(vec![insufficient_pledge.clone()])); + let env = test.env(); let result = try_add_gateway( - deps.as_mut(), + test.deps_mut(), env.clone(), info, gateway.clone(), @@ -233,47 +244,23 @@ pub mod tests { // if the signature provided is invalid, the bonding also fails let info = mock_info(sender, &[minimum_pledge]); - let result = try_add_gateway( - deps.as_mut(), - env.clone(), - info.clone(), - gateway.clone(), - "bad-signature".into(), - ); - assert!(matches!( - result, - Err(MixnetContractError::MalformedEd25519Signature(..)) - )); - // if there was already a gateway bonded by particular user - test_helpers::add_gateway( - &mut rng, - deps.as_mut(), - env.clone(), - sender, - fixtures::good_gateway_pledge(), - ); + test.add_dummy_gateway(sender, None); // it fails - let result = try_add_gateway(deps.as_mut(), env.clone(), info, gateway, sig); + let result = try_add_gateway(test.deps_mut(), env.clone(), info, gateway, sig); assert_eq!(Err(MixnetContractError::AlreadyOwnsGateway), result); // the same holds if the user already owns a mixnode let sender2 = "mixnode-owner"; - let mix_id = test_helpers::add_mixnode( - &mut rng, - deps.as_mut(), - env.clone(), - sender2, - vec![coin(100_000_000, TEST_COIN_DENOM)], - ); + let mix_id = test.add_dummy_mixnode(sender2, None); let info = mock_info(sender2, &fixtures::good_gateway_pledge()); - let (gateway, sig) = test_helpers::gateway_with_signature(&mut rng, sender2); + let (gateway, sig) = test.gateway_with_signature(sender2, None); let result = try_add_gateway( - deps.as_mut(), + test.deps_mut(), env.clone(), info.clone(), gateway.clone(), @@ -282,12 +269,84 @@ pub mod tests { assert_eq!(Err(MixnetContractError::AlreadyOwnsMixnode), result); // but after he unbonds it, it's all fine again - pending_events::unbond_mixnode(deps.as_mut(), &env, 123, mix_id).unwrap(); + pending_events::unbond_mixnode(test.deps_mut(), &env, 123, mix_id).unwrap(); - let result = try_add_gateway(deps.as_mut(), env, info, gateway, sig); + let result = try_add_gateway(test.deps_mut(), env, info, gateway, sig); assert!(result.is_ok()); } + #[test] + fn adding_gateway_with_invalid_signatures() { + let mut test = TestSetup::new(); + let env = test.env(); + + let sender = "alice"; + let pledge = good_mixnode_pledge(); + let info = mock_info(sender, pledge.as_ref()); + + let (gateway, signature) = test.gateway_with_signature(sender, Some(pledge.clone())); + + // using different parameters than what the signature was made on + let mut modified_gateway = gateway.clone(); + modified_gateway.mix_port += 1; + let res = try_add_gateway( + test.deps_mut(), + env.clone(), + info, + modified_gateway, + signature.clone(), + ); + assert_eq!(res, Err(MixnetContractError::InvalidEd25519Signature)); + + // even stake amount is protected + let mut different_pledge = pledge.clone(); + different_pledge[0].amount += Uint128::new(12345); + + let info = mock_info(sender, different_pledge.as_ref()); + let res = try_add_gateway( + test.deps_mut(), + env.clone(), + info.clone(), + gateway.clone(), + signature.clone(), + ); + assert_eq!(res, Err(MixnetContractError::InvalidEd25519Signature)); + + let other_sender = mock_info("another-sender", pledge.as_ref()); + let res = try_add_gateway( + test.deps_mut(), + env.clone(), + other_sender, + gateway.clone(), + signature.clone(), + ); + assert_eq!(res, Err(MixnetContractError::InvalidEd25519Signature)); + + // trying to reuse the same signature for another bonding fails (because nonce doesn't match!) + let info = mock_info(sender, pledge.as_ref()); + let current_nonce = + signing_storage::get_signing_nonce(test.deps().storage, Addr::unchecked(sender)) + .unwrap(); + assert_eq!(0, current_nonce); + let res = try_add_gateway( + test.deps_mut(), + env.clone(), + info.clone(), + gateway.clone(), + signature.clone(), + ); + assert!(res.is_ok()); + let updated_nonce = + signing_storage::get_signing_nonce(test.deps().storage, Addr::unchecked(sender)) + .unwrap(); + assert_eq!(1, updated_nonce); + + _try_remove_gateway(test.deps_mut(), Addr::unchecked(sender), None).unwrap(); + + let res = try_add_gateway(test.deps_mut(), env, info, gateway, signature); + assert_eq!(res, Err(MixnetContractError::InvalidEd25519Signature)); + } + #[test] fn gateway_add_with_illegal_proxy() { let mut test = TestSetup::new(); @@ -297,7 +356,7 @@ pub mod tests { let vesting_contract = test.vesting_contract(); let owner = "alice"; - let (gateway, sig) = test_helpers::gateway_with_signature(&mut test.rng, owner); + let (gateway, sig) = test.gateway_with_signature(owner, None); let res = try_add_gateway_on_behalf( test.deps_mut(), @@ -320,14 +379,13 @@ pub mod tests { #[test] fn gateway_remove() { - let mut deps = test_helpers::init_contract(); - let mut rng = test_helpers::test_rng(); - let env = mock_env(); + let mut test = TestSetup::new(); + let env = test.env(); // try unbond when no nodes exist yet let info = mock_info("anyone", &[]); let msg = ExecuteMsg::UnbondGateway {}; - let result = execute(deps.as_mut(), mock_env(), info, msg); + let result = execute(test.deps_mut(), env.clone(), info, msg); // we're told that there is no node for our address assert_eq!( @@ -338,18 +396,12 @@ pub mod tests { ); // let's add a node owned by bob - test_helpers::add_gateway( - &mut rng, - deps.as_mut(), - env.clone(), - "bob", - fixtures::good_gateway_pledge(), - ); + test.add_dummy_gateway("bob", None); // attempt to unbond fred's node, which doesn't exist let info = mock_info("fred", &[]); let msg = ExecuteMsg::UnbondGateway {}; - let result = execute(deps.as_mut(), mock_env(), info, msg); + let result = execute(test.deps_mut(), env.clone(), info, msg); assert_eq!( result, Err(MixnetContractError::NoAssociatedGatewayBond { @@ -358,28 +410,27 @@ pub mod tests { ); // bob's node is still there - let nodes = tests::queries::get_gateways(&mut deps); + let nodes = queries::query_gateways_paged(test.deps(), None, None) + .unwrap() + .nodes; assert_eq!(1, nodes.len()); let first_node = &nodes[0]; assert_eq!(&Addr::unchecked("bob"), first_node.owner()); // add a node owned by fred - let fred_identity = test_helpers::add_gateway( - &mut rng, - deps.as_mut(), - env, - "fred", - tests::fixtures::good_gateway_pledge(), - ); + let fred_identity = test.add_dummy_gateway("fred", None); // let's make sure we now have 2 nodes: - assert_eq!(2, tests::queries::get_gateways(&mut deps).len()); + let nodes = queries::query_gateways_paged(test.deps(), None, None) + .unwrap() + .nodes; + assert_eq!(2, nodes.len()); // unbond fred's node let info = mock_info("fred", &[]); let msg = ExecuteMsg::UnbondGateway {}; - let remove_fred = execute(deps.as_mut(), mock_env(), info.clone(), msg).unwrap(); + let remove_fred = execute(test.deps_mut(), env, info.clone(), msg).unwrap(); // we should see a funds transfer from the contract back to fred let expected_message = BankMsg::Send { @@ -401,9 +452,11 @@ pub mod tests { assert_eq!(expected_response, remove_fred); // only 1 node now exists, owned by bob: - let gateway_bonds = tests::queries::get_gateways(&mut deps); - assert_eq!(1, gateway_bonds.len()); - assert_eq!(&Addr::unchecked("bob"), gateway_bonds[0].owner()); + let nodes = queries::query_gateways_paged(test.deps(), None, None) + .unwrap() + .nodes; + assert_eq!(1, nodes.len()); + assert_eq!(&Addr::unchecked("bob"), nodes[0].owner()); } #[test] diff --git a/contracts/mixnet/src/lib.rs b/contracts/mixnet/src/lib.rs index fb98e68fe0..2a78d3b1c7 100644 --- a/contracts/mixnet/src/lib.rs +++ b/contracts/mixnet/src/lib.rs @@ -14,6 +14,7 @@ mod mixnet_contract_settings; mod mixnodes; mod queued_migrations; mod rewards; +mod signing; mod support; #[cfg(feature = "contract-testing")] diff --git a/contracts/mixnet/src/mixnodes/mod.rs b/contracts/mixnet/src/mixnodes/mod.rs index f3a3c9941f..ebff4a6ace 100644 --- a/contracts/mixnet/src/mixnodes/mod.rs +++ b/contracts/mixnet/src/mixnodes/mod.rs @@ -3,5 +3,6 @@ pub mod helpers; pub mod queries; +pub mod signature_helpers; pub mod storage; pub mod transactions; diff --git a/contracts/mixnet/src/mixnodes/queries.rs b/contracts/mixnet/src/mixnodes/queries.rs index 5411b108ff..ba244d7c9f 100644 --- a/contracts/mixnet/src/mixnodes/queries.rs +++ b/contracts/mixnet/src/mixnodes/queries.rs @@ -273,30 +273,24 @@ pub(crate) mod tests { #[cfg(test)] mod mixnode_bonds { use super::*; - use crate::support::tests::fixtures::good_mixnode_pledge; #[test] fn obeys_limits() { - let mut deps = test_helpers::init_contract(); - let env = mock_env(); - let mut rng = test_helpers::test_rng(); + let mut test = TestSetup::new(); + test.add_dummy_mixnodes(1000); let limit = 2; - test_helpers::add_dummy_mixnodes(&mut rng, deps.as_mut(), env, 1000); - let page1 = query_mixnode_bonds_paged(deps.as_ref(), None, Some(limit)).unwrap(); + let page1 = query_mixnode_bonds_paged(test.deps(), None, Some(limit)).unwrap(); assert_eq!(limit, page1.nodes.len() as u32); } #[test] fn has_default_limit() { - let mut deps = test_helpers::init_contract(); - let env = mock_env(); - let mut rng = test_helpers::test_rng(); - - test_helpers::add_dummy_mixnodes(&mut rng, deps.as_mut(), env, 1000); + let mut test = TestSetup::new(); + test.add_dummy_mixnodes(1000); // query without explicitly setting a limit - let page1 = query_mixnode_bonds_paged(deps.as_ref(), None, None).unwrap(); + let page1 = query_mixnode_bonds_paged(test.deps(), None, None).unwrap(); assert_eq!( MIXNODE_BOND_DEFAULT_RETRIEVAL_LIMIT, @@ -306,14 +300,12 @@ pub(crate) mod tests { #[test] fn has_max_limit() { - let mut deps = test_helpers::init_contract(); - let env = mock_env(); - let mut rng = test_helpers::test_rng(); - test_helpers::add_dummy_mixnodes(&mut rng, deps.as_mut(), env, 1000); + let mut test = TestSetup::new(); + test.add_dummy_mixnodes(1000); // query with a crazily high limit in an attempt to use too many resources let crazy_limit = 1000; - let page1 = query_mixnode_bonds_paged(deps.as_ref(), None, Some(crazy_limit)).unwrap(); + let page1 = query_mixnode_bonds_paged(test.deps(), None, Some(crazy_limit)).unwrap(); // we default to a decent sized upper bound instead assert_eq!(MIXNODE_BOND_MAX_RETRIEVAL_LIMIT, page1.nodes.len() as u32); @@ -322,63 +314,43 @@ pub(crate) mod tests { #[test] fn pagination_works() { // as we add mixnodes, we're always inserting them in ascending manner due to monotonically increasing id - let mut deps = test_helpers::init_contract(); - let env = mock_env(); - let mut rng = test_helpers::test_rng(); + let mut test = TestSetup::new(); - test_helpers::add_mixnode( - &mut rng, - deps.as_mut(), - env.clone(), - "addr1", - good_mixnode_pledge(), - ); + test.add_dummy_mixnode("addr1", None); let per_page = 2; - let page1 = query_mixnode_bonds_paged(deps.as_ref(), None, Some(per_page)).unwrap(); + let page1 = query_mixnode_bonds_paged(test.deps(), None, Some(per_page)).unwrap(); // page should have 1 result on it assert_eq!(1, page1.nodes.len()); // save another - test_helpers::add_mixnode( - &mut rng, - deps.as_mut(), - env.clone(), - "addr2", - good_mixnode_pledge(), - ); + test.add_dummy_mixnode("addr2", None); // page1 should have 2 results on it - let page1 = query_mixnode_bonds_paged(deps.as_ref(), None, Some(per_page)).unwrap(); + let page1 = query_mixnode_bonds_paged(test.deps(), None, Some(per_page)).unwrap(); assert_eq!(2, page1.nodes.len()); - test_helpers::add_mixnode( - &mut rng, - deps.as_mut(), - env.clone(), - "addr3", - good_mixnode_pledge(), - ); + test.add_dummy_mixnode("addr3", None); // page1 still has the same 2 results let another_page1 = - query_mixnode_bonds_paged(deps.as_ref(), None, Some(per_page)).unwrap(); + query_mixnode_bonds_paged(test.deps(), None, Some(per_page)).unwrap(); assert_eq!(2, another_page1.nodes.len()); assert_eq!(page1, another_page1); // retrieving the next page should start after the last key on this page let start_after = page1.start_next_after.unwrap(); - let page2 = query_mixnode_bonds_paged(deps.as_ref(), Some(start_after), Some(per_page)) - .unwrap(); + let page2 = + query_mixnode_bonds_paged(test.deps(), Some(start_after), Some(per_page)).unwrap(); assert_eq!(1, page2.nodes.len()); // save another one - test_helpers::add_mixnode(&mut rng, deps.as_mut(), env, "addr4", good_mixnode_pledge()); + test.add_dummy_mixnode("addr4", None); - let page2 = query_mixnode_bonds_paged(deps.as_ref(), Some(start_after), Some(per_page)) - .unwrap(); + let page2 = + query_mixnode_bonds_paged(test.deps(), Some(start_after), Some(per_page)).unwrap(); // now we have 2 pages, with 2 results on the second page assert_eq!(2, page2.nodes.len()); @@ -388,29 +360,24 @@ pub(crate) mod tests { #[cfg(test)] mod mixnode_details { use super::*; - use crate::support::tests::fixtures::good_mixnode_pledge; #[test] fn obeys_limits() { - let mut deps = test_helpers::init_contract(); - let env = mock_env(); - let mut rng = test_helpers::test_rng(); + let mut test = TestSetup::new(); + test.add_dummy_mixnodes(1000); let limit = 2; - test_helpers::add_dummy_mixnodes(&mut rng, deps.as_mut(), env, 1000); - let page1 = query_mixnodes_details_paged(deps.as_ref(), None, Some(limit)).unwrap(); + let page1 = query_mixnodes_details_paged(test.deps(), None, Some(limit)).unwrap(); assert_eq!(limit, page1.nodes.len() as u32); } #[test] fn has_default_limit() { - let mut deps = test_helpers::init_contract(); - let env = mock_env(); - let mut rng = test_helpers::test_rng(); - test_helpers::add_dummy_mixnodes(&mut rng, deps.as_mut(), env, 1000); + let mut test = TestSetup::new(); + test.add_dummy_mixnodes(1000); // query without explicitly setting a limit - let page1 = query_mixnodes_details_paged(deps.as_ref(), None, None).unwrap(); + let page1 = query_mixnodes_details_paged(test.deps(), None, None).unwrap(); assert_eq!( MIXNODE_DETAILS_DEFAULT_RETRIEVAL_LIMIT, @@ -420,15 +387,12 @@ pub(crate) mod tests { #[test] fn has_max_limit() { - let mut deps = test_helpers::init_contract(); - let env = mock_env(); - let mut rng = test_helpers::test_rng(); - test_helpers::add_dummy_mixnodes(&mut rng, deps.as_mut(), env, 1000); + let mut test = TestSetup::new(); + test.add_dummy_mixnodes(1000); // query with a crazily high limit in an attempt to use too many resources let crazy_limit = 1000; - let page1 = - query_mixnodes_details_paged(deps.as_ref(), None, Some(crazy_limit)).unwrap(); + let page1 = query_mixnodes_details_paged(test.deps(), None, Some(crazy_limit)).unwrap(); // we default to a decent sized upper bound instead assert_eq!( @@ -440,64 +404,44 @@ pub(crate) mod tests { #[test] fn pagination_works() { // as we add mixnodes, we're always inserting them in ascending manner due to monotonically increasing id - let mut deps = test_helpers::init_contract(); - let env = mock_env(); - let mut rng = test_helpers::test_rng(); + let mut test = TestSetup::new(); - test_helpers::add_mixnode( - &mut rng, - deps.as_mut(), - env.clone(), - "addr1", - good_mixnode_pledge(), - ); + test.add_dummy_mixnode("addr1", None); let per_page = 2; - let page1 = query_mixnodes_details_paged(deps.as_ref(), None, Some(per_page)).unwrap(); + let page1 = query_mixnodes_details_paged(test.deps(), None, Some(per_page)).unwrap(); // page should have 1 result on it assert_eq!(1, page1.nodes.len()); // save another - test_helpers::add_mixnode( - &mut rng, - deps.as_mut(), - env.clone(), - "addr2", - good_mixnode_pledge(), - ); + test.add_dummy_mixnode("addr2", None); // page1 should have 2 results on it - let page1 = query_mixnodes_details_paged(deps.as_ref(), None, Some(per_page)).unwrap(); + let page1 = query_mixnodes_details_paged(test.deps(), None, Some(per_page)).unwrap(); assert_eq!(2, page1.nodes.len()); - test_helpers::add_mixnode( - &mut rng, - deps.as_mut(), - env.clone(), - "addr3", - good_mixnode_pledge(), - ); + test.add_dummy_mixnode("addr3", None); // page1 still has the same 2 results let another_page1 = - query_mixnodes_details_paged(deps.as_ref(), None, Some(per_page)).unwrap(); + query_mixnodes_details_paged(test.deps(), None, Some(per_page)).unwrap(); assert_eq!(2, another_page1.nodes.len()); assert_eq!(page1, another_page1); // retrieving the next page should start after the last key on this page let start_after = page1.start_next_after.unwrap(); let page2 = - query_mixnodes_details_paged(deps.as_ref(), Some(start_after), Some(per_page)) + query_mixnodes_details_paged(test.deps(), Some(start_after), Some(per_page)) .unwrap(); assert_eq!(1, page2.nodes.len()); // save another one - test_helpers::add_mixnode(&mut rng, deps.as_mut(), env, "addr4", good_mixnode_pledge()); + test.add_dummy_mixnode("addr4", None); let page2 = - query_mixnodes_details_paged(deps.as_ref(), Some(start_after), Some(per_page)) + query_mixnodes_details_paged(test.deps(), Some(start_after), Some(per_page)) .unwrap(); // now we have 2 pages, with 2 results on the second page @@ -1135,26 +1079,18 @@ pub(crate) mod tests { #[test] fn query_for_owned_mixnode() { - let mut deps = test_helpers::init_contract(); - let env = mock_env(); - let mut rng = test_helpers::test_rng(); + let mut test = TestSetup::new(); let address = "mix-owner".to_string(); // when it doesnt exist - let res = query_owned_mixnode(deps.as_ref(), address.clone()).unwrap(); + let res = query_owned_mixnode(test.deps(), address.clone()).unwrap(); assert!(res.mixnode_details.is_none()); assert_eq!(address, res.address); // when it [fully] exists - let id = test_helpers::add_mixnode( - &mut rng, - deps.as_mut(), - env, - &address, - good_mixnode_pledge(), - ); - let res = query_owned_mixnode(deps.as_ref(), address.clone()).unwrap(); + let id = test.add_dummy_mixnode(&address, None); + let res = query_owned_mixnode(test.deps(), address.clone()).unwrap(); let details = res.mixnode_details.unwrap(); assert_eq!(address, details.bond_information.owner); assert_eq!( @@ -1171,30 +1107,27 @@ pub(crate) mod tests { rewarding_details.delegates = Decimal::raw(12345); rewarding_details.unique_delegations = 1; rewards_storage::MIXNODE_REWARDING - .save(deps.as_mut().storage, id, &rewarding_details) + .save(test.deps_mut().storage, id, &rewarding_details) .unwrap(); - pending_events::unbond_mixnode(deps.as_mut(), &mock_env(), 123, id).unwrap(); - let res = query_owned_mixnode(deps.as_ref(), address.clone()).unwrap(); + pending_events::unbond_mixnode(test.deps_mut(), &mock_env(), 123, id).unwrap(); + let res = query_owned_mixnode(test.deps(), address.clone()).unwrap(); assert!(res.mixnode_details.is_none()); assert_eq!(address, res.address); } #[test] fn query_for_mixnode_details() { - let mut deps = test_helpers::init_contract(); - let env = mock_env(); - let mut rng = test_helpers::test_rng(); + let mut test = TestSetup::new(); // no node under this id - let res = query_mixnode_details(deps.as_ref(), 42).unwrap(); + let res = query_mixnode_details(test.deps(), 42).unwrap(); assert!(res.mixnode_details.is_none()); assert_eq!(42, res.mix_id); // it exists - let mix_id = - test_helpers::add_mixnode(&mut rng, deps.as_mut(), env, "foomp", good_mixnode_pledge()); - let res = query_mixnode_details(deps.as_ref(), mix_id).unwrap(); + let mix_id = test.add_dummy_mixnode("foomp", None); + let res = query_mixnode_details(test.deps(), mix_id).unwrap(); let details = res.mixnode_details.unwrap(); assert_eq!(mix_id, details.bond_information.mix_id); assert_eq!( @@ -1227,18 +1160,15 @@ pub(crate) mod tests { #[test] fn query_for_mixnode_rewarding_details() { - let mut deps = test_helpers::init_contract(); - let env = mock_env(); - let mut rng = test_helpers::test_rng(); + let mut test = TestSetup::new(); // no node under this id - let res = query_mixnode_rewarding_details(deps.as_ref(), 42).unwrap(); + let res = query_mixnode_rewarding_details(test.deps(), 42).unwrap(); assert!(res.rewarding_details.is_none()); assert_eq!(42, res.mix_id); - let mix_id = - test_helpers::add_mixnode(&mut rng, deps.as_mut(), env, "foomp", good_mixnode_pledge()); - let res = query_mixnode_rewarding_details(deps.as_ref(), mix_id).unwrap(); + let mix_id = test.add_dummy_mixnode("foomp", None); + let res = query_mixnode_rewarding_details(test.deps(), mix_id).unwrap(); let details = res.rewarding_details.unwrap(); assert_eq!( fixtures::mix_node_cost_params_fixture(), @@ -1249,80 +1179,74 @@ pub(crate) mod tests { #[test] fn query_for_unbonded_mixnode() { - let mut deps = test_helpers::init_contract(); - let env = mock_env(); - let mut rng = test_helpers::test_rng(); + let mut test = TestSetup::new(); let sender = "mix-owner"; // no node under this id - let res = query_unbonded_mixnode(deps.as_ref(), 42).unwrap(); + let res = query_unbonded_mixnode(test.deps(), 42).unwrap(); assert!(res.unbonded_info.is_none()); assert_eq!(42, res.mix_id); // add and unbond the mixnode - let mix_id = - test_helpers::add_mixnode(&mut rng, deps.as_mut(), env, sender, good_mixnode_pledge()); - pending_events::unbond_mixnode(deps.as_mut(), &mock_env(), 123, mix_id).unwrap(); + let mix_id = test.add_dummy_mixnode(sender, None); + pending_events::unbond_mixnode(test.deps_mut(), &mock_env(), 123, mix_id).unwrap(); - let res = query_unbonded_mixnode(deps.as_ref(), mix_id).unwrap(); + let res = query_unbonded_mixnode(test.deps(), mix_id).unwrap(); assert_eq!(res.unbonded_info.unwrap().owner, sender); assert_eq!(mix_id, res.mix_id); } #[test] fn query_for_stake_saturation() { - let mut deps = test_helpers::init_contract(); - let env = mock_env(); - let mut rng = test_helpers::test_rng(); + let mut test = TestSetup::new(); // no node under this id - let res = query_stake_saturation(deps.as_ref(), 42).unwrap(); + let res = query_stake_saturation(test.deps(), 42).unwrap(); assert!(res.current_saturation.is_none()); assert!(res.uncapped_saturation.is_none()); assert_eq!(42, res.mix_id); let rewarding_params = rewards_storage::REWARDING_PARAMS - .load(deps.as_ref().storage) + .load(test.deps().storage) .unwrap(); let saturation_point = rewarding_params.interval.stake_saturation_point; - let mix_id = - test_helpers::add_mixnode(&mut rng, deps.as_mut(), env, "foomp", good_mixnode_pledge()); + let mix_id = test.add_dummy_mixnode("foomp", None); // below saturation point // there's only the base pledge without any delegation let expected = Decimal::from_atomics(good_mixnode_pledge()[0].amount, 0).unwrap() / saturation_point; - let res = query_stake_saturation(deps.as_ref(), mix_id).unwrap(); + let res = query_stake_saturation(test.deps(), mix_id).unwrap(); assert_eq!(expected, res.current_saturation.unwrap()); assert_eq!(expected, res.uncapped_saturation.unwrap()); assert_eq!(mix_id, res.mix_id); // exactly at saturation point let mut mix_rewarding = rewards_storage::MIXNODE_REWARDING - .load(deps.as_ref().storage, mix_id) + .load(test.deps().storage, mix_id) .unwrap(); mix_rewarding.operator = saturation_point; rewards_storage::MIXNODE_REWARDING - .save(deps.as_mut().storage, mix_id, &mix_rewarding) + .save(test.deps_mut().storage, mix_id, &mix_rewarding) .unwrap(); - let res = query_stake_saturation(deps.as_ref(), mix_id).unwrap(); + let res = query_stake_saturation(test.deps(), mix_id).unwrap(); assert_eq!(Decimal::one(), res.current_saturation.unwrap()); assert_eq!(Decimal::one(), res.uncapped_saturation.unwrap()); assert_eq!(mix_id, res.mix_id); // above the saturation point let mut mix_rewarding = rewards_storage::MIXNODE_REWARDING - .load(deps.as_ref().storage, mix_id) + .load(test.deps().storage, mix_id) .unwrap(); mix_rewarding.delegates = mix_rewarding.operator * Decimal::percent(150); rewards_storage::MIXNODE_REWARDING - .save(deps.as_mut().storage, mix_id, &mix_rewarding) + .save(test.deps_mut().storage, mix_id, &mix_rewarding) .unwrap(); - let res = query_stake_saturation(deps.as_ref(), mix_id).unwrap(); + let res = query_stake_saturation(test.deps(), mix_id).unwrap(); assert_eq!(Decimal::one(), res.current_saturation.unwrap()); assert_eq!(Decimal::percent(250), res.uncapped_saturation.unwrap()); assert_eq!(mix_id, res.mix_id); diff --git a/contracts/mixnet/src/mixnodes/signature_helpers.rs b/contracts/mixnet/src/mixnodes/signature_helpers.rs new file mode 100644 index 0000000000..24d87eb2e9 --- /dev/null +++ b/contracts/mixnet/src/mixnodes/signature_helpers.rs @@ -0,0 +1,36 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::signing::storage as signing_storage; +use cosmwasm_std::{Addr, Coin, Deps}; +use mixnet_contract_common::error::MixnetContractError; +use mixnet_contract_common::{construct_mixnode_bonding_sign_payload, MixNode, MixNodeCostParams}; +use nym_contracts_common::signing::MessageSignature; +use nym_contracts_common::signing::Verifier; + +pub(crate) fn verify_mixnode_bonding_signature( + deps: Deps<'_>, + sender: Addr, + proxy: Option, + pledge: Coin, + mixnode: MixNode, + cost_params: MixNodeCostParams, + 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()))?; + + // reconstruct the payload + let nonce = signing_storage::get_signing_nonce(deps.storage, sender.clone())?; + let msg = + construct_mixnode_bonding_sign_payload(nonce, sender, proxy, pledge, mixnode, cost_params); + + if deps.api.verify_message(msg, signature, &public_key)? { + Ok(()) + } else { + Err(MixnetContractError::InvalidEd25519Signature) + } +} diff --git a/contracts/mixnet/src/mixnodes/transactions.rs b/contracts/mixnet/src/mixnodes/transactions.rs index d31dbbf13e..6ce2416851 100644 --- a/contracts/mixnet/src/mixnodes/transactions.rs +++ b/contracts/mixnet/src/mixnodes/transactions.rs @@ -9,10 +9,11 @@ use crate::mixnet_contract_settings::storage::rewarding_denom; use crate::mixnodes::helpers::{ get_mixnode_details_by_owner, must_get_mixnode_bond_by_owner, save_new_mixnode, }; +use crate::mixnodes::signature_helpers::verify_mixnode_bonding_signature; +use crate::signing::storage as signing_storage; use crate::support::helpers::{ ensure_bonded, ensure_epoch_in_progress_state, ensure_is_authorized, ensure_no_existing_bond, - ensure_proxy_match, ensure_sent_by_vesting_contract, validate_node_identity_signature, - validate_pledge, + ensure_proxy_match, ensure_sent_by_vesting_contract, validate_pledge, }; use cosmwasm_std::{coin, Addr, Coin, DepsMut, Env, MessageInfo, Response, Storage}; use mixnet_contract_common::error::MixnetContractError; @@ -24,6 +25,7 @@ use mixnet_contract_common::events::{ use mixnet_contract_common::mixnode::{MixNodeConfigUpdate, MixNodeCostParams}; use mixnet_contract_common::pending_events::{PendingEpochEventKind, PendingIntervalEventKind}; use mixnet_contract_common::{Layer, MixId, MixNode}; +use nym_contracts_common::signing::MessageSignature; pub(crate) fn update_mixnode_layer( mix_id: MixId, @@ -61,7 +63,7 @@ pub fn try_add_mixnode( info: MessageInfo, mix_node: MixNode, cost_params: MixNodeCostParams, - owner_signature: String, + owner_signature: MessageSignature, ) -> Result { _try_add_mixnode( deps, @@ -82,7 +84,7 @@ pub fn try_add_mixnode_on_behalf( mix_node: MixNode, cost_params: MixNodeCostParams, owner: String, - owner_signature: String, + owner_signature: MessageSignature, ) -> Result { ensure_sent_by_vesting_contract(&info, deps.storage)?; @@ -101,6 +103,9 @@ pub fn try_add_mixnode_on_behalf( } // I'm not entirely sure how to deal with this warning at the current moment +// +// TODO: perhaps also require the user to explicitly provide what it thinks is the current nonce +// so that we could return a better error message if it doesn't match? #[allow(clippy::too_many_arguments)] fn _try_add_mixnode( deps: DepsMut<'_>, @@ -109,7 +114,7 @@ fn _try_add_mixnode( cost_params: MixNodeCostParams, pledge: Vec, owner: Addr, - owner_signature: String, + owner_signature: MessageSignature, proxy: Option, ) -> Result { // check if the pledge contains any funds of the appropriate denomination @@ -126,13 +131,19 @@ fn _try_add_mixnode( // the bond information due to `UniqueIndex` constraint defined on those fields. // check if this sender actually owns the mixnode by checking the signature - validate_node_identity_signature( + verify_mixnode_bonding_signature( deps.as_ref(), - &owner, - &owner_signature, - &mixnode.identity_key, + owner.clone(), + proxy.clone(), + pledge.clone(), + mixnode.clone(), + cost_params.clone(), + owner_signature, )?; + // update the signing nonce associated with this sender so that the future signature would be made on the new value + signing_storage::increment_signing_nonce(deps.storage, owner.clone())?; + let node_identity = mixnode.identity_key.clone(); let (node_id, layer) = save_new_mixnode( deps.storage, @@ -393,7 +404,7 @@ pub mod tests { use crate::support::tests::fixtures::{good_mixnode_pledge, TEST_COIN_DENOM}; use crate::support::tests::test_helpers::TestSetup; use crate::support::tests::{fixtures, test_helpers}; - use cosmwasm_std::testing::{mock_env, mock_info}; + use cosmwasm_std::testing::mock_info; use cosmwasm_std::{Order, StdResult, Uint128}; use mixnet_contract_common::{ EpochState, EpochStatus, ExecuteMsg, Layer, LayerDistribution, Percent, @@ -401,23 +412,23 @@ pub mod tests { #[test] fn mixnode_add() { - let mut deps = test_helpers::init_contract(); - let env = mock_env(); - let mut rng = test_helpers::test_rng(); + let mut test = TestSetup::new(); + let env = test.env(); let sender = "alice"; - let minimum_pledge = minimum_mixnode_pledge(deps.as_ref().storage).unwrap(); + let minimum_pledge = minimum_mixnode_pledge(test.deps().storage).unwrap(); let mut insufficient_pledge = minimum_pledge.clone(); insufficient_pledge.amount -= Uint128::new(1000); // if we don't send enough funds let info = mock_info(sender, &[insufficient_pledge.clone()]); - let (mixnode, sig, _) = test_helpers::mixnode_with_signature(&mut rng, sender); + let (mixnode, sig, _) = + test.mixnode_with_signature(sender, Some(vec![insufficient_pledge.clone()])); let cost_params = fixtures::mix_node_cost_params_fixture(); // we are informed that we didn't send enough funds let result = try_add_mixnode( - deps.as_mut(), + test.deps_mut(), env.clone(), info, mixnode.clone(), @@ -435,31 +446,12 @@ pub mod tests { // if the signature provided is invalid, the bonding also fails let info = mock_info(sender, &[minimum_pledge]); - let result = try_add_mixnode( - deps.as_mut(), - env.clone(), - info.clone(), - mixnode.clone(), - cost_params.clone(), - "bad-signature".into(), - ); - assert!(matches!( - result, - Err(MixnetContractError::MalformedEd25519Signature(..)) - )); - // if there was already a mixnode bonded by particular user - test_helpers::add_mixnode( - &mut rng, - deps.as_mut(), - env.clone(), - sender, - fixtures::good_mixnode_pledge(), - ); + test.add_dummy_mixnode(sender, None); // it fails let result = try_add_mixnode( - deps.as_mut(), + test.deps_mut(), env.clone(), info, mixnode, @@ -471,19 +463,13 @@ pub mod tests { // the same holds if the user already owns a gateway let sender2 = "gateway-owner"; - test_helpers::add_gateway( - &mut rng, - deps.as_mut(), - env.clone(), - sender2, - tests::fixtures::good_gateway_pledge(), - ); + test.add_dummy_gateway(sender2, None); let info = mock_info(sender2, &tests::fixtures::good_mixnode_pledge()); - let (mixnode, sig, _) = test_helpers::mixnode_with_signature(&mut rng, sender2); + let (mixnode, sig, _) = test.mixnode_with_signature(sender2, None); let result = try_add_mixnode( - deps.as_mut(), + test.deps_mut(), env.clone(), info.clone(), mixnode.clone(), @@ -494,14 +480,14 @@ pub mod tests { // but after he unbonds it, it's all fine again let msg = ExecuteMsg::UnbondGateway {}; - execute(deps.as_mut(), mock_env(), info.clone(), msg).unwrap(); + execute(test.deps_mut(), env.clone(), info.clone(), msg).unwrap(); - let result = try_add_mixnode(deps.as_mut(), env, info, mixnode, cost_params, sig); + let result = try_add_mixnode(test.deps_mut(), env, info, mixnode, cost_params, sig); assert!(result.is_ok()); // make sure we got assigned the next id (note: we have already bonded a mixnode before in this test) - let bond = must_get_mixnode_bond_by_owner(deps.as_ref().storage, &Addr::unchecked(sender2)) - .unwrap(); + let bond = + must_get_mixnode_bond_by_owner(test.deps().storage, &Addr::unchecked(sender2)).unwrap(); assert_eq!(2, bond.mix_id); // and make sure we're on layer 2 (because it was the next empty one) @@ -513,10 +499,84 @@ pub mod tests { layer2: 1, layer3: 0, }; - assert_eq!( - expected, - storage::LAYERS.load(deps.as_ref().storage).unwrap() - ) + assert_eq!(expected, storage::LAYERS.load(test.deps().storage).unwrap()) + } + + #[test] + fn adding_mixnode_with_invalid_signatures() { + let mut test = TestSetup::new(); + let env = test.env(); + + let sender = "alice"; + let pledge = good_mixnode_pledge(); + let info = mock_info(sender, pledge.as_ref()); + + let (mixnode, signature, _) = test.mixnode_with_signature(sender, Some(pledge.clone())); + // the above using cost params fixture + let cost_params = fixtures::mix_node_cost_params_fixture(); + + // using different parameters than what the signature was made on + let mut modified_mixnode = mixnode.clone(); + modified_mixnode.mix_port += 1; + let res = try_add_mixnode( + test.deps_mut(), + env.clone(), + info, + modified_mixnode, + cost_params.clone(), + signature.clone(), + ); + assert_eq!(res, Err(MixnetContractError::InvalidEd25519Signature)); + + // even stake amount is protected + let mut different_pledge = pledge.clone(); + different_pledge[0].amount += Uint128::new(12345); + + let info = mock_info(sender, different_pledge.as_ref()); + let res = try_add_mixnode( + test.deps_mut(), + env.clone(), + info.clone(), + mixnode.clone(), + cost_params.clone(), + signature.clone(), + ); + assert_eq!(res, Err(MixnetContractError::InvalidEd25519Signature)); + + let other_sender = mock_info("another-sender", pledge.as_ref()); + let res = try_add_mixnode( + test.deps_mut(), + env.clone(), + other_sender, + mixnode.clone(), + cost_params.clone(), + signature.clone(), + ); + assert_eq!(res, Err(MixnetContractError::InvalidEd25519Signature)); + + // trying to reuse the same signature for another bonding fails (because nonce doesn't match!) + let info = mock_info(sender, pledge.as_ref()); + let current_nonce = + signing_storage::get_signing_nonce(test.deps().storage, Addr::unchecked(sender)) + .unwrap(); + assert_eq!(0, current_nonce); + let res = try_add_mixnode( + test.deps_mut(), + env.clone(), + info.clone(), + mixnode.clone(), + cost_params.clone(), + signature.clone(), + ); + assert!(res.is_ok()); + let updated_nonce = + signing_storage::get_signing_nonce(test.deps().storage, Addr::unchecked(sender)) + .unwrap(); + assert_eq!(1, updated_nonce); + + test.immediately_unbond_mixnode(1); + let res = try_add_mixnode(test.deps_mut(), env, info, mixnode, cost_params, signature); + assert_eq!(res, Err(MixnetContractError::InvalidEd25519Signature)); } #[test] @@ -528,10 +588,9 @@ pub mod tests { let vesting_contract = test.vesting_contract(); let owner = "alice"; - let (mixnode, sig, _) = test_helpers::mixnode_with_signature(&mut test.rng, owner); + let (mixnode, sig, _) = test.mixnode_with_signature(owner, None); let cost_params = fixtures::mix_node_cost_params_fixture(); - // we are informed that we didn't send enough funds let res = try_add_mixnode_on_behalf( test.deps_mut(), env, @@ -920,52 +979,53 @@ pub mod tests { #[test] fn adding_mixnode_with_duplicate_sphinx_key_errors_out() { - let mut deps = test_helpers::init_contract(); - let mut rng = test_helpers::test_rng(); + let mut test = TestSetup::new(); + let env = test.env(); - let keypair1 = nym_crypto::asymmetric::identity::KeyPair::new(&mut rng); - let keypair2 = nym_crypto::asymmetric::identity::KeyPair::new(&mut rng); - let sig1 = keypair1.private_key().sign_text("alice"); - let sig2 = keypair1.private_key().sign_text("bob"); + let keypair1 = nym_crypto::asymmetric::identity::KeyPair::new(&mut test.rng); + let keypair2 = nym_crypto::asymmetric::identity::KeyPair::new(&mut test.rng); - let info_alice = mock_info("alice", &tests::fixtures::good_mixnode_pledge()); - let info_bob = mock_info("bob", &tests::fixtures::good_mixnode_pledge()); - - let mut mixnode = MixNode { + let cost_params = fixtures::mix_node_cost_params_fixture(); + let mixnode1 = MixNode { host: "1.2.3.4".to_string(), mix_port: 1234, verloc_port: 1234, http_api_port: 1234, - sphinx_key: nym_crypto::asymmetric::encryption::KeyPair::new(&mut rng) + sphinx_key: nym_crypto::asymmetric::encryption::KeyPair::new(&mut test.rng) .public_key() .to_base58_string(), identity_key: keypair1.public_key().to_base58_string(), version: "v0.1.2.3".to_string(), }; - let cost_params = fixtures::mix_node_cost_params_fixture(); + + // change identity but reuse sphinx key + let mut mixnode2 = mixnode1.clone(); + mixnode2.sphinx_key = nym_crypto::asymmetric::encryption::KeyPair::new(&mut test.rng) + .public_key() + .to_base58_string(); + + let sig1 = + test.mixnode_bonding_signature(keypair1.private_key(), "alice", mixnode1.clone(), None); + let sig2 = + test.mixnode_bonding_signature(keypair2.private_key(), "bob", mixnode2.clone(), None); + + let info_alice = mock_info("alice", &tests::fixtures::good_mixnode_pledge()); + let info_bob = mock_info("bob", &tests::fixtures::good_mixnode_pledge()); assert!(try_add_mixnode( - deps.as_mut(), - mock_env(), + test.deps_mut(), + env.clone(), info_alice, - mixnode.clone(), + mixnode1, cost_params.clone(), sig1 ) .is_ok()); - mixnode.identity_key = keypair2.public_key().to_base58_string(); - // change identity but reuse sphinx key - assert!(try_add_mixnode( - deps.as_mut(), - mock_env(), - info_bob, - mixnode, - cost_params, - sig2 - ) - .is_err()); + assert!( + try_add_mixnode(test.deps_mut(), env, info_bob, mixnode2, cost_params, sig2).is_err() + ); } #[cfg(test)] diff --git a/contracts/mixnet/src/signing/mod.rs b/contracts/mixnet/src/signing/mod.rs new file mode 100644 index 0000000000..fc7eededb1 --- /dev/null +++ b/contracts/mixnet/src/signing/mod.rs @@ -0,0 +1,5 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod queries; +pub mod storage; diff --git a/contracts/mixnet/src/signing/queries.rs b/contracts/mixnet/src/signing/queries.rs new file mode 100644 index 0000000000..72ca4aed1c --- /dev/null +++ b/contracts/mixnet/src/signing/queries.rs @@ -0,0 +1,11 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::signing::storage::get_signing_nonce; +use cosmwasm_std::{Deps, StdResult}; +use nym_contracts_common::signing::Nonce; + +pub fn query_current_signing_nonce(deps: Deps<'_>, address: String) -> StdResult { + let address = deps.api.addr_validate(&address)?; + get_signing_nonce(deps.storage, address) +} diff --git a/contracts/mixnet/src/signing/storage.rs b/contracts/mixnet/src/signing/storage.rs new file mode 100644 index 0000000000..0e7975a37a --- /dev/null +++ b/contracts/mixnet/src/signing/storage.rs @@ -0,0 +1,30 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::constants::SIGNING_NONCES_NAMESPACE; +use cosmwasm_std::{Addr, StdResult, Storage}; +use cw_storage_plus::Map; +use nym_contracts_common::signing::Nonce; + +pub const NONCES: Map<'_, Addr, Nonce> = Map::new(SIGNING_NONCES_NAMESPACE); + +pub fn get_signing_nonce(storage: &dyn Storage, address: Addr) -> StdResult { + let nonce = NONCES.may_load(storage, address)?.unwrap_or(0); + Ok(nonce) +} + +pub fn update_signing_nonce( + storage: &mut dyn Storage, + address: Addr, + value: Nonce, +) -> StdResult<()> { + NONCES.save(storage, address, &value) +} + +pub fn increment_signing_nonce(storage: &mut dyn Storage, address: Addr) -> StdResult<()> { + // get the current nonce + let nonce = get_signing_nonce(storage, address.clone())?; + + // increment it for the next use + update_signing_nonce(storage, address, nonce + 1) +} diff --git a/contracts/mixnet/src/support/helpers.rs b/contracts/mixnet/src/support/helpers.rs index fe13150ec3..e2d1c36ddd 100644 --- a/contracts/mixnet/src/support/helpers.rs +++ b/contracts/mixnet/src/support/helpers.rs @@ -377,7 +377,7 @@ pub fn validate_node_identity_signature( signature: &str, identity: IdentityKeyRef<'_>, ) -> Result<(), MixnetContractError> { - validate_signature(deps, owner.as_bytes(), signature, identity) + validate_ed25519_signature(deps, owner.as_bytes(), signature, identity) } pub fn validate_family_signature( @@ -386,10 +386,10 @@ pub fn validate_family_signature( signature: &str, family_head: IdentityKeyRef<'_>, ) -> Result<(), MixnetContractError> { - validate_signature(deps, family_member.as_bytes(), signature, family_head) + validate_ed25519_signature(deps, family_member.as_bytes(), signature, family_head) } -pub(crate) fn validate_signature( +pub(crate) fn validate_ed25519_signature( deps: Deps<'_>, signed_bytes: &[u8], signature: &str, diff --git a/contracts/mixnet/src/support/tests/messages.rs b/contracts/mixnet/src/support/tests/messages.rs index bf9ed53895..a1bf5ab37e 100644 --- a/contracts/mixnet/src/support/tests/messages.rs +++ b/contracts/mixnet/src/support/tests/messages.rs @@ -1,25 +1,34 @@ +use cosmwasm_std::{Coin, Deps}; use mixnet_contract_common::{ExecuteMsg, Gateway, IdentityKey}; +use nym_crypto::asymmetric::identity; use rand_chacha::rand_core::{CryptoRng, RngCore}; use crate::support::tests; +use crate::support::tests::test_helpers::{ed25519_sign_message, gateway_bonding_sign_payload}; pub(crate) fn valid_bond_gateway_msg( mut rng: impl RngCore + CryptoRng, + deps: Deps<'_>, + stake: Vec, sender: &str, ) -> (ExecuteMsg, IdentityKey) { - let keypair = nym_crypto::asymmetric::identity::KeyPair::new(&mut rng); - let owner_signature = keypair - .private_key() - .sign(sender.as_bytes()) - .to_base58_string(); + let keypair = identity::KeyPair::new(&mut rng); + let identity_key = keypair.public_key().to_base58_string(); + let legit_sphinx_keys = nym_crypto::asymmetric::encryption::KeyPair::new(&mut rng); + + let gateway = Gateway { + identity_key, + sphinx_key: legit_sphinx_keys.public_key().to_base58_string(), + ..tests::fixtures::gateway_fixture() + }; + + let msg = gateway_bonding_sign_payload(deps, sender, None, gateway.clone(), stake); + let owner_signature = ed25519_sign_message(msg, keypair.private_key()); let identity_key = keypair.public_key().to_base58_string(); ( ExecuteMsg::BondGateway { - gateway: Gateway { - identity_key: identity_key.clone(), - ..tests::fixtures::gateway_fixture() - }, + gateway, owner_signature, }, identity_key, diff --git a/contracts/mixnet/src/support/tests/mod.rs b/contracts/mixnet/src/support/tests/mod.rs index 4e22de893f..71a2d96437 100644 --- a/contracts/mixnet/src/support/tests/mod.rs +++ b/contracts/mixnet/src/support/tests/mod.rs @@ -5,8 +5,6 @@ pub mod fixtures; #[cfg(test)] pub mod messages; -#[cfg(test)] -pub mod queries; #[cfg(test)] pub mod test_helpers { @@ -24,7 +22,8 @@ pub mod test_helpers { use crate::interval::{pending_events, storage as interval_storage}; use crate::mixnet_contract_settings::storage as mixnet_params_storage; use crate::mixnet_contract_settings::storage::{ - minimum_mixnode_pledge, rewarding_denom, rewarding_validator_address, + minimum_gateway_pledge, minimum_mixnode_pledge, rewarding_denom, + rewarding_validator_address, }; use crate::mixnodes::storage as mixnodes_storage; use crate::mixnodes::storage::mixnode_bonds; @@ -36,8 +35,11 @@ pub mod test_helpers { }; use crate::rewards::storage as rewards_storage; use crate::rewards::transactions::try_reward_mixnode; + use crate::signing::storage as signing_storage; use crate::support::tests; - use crate::support::tests::fixtures::TEST_COIN_DENOM; + use crate::support::tests::fixtures::{ + good_gateway_pledge, good_mixnode_pledge, TEST_COIN_DENOM, + }; use cosmwasm_std::testing::mock_dependencies; use cosmwasm_std::testing::mock_env; use cosmwasm_std::testing::mock_info; @@ -59,13 +61,19 @@ 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, IdentityKey, InitialRewardingParams, - InstantiateMsg, Interval, MixId, MixNode, MixNodeBond, Percent, RewardedSetNodeStatus, + Delegation, EpochState, EpochStatus, Gateway, GatewayBondingPayload, IdentityKey, + InitialRewardingParams, InstantiateMsg, Interval, MixId, MixNode, MixNodeBond, + MixnodeBondingPayload, Percent, RewardedSetNodeStatus, SignableGatewayBondingMsg, + SignableMixNodeBondingMsg, + }; + use nym_contracts_common::signing::{ + ContractMessageContent, MessageSignature, SignableMessage, SigningAlgorithm, SigningPurpose, }; use nym_crypto::asymmetric::identity; use nym_crypto::asymmetric::identity::KeyPair; use rand_chacha::rand_core::{CryptoRng, RngCore, SeedableRng}; use rand_chacha::ChaCha20Rng; + use serde::Serialize; use std::time::Duration; pub fn assert_eq_with_leeway(a: Uint128, b: Uint128, leeway: Uint128) { @@ -190,7 +198,7 @@ pub mod test_helpers { head: &str, label: &str, ) -> (MixId, identity::KeyPair) { - let (mix_id, keys) = self.add_dummy_mixnode_with_keypair(head, None); + 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(); @@ -198,6 +206,57 @@ pub mod test_helpers { } pub fn add_dummy_mixnode(&mut self, owner: &str, stake: Option) -> MixId { + let stake = self.make_mix_pledge(stake); + let (mixnode, owner_signature, _) = + self.mixnode_with_signature(owner, Some(stake.clone())); + + let info = mock_info(owner, stake.as_ref()); + 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 + } + + pub fn add_dummy_gateway(&mut self, sender: &str, stake: Option) -> IdentityKey { + let stake = self.make_gateway_pledge(stake); + let (gateway, owner_signature) = + self.gateway_with_signature(sender, Some(stake.clone())); + + let info = mock_info(sender, &stake); + let key = gateway.identity_key.clone(); + let env = self.env(); + try_add_gateway(self.deps_mut(), env, info, gateway, owner_signature).unwrap(); + key + } + + pub fn add_dummy_mixnodes(&mut self, n: usize) { + for i in 0..n { + self.add_dummy_mixnode(&format!("owner{i}"), None); + } + } + + pub fn add_dummy_gateways(&mut self, n: usize) { + for i in 0..n { + self.add_dummy_gateway(&format!("owner{i}"), None); + } + } + + pub fn make_mix_pledge(&self, stake: Option) -> Vec { let stake = match stake { Some(amount) => { let denom = rewarding_denom(self.deps().storage).unwrap(); @@ -205,36 +264,61 @@ pub mod test_helpers { } None => minimum_mixnode_pledge(self.deps.as_ref().storage).unwrap(), }; - - let env = self.env(); - add_mixnode(&mut self.rng, self.deps.as_mut(), env, owner, vec![stake]) + vec![stake] } - pub fn add_dummy_mixnode_with_keypair( + pub fn make_gateway_pledge(&self, stake: Option) -> Vec { + let stake = match stake { + Some(amount) => { + let denom = rewarding_denom(self.deps().storage).unwrap(); + Coin { denom, amount } + } + None => minimum_gateway_pledge(self.deps.as_ref().storage).unwrap(), + }; + vec![stake] + } + + pub fn mixnode_bonding_signature( + &mut self, + key: &identity::PrivateKey, + owner: &str, + mixnode: MixNode, + stake: Option, + ) -> MessageSignature { + let stake = self.make_mix_pledge(stake); + let msg = mixnode_bonding_sign_payload(self.deps(), owner, None, mixnode, stake); + ed25519_sign_message(msg, key) + } + + pub fn add_dummy_mixnode_with_proxy_and_keypair( &mut self, owner: &str, stake: Option, ) -> (MixId, identity::KeyPair) { - let stake = match stake { - Some(amount) => { - let denom = rewarding_denom(self.deps().storage).unwrap(); - Coin { denom, amount } - } - None => minimum_mixnode_pledge(self.deps.as_ref().storage).unwrap(), - }; + let stake = self.make_mix_pledge(stake); let proxy = self.vesting_contract(); let keypair = identity::KeyPair::new(&mut self.rng); - let owner_signature = keypair - .private_key() - .sign(owner.as_bytes()) - .to_base58_string(); + let identity_key = keypair.public_key().to_base58_string(); + let legit_sphinx_keys = nym_crypto::asymmetric::encryption::KeyPair::new(&mut self.rng); - let legit_sphinx_key = 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 info = mock_info(proxy.as_str(), &[stake]); - let key = keypair.public_key().to_base58_string(); + let msg = mixnode_bonding_sign_payload( + self.deps(), + owner, + Some(proxy.clone()), + mixnode.clone(), + stake.clone(), + ); + let owner_signature = ed25519_sign_message(msg, keypair.private_key()); + + let info = mock_info(proxy.as_str(), &stake); let current_id_counter = mixnodes_storage::MIXNODE_ID_COUNTER .may_load(self.deps().storage) .unwrap() @@ -245,11 +329,7 @@ pub mod test_helpers { self.deps_mut(), env, info, - MixNode { - identity_key: key, - sphinx_key: legit_sphinx_key.public_key().to_base58_string(), - ..tests::fixtures::mix_node_fixture() - }, + mixnode, tests::fixtures::mix_node_cost_params_fixture(), owner.to_string(), owner_signature, @@ -265,7 +345,8 @@ pub mod test_helpers { owner: &str, stake: Option, ) -> MixId { - self.add_dummy_mixnode_with_keypair(owner, stake).0 + self.add_dummy_mixnode_with_proxy_and_keypair(owner, stake) + .0 } pub fn set_illegal_mixnode_proxy(&mut self, mix_id: MixId, proxy: Addr) { @@ -312,14 +393,26 @@ pub mod test_helpers { None => minimum_mixnode_pledge(self.deps.as_ref().storage).unwrap(), }; + 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 proxy = self.vesting_contract(); - let legit_sphinx_key = nym_crypto::asymmetric::encryption::KeyPair::new(&mut self.rng); - let keypair = nym_crypto::asymmetric::identity::KeyPair::new(&mut self.rng); - let owner_signature = keypair - .private_key() - .sign(owner.as_bytes()) - .to_base58_string(); + let gateway = Gateway { + identity_key, + sphinx_key: legit_sphinx_keys.public_key().to_base58_string(), + ..tests::fixtures::gateway_fixture() + }; + + let msg = gateway_bonding_sign_payload( + self.deps(), + owner, + Some(proxy.clone()), + gateway.clone(), + vec![stake.clone()], + ); + let owner_signature = ed25519_sign_message(msg, keypair.private_key()); let env = self.env(); let info = mock_info(proxy.as_ref(), &[stake]); @@ -328,11 +421,7 @@ pub mod test_helpers { self.deps_mut(), env, info, - Gateway { - identity_key: keypair.public_key().to_base58_string(), - sphinx_key: legit_sphinx_key.public_key().to_base58_string(), - ..tests::fixtures::gateway_fixture() - }, + gateway, owner.to_string(), owner_signature, ) @@ -351,6 +440,63 @@ pub mod test_helpers { mix_id } + pub fn mixnode_with_signature( + &mut self, + sender: &str, + stake: Option>, + ) -> (MixNode, MessageSignature, KeyPair) { + let stake = stake.unwrap_or(good_mixnode_pledge()); + + 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(), + sender, + None, + mixnode.clone(), + stake.clone(), + ); + let owner_signature = ed25519_sign_message(msg, keypair.private_key()); + + (mixnode, owner_signature, keypair) + } + + pub fn gateway_with_signature( + &mut self, + sender: &str, + stake: Option>, + ) -> (Gateway, MessageSignature) { + let stake = stake.unwrap_or(good_gateway_pledge()); + + 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 gateway = Gateway { + identity_key, + sphinx_key: legit_sphinx_keys.public_key().to_base58_string(), + ..tests::fixtures::gateway_fixture() + }; + + let msg = gateway_bonding_sign_payload( + self.deps(), + sender, + None, + gateway.clone(), + stake.clone(), + ); + let owner_signature = ed25519_sign_message(msg, keypair.private_key()); + + (gateway, owner_signature) + } + pub fn start_unbonding_mixnode(&mut self, mix_id: MixId) { let bond_details = mixnodes_storage::mixnode_bonds() .load(self.deps().storage, mix_id) @@ -714,6 +860,22 @@ pub mod test_helpers { } } + pub fn ed25519_sign_message( + message: SignableMessage, + private_key: &identity::PrivateKey, + ) -> MessageSignature { + match message.algorithm { + SigningAlgorithm::Ed25519 => { + let plaintext = message.to_plaintext().unwrap(); + let signature = private_key.sign(&plaintext); + MessageSignature::from(signature.to_bytes().as_ref()) + } + SigningAlgorithm::Secp256k1 => { + unimplemented!() + } + } + } + pub fn simulator_from_single_node_state(deps: Deps<'_>, node: MixId) -> Simulator { let mix_rewarding = rewards_storage::MIXNODE_REWARDING .load(deps.storage, node) @@ -800,54 +962,54 @@ pub mod test_helpers { perform_pending_interval_actions(deps.branch(), &env, None).unwrap(); } - pub fn mixnode_with_signature( - mut rng: impl RngCore + CryptoRng, - sender: &str, - ) -> (MixNode, String, KeyPair) { - let keypair = nym_crypto::asymmetric::identity::KeyPair::new(&mut rng); - let legit_sphinx_key = nym_crypto::asymmetric::encryption::KeyPair::new(&mut rng); - let owner_signature = keypair - .private_key() - .sign(sender.as_bytes()) - .to_base58_string(); + // pub fn mixnode_with_signature( + // mut rng: impl RngCore + CryptoRng, + // deps: Deps<'_>, + // sender: &str, + // stake: Option>, + // ) -> (MixNode, MessageSignature, KeyPair) { + // // hehe stupid workaround for bypassing the immutable borrow and removing duplicate code + // + // let stake = stake.unwrap_or(good_mixnode_pledge()); + // + // let keypair = identity::KeyPair::new(&mut rng); + // let identity_key = keypair.public_key().to_base58_string(); + // let legit_sphinx_keys = nym_crypto::asymmetric::encryption::KeyPair::new(&mut 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(deps, sender, None, mixnode.clone(), stake.clone()); + // let owner_signature = ed25519_sign_message(msg, keypair.private_key()); + // + // (mixnode, owner_signature, keypair) + // } - let identity_key = keypair.public_key().to_base58_string(); - let sphinx_key = legit_sphinx_key.public_key().to_base58_string(); - - ( - MixNode { - identity_key, - sphinx_key, - ..tests::fixtures::mix_node_fixture() - }, - owner_signature, - keypair, - ) - } - - pub fn gateway_with_signature( - mut rng: impl RngCore + CryptoRng, - sender: &str, - ) -> (Gateway, String) { - let keypair = nym_crypto::asymmetric::identity::KeyPair::new(&mut rng); - let legit_sphinx_key = nym_crypto::asymmetric::encryption::KeyPair::new(&mut rng); - let owner_signature = keypair - .private_key() - .sign(sender.as_bytes()) - .to_base58_string(); - - let identity_key = keypair.public_key().to_base58_string(); - let sphinx_key = legit_sphinx_key.public_key().to_base58_string(); - - ( - Gateway { - identity_key, - sphinx_key, - ..tests::fixtures::gateway_fixture() - }, - owner_signature, - ) - } + // pub fn gateway_with_signature( + // mut rng: impl RngCore + CryptoRng, + // deps: Deps<'_>, + // sender: &str, + // stake: Option>, + // ) -> (Gateway, MessageSignature) { + // let stake = stake.unwrap_or(good_gateway_pledge()); + // + // let keypair = identity::KeyPair::new(&mut rng); + // let identity_key = keypair.public_key().to_base58_string(); + // let legit_sphinx_keys = nym_crypto::asymmetric::encryption::KeyPair::new(&mut rng); + // + // let gateway = Gateway { + // identity_key, + // sphinx_key: legit_sphinx_keys.public_key().to_base58_string(), + // ..tests::fixtures::gateway_fixture() + // }; + // + // let msg = gateway_bonding_sign_payload(deps, sender, None, gateway.clone(), stake.clone()); + // let owner_signature = ed25519_sign_message(msg, keypair.private_key()); + // + // (gateway, owner_signature) + // } pub fn add_dummy_delegations(mut deps: DepsMut<'_>, env: Env, mix_id: MixId, n: usize) { for i in 0..n { @@ -864,39 +1026,22 @@ pub mod test_helpers { } } - pub fn add_dummy_mixnodes( - mut rng: impl RngCore + CryptoRng, - mut deps: DepsMut<'_>, - env: Env, - n: usize, - ) { - for i in 0..n { - add_mixnode( - &mut rng, - deps.branch(), - env.clone(), - &format!("owner{}", i), - tests::fixtures::good_mixnode_pledge(), - ); - } - } - - pub fn add_dummy_gateways( - mut rng: impl RngCore + CryptoRng, - mut deps: DepsMut<'_>, - env: Env, - n: usize, - ) { - for i in 0..n { - add_gateway( - &mut rng, - deps.branch(), - env.clone(), - &format!("owner{}", i), - tests::fixtures::good_mixnode_pledge(), - ); - } - } + // pub fn add_dummy_mixnodes( + // mut rng: impl RngCore + CryptoRng, + // mut deps: DepsMut<'_>, + // env: Env, + // n: usize, + // ) { + // for i in 0..n { + // add_mixnode( + // &mut rng, + // deps.branch(), + // env.clone(), + // &format!("owner{}", i), + // tests::fixtures::good_mixnode_pledge(), + // ); + // } + // } pub fn add_dummy_unbonded_mixnodes( mut rng: impl RngCore + CryptoRng, @@ -968,79 +1113,35 @@ pub mod test_helpers { id } - // note to whoever wants to refactor this function, you dont want to grab rng here directly - // via `let rng = test_rng()` - // because it's extremely likely you might end up calling `add_mixnode()` multiple times - // in the same test and thus you're going to get mixnodes with the same keys and that's - // not what you want (presumably) - pub fn add_mixnode( - mut rng: impl RngCore + CryptoRng, - deps: DepsMut<'_>, - env: Env, - sender: &str, + pub fn mixnode_bonding_sign_payload( + deps: Deps<'_>, + owner: &str, + proxy: Option, + mixnode: MixNode, stake: Vec, - ) -> MixId { - let keypair = nym_crypto::asymmetric::identity::KeyPair::new(&mut rng); - let owner_signature = keypair - .private_key() - .sign(sender.as_bytes()) - .to_base58_string(); + ) -> SignableMixNodeBondingMsg { + let cost_params = tests::fixtures::mix_node_cost_params_fixture(); + let nonce = + signing_storage::get_signing_nonce(deps.storage, Addr::unchecked(owner)).unwrap(); - let legit_sphinx_key = nym_crypto::asymmetric::encryption::KeyPair::new(&mut rng); - - let info = mock_info(sender, &stake); - let key = keypair.public_key().to_base58_string(); - let current_id_counter = mixnodes_storage::MIXNODE_ID_COUNTER - .may_load(deps.storage) - .unwrap() - .unwrap_or_default(); - - try_add_mixnode( - deps, - env, - info, - MixNode { - identity_key: key, - sphinx_key: legit_sphinx_key.public_key().to_base58_string(), - ..tests::fixtures::mix_node_fixture() - }, - tests::fixtures::mix_node_cost_params_fixture(), - owner_signature, - ) - .unwrap(); - - // newly added mixnode gets assigned the current counter + 1 - current_id_counter + 1 + let payload = MixnodeBondingPayload::new(mixnode, cost_params); + let content = ContractMessageContent::new(Addr::unchecked(owner), proxy, stake, payload); + SignableMixNodeBondingMsg::new(nonce, content) } - // same note as with `add_mixnode` - pub fn add_gateway( - mut rng: impl RngCore + CryptoRng, - deps: DepsMut<'_>, - env: Env, - sender: &str, + pub fn gateway_bonding_sign_payload( + deps: Deps<'_>, + owner: &str, + proxy: Option, + gateway: Gateway, stake: Vec, - ) -> String { - let keypair = nym_crypto::asymmetric::identity::KeyPair::new(&mut rng); - let owner_signature = keypair - .private_key() - .sign(sender.as_bytes()) - .to_base58_string(); + ) -> SignableGatewayBondingMsg { + let nonce = + signing_storage::get_signing_nonce(deps.storage, Addr::unchecked(owner)).unwrap(); - let info = mock_info(sender, &stake); - let key = keypair.public_key().to_base58_string(); - try_add_gateway( - deps, - env, - info, - Gateway { - identity_key: key.clone(), - ..tests::fixtures::gateway_fixture() - }, - owner_signature, - ) - .unwrap(); - key + let payload = GatewayBondingPayload::new(gateway); + let content = ContractMessageContent::new(Addr::unchecked(owner), proxy, stake, payload); + SignableGatewayBondingMsg::new(nonce, content) } fn initial_rewarding_params() -> InitialRewardingParams { diff --git a/contracts/mixnet/src/support/tests/queries.rs b/contracts/mixnet/src/support/tests/queries.rs deleted file mode 100644 index 1955e8a85c..0000000000 --- a/contracts/mixnet/src/support/tests/queries.rs +++ /dev/null @@ -1,39 +0,0 @@ -use crate::contract::query; -use cosmwasm_std::{ - from_binary, - testing::{mock_env, MockApi, MockQuerier, MockStorage}, - OwnedDeps, -}; -use mixnet_contract_common::{GatewayBond, PagedGatewayResponse, QueryMsg}; - -// I honestly don't know why we're using this way of querying in tests, but I couldn't be bothered to change it -// since I haven't done anything to gateways -pub fn get_gateways(deps: &mut OwnedDeps) -> Vec { - let result = query( - deps.as_ref(), - mock_env(), - QueryMsg::GetGateways { - start_after: None, - limit: None, - }, - ) - .unwrap(); - - let page: PagedGatewayResponse = from_binary(&result).unwrap(); - if page.start_next_after.is_some() { - let next_page = query( - deps.as_ref(), - mock_env(), - QueryMsg::GetGateways { - start_after: page.start_next_after, - limit: None, - }, - ) - .unwrap(); - let next_page: PagedGatewayResponse = from_binary(&next_page).unwrap(); - if !next_page.nodes.is_empty() { - panic!("we didn't manage to get all gateways in a single query") - } - } - page.nodes -} diff --git a/contracts/vesting/src/contract.rs b/contracts/vesting/src/contract.rs index 39eff3a86c..cc6e59aa29 100644 --- a/contracts/vesting/src/contract.rs +++ b/contracts/vesting/src/contract.rs @@ -7,6 +7,7 @@ use crate::traits::{ DelegatingAccount, GatewayBondingAccount, MixnodeBondingAccount, NodeFamilies, VestingAccount, }; use crate::vesting::{populate_vesting_periods, Account}; +use contracts_common::signing::MessageSignature; use contracts_common::ContractBuildInformation; use cosmwasm_std::{ coin, entry_point, to_binary, Addr, BankMsg, Coin, Deps, DepsMut, Env, MessageInfo, Order, @@ -411,7 +412,7 @@ fn try_update_staking_address( /// Bond a gateway, sends [mixnet_contract_common::ExecuteMsg::BondGatewayOnBehalf] to [crate::storage::MIXNET_CONTRACT_ADDRESS]. pub fn try_bond_gateway( gateway: Gateway, - owner_signature: String, + owner_signature: MessageSignature, amount: Coin, info: MessageInfo, env: Env, @@ -448,7 +449,7 @@ pub fn try_track_unbond_gateway( pub fn try_bond_mixnode( mix_node: MixNode, cost_params: MixNodeCostParams, - owner_signature: String, + owner_signature: MessageSignature, amount: Coin, info: MessageInfo, env: Env, diff --git a/contracts/vesting/src/traits/bonding_account.rs b/contracts/vesting/src/traits/bonding_account.rs index 491378d630..242620a3be 100644 --- a/contracts/vesting/src/traits/bonding_account.rs +++ b/contracts/vesting/src/traits/bonding_account.rs @@ -1,4 +1,5 @@ use crate::errors::ContractError; +use contracts_common::signing::MessageSignature; use cosmwasm_std::{Coin, Env, Response, Storage}; use mixnet_contract_common::{ mixnode::{MixNodeConfigUpdate, MixNodeCostParams}, @@ -12,7 +13,7 @@ pub trait MixnodeBondingAccount { &self, mix_node: MixNode, cost_params: MixNodeCostParams, - owner_signature: String, + owner_signature: MessageSignature, pledge: Coin, env: &Env, storage: &mut dyn Storage, @@ -50,7 +51,7 @@ pub trait GatewayBondingAccount { fn try_bond_gateway( &self, gateway: Gateway, - owner_signature: String, + owner_signature: MessageSignature, pledge: Coin, env: &Env, storage: &mut dyn Storage, diff --git a/contracts/vesting/src/vesting/account/gateway_bonding_account.rs b/contracts/vesting/src/vesting/account/gateway_bonding_account.rs index 6669593871..72f83405b3 100644 --- a/contracts/vesting/src/vesting/account/gateway_bonding_account.rs +++ b/contracts/vesting/src/vesting/account/gateway_bonding_account.rs @@ -2,6 +2,7 @@ use super::PledgeData; use crate::errors::ContractError; use crate::storage::MIXNET_CONTRACT_ADDRESS; use crate::traits::GatewayBondingAccount; +use contracts_common::signing::MessageSignature; use cosmwasm_std::{wasm_execute, Coin, Env, Response, Storage, Uint128}; use mixnet_contract_common::{ExecuteMsg as MixnetExecuteMsg, Gateway}; use vesting_contract_common::events::{ @@ -14,7 +15,7 @@ impl GatewayBondingAccount for Account { fn try_bond_gateway( &self, gateway: Gateway, - owner_signature: String, + owner_signature: MessageSignature, pledge: Coin, env: &Env, storage: &mut dyn Storage, diff --git a/contracts/vesting/src/vesting/account/mixnode_bonding_account.rs b/contracts/vesting/src/vesting/account/mixnode_bonding_account.rs index 93bfb8840c..62aae9716a 100644 --- a/contracts/vesting/src/vesting/account/mixnode_bonding_account.rs +++ b/contracts/vesting/src/vesting/account/mixnode_bonding_account.rs @@ -5,6 +5,7 @@ use super::Account; use crate::errors::ContractError; use crate::storage::MIXNET_CONTRACT_ADDRESS; use crate::traits::MixnodeBondingAccount; +use contracts_common::signing::MessageSignature; use cosmwasm_std::{wasm_execute, Coin, Env, Response, Storage, Uint128}; use mixnet_contract_common::mixnode::MixNodeConfigUpdate; use mixnet_contract_common::mixnode::MixNodeCostParams; @@ -32,7 +33,7 @@ impl MixnodeBondingAccount for Account { &self, mix_node: MixNode, cost_params: MixNodeCostParams, - owner_signature: String, + owner_signature: MessageSignature, pledge: Coin, env: &Env, storage: &mut dyn Storage, diff --git a/contracts/vesting/src/vesting/mod.rs b/contracts/vesting/src/vesting/mod.rs index bd50838175..102d20cef6 100644 --- a/contracts/vesting/src/vesting/mod.rs +++ b/contracts/vesting/src/vesting/mod.rs @@ -47,6 +47,7 @@ mod tests { use crate::traits::VestingAccount; use crate::traits::{GatewayBondingAccount, MixnodeBondingAccount}; use crate::vesting::{populate_vesting_periods, Account}; + use contracts_common::signing::MessageSignature; use cosmwasm_std::testing::{mock_env, mock_info}; use cosmwasm_std::{coin, coins, Addr, Coin, Timestamp, Uint128}; use mixnet_contract_common::mixnode::MixNodeCostParams; @@ -674,7 +675,7 @@ mod tests { let err = account.try_bond_mixnode( mix_node.clone(), cost_params.clone(), - "alice".to_string(), + MessageSignature::from(vec![1, 2, 3]), Coin { amount: Uint128::new(1_000_000_000_001), denom: TEST_COIN_DENOM.to_string(), @@ -687,7 +688,7 @@ mod tests { let ok = account.try_bond_mixnode( mix_node.clone(), cost_params.clone(), - "alice".to_string(), + MessageSignature::from(vec![1, 2, 3]), Coin { amount: Uint128::new(90_000_000_000), denom: TEST_COIN_DENOM.to_string(), @@ -704,7 +705,7 @@ mod tests { let err = account.try_bond_mixnode( mix_node, cost_params, - "alice".to_string(), + MessageSignature::from(vec![1, 2, 3]), Coin { amount: Uint128::new(10_000_000_001), denom: TEST_COIN_DENOM.to_string(), @@ -738,7 +739,7 @@ mod tests { // Try delegating too much let err = account.try_bond_gateway( gateway.clone(), - "alice".to_string(), + MessageSignature::from(vec![1, 2, 3]), Coin { amount: Uint128::new(1_000_000_000_001), denom: TEST_COIN_DENOM.to_string(), @@ -750,7 +751,7 @@ mod tests { let ok = account.try_bond_gateway( gateway.clone(), - "alice".to_string(), + MessageSignature::from(vec![1, 2, 3]), Coin { amount: Uint128::new(90_000_000_000), denom: TEST_COIN_DENOM.to_string(), @@ -766,7 +767,7 @@ mod tests { // Try delegating too much again let err = account.try_bond_gateway( gateway, - "alice".to_string(), + MessageSignature::from(vec![1, 2, 3]), Coin { amount: Uint128::new(500_000_000_001), denom: TEST_COIN_DENOM.to_string(), diff --git a/gateway/src/commands/sign.rs b/gateway/src/commands/sign.rs index 61b587fa21..5037346b4b 100644 --- a/gateway/src/commands/sign.rs +++ b/gateway/src/commands/sign.rs @@ -8,31 +8,37 @@ use crate::{ commands::ensure_config_version_compatibility, config::persistence::pathfinder::GatewayPathfinder, }; -use anyhow::{anyhow, Result}; +use anyhow::{bail, Result}; use clap::{ArgGroup, Args}; use nym_crypto::asymmetric::identity; use std::error::Error; use validator_client::nyxd; #[derive(Args, Clone)] -#[clap(group(ArgGroup::new("sign").required(true).args(&["wallet_address", "text"])))] +#[clap(group(ArgGroup::new("sign").required(true).args(&["wallet_address", "text", "contract_msg"])))] pub struct Sign { /// The id of the mixnode you want to sign with #[clap(long)] id: String, /// Signs your blockchain address with your identity key - #[clap(long)] + // the alias here is included for backwards compatibility (1.1.4 and before) + #[clap(long, alias = "address")] wallet_address: Option, /// Signs an arbitrary piece of text with your identity key #[clap(long)] text: Option, + + /// Signs a transaction-specific payload, that is going to be sent to the smart contract, with your identity key + #[clap(long)] + contract_msg: Option, } enum SignedTarget { Text(String), Address(nyxd::AccountId), + ContractMsg(String), } impl TryFrom for SignedTarget { @@ -43,11 +49,13 @@ impl TryFrom for SignedTarget { Ok(SignedTarget::Text(text)) } else if let Some(address) = args.wallet_address { Ok(SignedTarget::Address(address)) + } else if let Some(msg) = args.contract_msg { + Ok(SignedTarget::ContractMsg(msg)) } else { // This is unreachable, and hopefully clap will support it explicitly by outputting an // enum from the ArgGroup in the future. // See: https://github.com/clap-rs/clap/issues/2621 - Err(anyhow!("Error: missing signed target flag")) + bail!("Error: missing signed target flag") } } } @@ -88,6 +96,32 @@ fn print_signed_text(private_key: &identity::PrivateKey, text: &str) { ); } +fn print_signed_contract_msg(private_key: &identity::PrivateKey, raw_msg: &str) { + let trimmed = raw_msg.trim(); + println!(">>> attempting to sign {trimmed}"); + + let Ok(decoded) = bs58::decode(trimmed).into_vec() else { + println!("it seems you have incorrectly copied the message to sign. Make sure you didn't accidentally skip any characters"); + return; + }; + + println!(">>> decoding the message..."); + + // we don't really care about what particular information is embedded inside of it, + // we just want to know if user correctly copied the string, i.e. whether it's a valid bs58 encoded json + if serde_json::from_slice::(&decoded).is_err() { + println!("it seems you have incorrectly copied the message to sign. Make sure you didn't accidentally skip any characters"); + return; + }; + + // if this is a valid json, it MUST be a valid string + let decoded_string = String::from_utf8(decoded.clone()).unwrap(); + println!(">>> message to sign: {decoded_string}"); + + let signature = private_key.sign(&decoded).to_base58_string(); + println!(">>> The base58-encoded signature is:\n{signature}"); +} + pub fn execute(args: Sign) -> Result<(), Box> { let config = build_config(args.id.clone(), OverrideConfig::default())?; ensure_config_version_compatibility(&config)?; @@ -99,6 +133,9 @@ pub fn execute(args: Sign) -> Result<(), Box> { match signed_target { SignedTarget::Text(text) => print_signed_text(identity_keypair.private_key(), &text), SignedTarget::Address(addr) => print_signed_address(identity_keypair.private_key(), addr)?, + SignedTarget::ContractMsg(raw_msg) => { + print_signed_contract_msg(identity_keypair.private_key(), &raw_msg) + } } Ok(()) diff --git a/gateway/src/config/mod.rs b/gateway/src/config/mod.rs index 2442632d59..d7d5a297e0 100644 --- a/gateway/src/config/mod.rs +++ b/gateway/src/config/mod.rs @@ -291,6 +291,7 @@ impl Config { &self.gateway.version } + #[allow(unused)] pub fn get_wallet_address(&self) -> Option { self.gateway.wallet_address.clone() } diff --git a/gateway/src/node/mod.rs b/gateway/src/node/mod.rs index cf716d1e10..6cd200b19a 100644 --- a/gateway/src/node/mod.rs +++ b/gateway/src/node/mod.rs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 use self::storage::PersistentStorage; -use crate::commands::ensure_correct_bech32_prefix; use crate::config::persistence::pathfinder::GatewayPathfinder; use crate::config::Config; use crate::error::GatewayError; @@ -12,8 +11,7 @@ use crate::node::client_handling::websocket::connection_handler::coconut::Coconu use crate::node::mixnet_handling::receiver::connection_handler::ConnectionHandler; use crate::node::statistics::collector::GatewayStatisticsCollector; use crate::node::storage::Storage; -use crate::{commands::sign::load_identity_keys, OutputFormat}; -use colored::Colorize; +use crate::OutputFormat; use log::*; use mixnet_client::forwarder::{MixForwardingSender, PacketForwarder}; use nym_crypto::asymmetric::{encryption, identity}; @@ -24,7 +22,6 @@ use rand::seq::SliceRandom; use rand::thread_rng; use std::error::Error; use std::net::SocketAddr; -use std::process; use std::sync::Arc; use validator_client::Client; @@ -109,28 +106,10 @@ where sphinx_keypair } - /// Signs the node config's bech32 address to produce a verification code for use in the wallet. - /// Exits if the address isn't valid (which should protect against manual edits). - fn generate_owner_signature(&self) -> Result { - let pathfinder = GatewayPathfinder::new_from_config(&self.config); - let identity_keypair = load_identity_keys(&pathfinder); - let Some(address) = self.config.get_wallet_address() else { - let error_message = "Error: gateway hasn't set its wallet address".red(); - eprintln!("{error_message}"); - eprintln!("Exiting..."); - process::exit(1); - }; - // perform extra validation to ensure we have correct prefix - ensure_correct_bech32_prefix(&address)?; - let verification_code = identity_keypair.private_key().sign_text(address.as_ref()); - Ok(verification_code) - } - pub(crate) fn print_node_details(&self, output: OutputFormat) -> Result<(), GatewayError> { let node_details = nym_types::gateway::GatewayNodeDetailsResponse { identity_key: self.identity_keypair.public_key().to_base58_string(), sphinx_key: self.sphinx_keypair.public_key().to_base58_string(), - owner_signature: self.generate_owner_signature()?, announce_address: self.config.get_announce_address(), bind_address: self.config.get_listening_address().to_string(), version: self.config.get_version().to_string(), diff --git a/mixnode/Cargo.toml b/mixnode/Cargo.toml index 343aab2622..d4bf22746c 100644 --- a/mixnode/Cargo.toml +++ b/mixnode/Cargo.toml @@ -41,6 +41,7 @@ atty = "0.2" ## internal nym-config = { path="../common/config" } nym-crypto = { path="../common/crypto" } +nym-contracts-common = { path = "../common/cosmwasm-smart-contracts/contracts-common" } mixnet-client = { path="../common/client-libs/mixnet-client" } mixnode-common = { path="../common/mixnode-common" } nym-nonexhaustive-delayqueue = { path="../common/nonexhaustive-delayqueue" } diff --git a/mixnode/src/commands/sign.rs b/mixnode/src/commands/sign.rs index 6c5aa74e3b..8189c6d7de 100644 --- a/mixnode/src/commands/sign.rs +++ b/mixnode/src/commands/sign.rs @@ -6,7 +6,7 @@ use std::convert::TryFrom; use crate::commands::validate_bech32_address_or_exit; use crate::config::{persistence::pathfinder::MixNodePathfinder, Config}; use crate::node::MixNode; -use anyhow::{anyhow, Result}; +use anyhow::{bail, Result}; use clap::{ArgGroup, Args}; use log::error; use nym_config::NymConfig; @@ -16,7 +16,7 @@ use validator_client::nyxd; use super::version_check; #[derive(Args, Clone)] -#[clap(group(ArgGroup::new("sign").required(true).args(&["wallet_address", "text"])))] +#[clap(group(ArgGroup::new("sign").required(true).args(&["wallet_address", "text", "contract_msg"])))] pub(crate) struct Sign { /// The id of the mixnode you want to sign with #[clap(long)] @@ -30,11 +30,16 @@ pub(crate) struct Sign { /// Signs an arbitrary piece of text with your identity key #[clap(long)] text: Option, + + /// Signs a transaction-specific payload, that is going to be sent to the smart contract, with your identity key + #[clap(long)] + contract_msg: Option, } enum SignedTarget { Text(String), Address(nyxd::AccountId), + ContractMsg(String), } impl TryFrom for SignedTarget { @@ -45,11 +50,13 @@ impl TryFrom for SignedTarget { Ok(SignedTarget::Text(text)) } else if let Some(address) = args.wallet_address { Ok(SignedTarget::Address(address)) + } else if let Some(msg) = args.contract_msg { + Ok(SignedTarget::ContractMsg(msg)) } else { // This is unreachable, and hopefully clap will support it explicitly by outputting an // enum from the ArgGroup in the future. // See: https://github.com/clap-rs/clap/issues/2621 - Err(anyhow!("Error: missing signed target flag")) + bail!("Error: missing signed target flag") } } } @@ -70,14 +77,39 @@ fn print_signed_text(private_key: &identity::PrivateKey, text: &str) { println!("The base58-encoded signature on '{text}' is: {signature}"); } +fn print_signed_contract_msg(private_key: &identity::PrivateKey, raw_msg: &str) { + let trimmed = raw_msg.trim(); + println!(">>> attempting to sign {trimmed}"); + + let Ok(decoded) = bs58::decode(trimmed).into_vec() else { + println!("it seems you have incorrectly copied the message to sign. Make sure you didn't accidentally skip any characters"); + return; + }; + + println!(">>> decoding the message..."); + + // we don't really care about what particular information is embedded inside of it, + // we just want to know if user correctly copied the string, i.e. whether it's a valid bs58 encoded json + if serde_json::from_slice::(&decoded).is_err() { + println!("it seems you have incorrectly copied the message to sign. Make sure you didn't accidentally skip any characters"); + return; + }; + + // if this is a valid json, it MUST be a valid string + let decoded_string = String::from_utf8(decoded.clone()).unwrap(); + println!(">>> message to sign: {decoded_string}"); + + let signature = private_key.sign(&decoded).to_base58_string(); + println!(">>> The base58-encoded signature is:\n{signature}"); +} + pub(crate) fn execute(args: &Sign) { let config = match Config::load_from_file(&args.id) { Ok(cfg) => cfg, Err(err) => { error!( - "Failed to load config for {}. Are you sure you have run `init` before? (Error was: {})", + "Failed to load config for {}. Are you sure you have run `init` before? (Error was: {err})", args.id, - err, ); return; } @@ -101,5 +133,8 @@ pub(crate) fn execute(args: &Sign) { match signed_target { SignedTarget::Text(text) => print_signed_text(identity_keypair.private_key(), &text), SignedTarget::Address(addr) => print_signed_address(identity_keypair.private_key(), addr), + SignedTarget::ContractMsg(raw_msg) => { + print_signed_contract_msg(identity_keypair.private_key(), &raw_msg) + } } } diff --git a/mixnode/src/node/mod.rs b/mixnode/src/node/mod.rs index b1113481e1..b38fe40228 100644 --- a/mixnode/src/node/mod.rs +++ b/mixnode/src/node/mod.rs @@ -1,7 +1,6 @@ // Copyright 2020 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::commands::validate_bech32_address_or_exit; use crate::config::persistence::pathfinder::MixNodePathfinder; use crate::config::Config; use crate::node::http::{ @@ -18,7 +17,6 @@ use crate::node::node_description::NodeDescription; use crate::node::node_statistics::SharedNodeStats; use crate::node::packet_delayforwarder::{DelayForwarder, PacketDelayForwardSender}; use crate::OutputFormat; -use colored::Colorize; use log::{error, info, warn}; use mixnode_common::verloc::{self, AtomicVerlocResult, VerlocMeasurer}; use nym_bin_common::version_checker::parse_version; @@ -84,29 +82,11 @@ impl MixNode { sphinx_keypair } - /// Signs the node config's bech32 address to produce a verification code for use in the wallet. - /// Exits if the address isn't valid (which should protect against manual edits). - fn generate_owner_signature(&self) -> String { - let pathfinder = MixNodePathfinder::new_from_config(&self.config); - let identity_keypair = Self::load_identity_keys(&pathfinder); - let Some(address) = self.config.get_wallet_address() else { - let error_message = "Error: mixnode hasn't set its wallet address".red(); - println!("{error_message}"); - println!("Exiting..."); - process::exit(1); - }; - // perform extra validation to ensure we have correct prefix - validate_bech32_address_or_exit(address.as_ref()); - let verification_code = identity_keypair.private_key().sign_text(address.as_ref()); - verification_code - } - /// Prints relevant node details to the console pub(crate) fn print_node_details(&self, output: OutputFormat) { let node_details = nym_types::mixnode::MixnodeNodeDetailsResponse { identity_key: self.identity_keypair.public_key().to_base58_string(), sphinx_key: self.sphinx_keypair.public_key().to_base58_string(), - owner_signature: self.generate_owner_signature(), announce_address: self.config.get_announce_address(), bind_address: self.config.get_listening_address().to_string(), version: self.config.get_version().to_string(), diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index d88f92b3ea..2be6c2fb75 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -20,7 +20,7 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b613b8e1e3cf911a086f53f03bf286f52fd7a7258e4fa606f0ef220d39d8877" dependencies = [ - "generic-array", + "generic-array 0.14.6", ] [[package]] @@ -32,7 +32,8 @@ dependencies = [ "cfg-if", "cipher", "cpufeatures", - "opaque-debug", + "ctr", + "opaque-debug 0.3.0", ] [[package]] @@ -46,7 +47,7 @@ dependencies = [ "cipher", "ctr", "ghash", - "subtle", + "subtle 2.4.1", ] [[package]] @@ -97,7 +98,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25df3c03f1040d0069fcd3907e24e36d59f9b6fa07ba49be0eb25a794f036ba7" dependencies = [ "base64ct", - "blake2", + "blake2 0.10.6", "password-hash 0.3.2", ] @@ -108,10 +109,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a27e27b63e4a34caee411ade944981136fdfa535522dc9944d6700196cbd899f" dependencies = [ "base64ct", - "blake2", + "blake2 0.10.6", "password-hash 0.4.2", ] +[[package]] +name = "arrayref" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" + [[package]] name = "async-trait" version = "0.1.64" @@ -227,7 +234,7 @@ dependencies = [ "rand_core 0.6.4", "ripemd160", "sha2 0.9.9", - "subtle", + "subtle 2.4.1", "zeroize", ] @@ -268,6 +275,18 @@ dependencies = [ "wyz", ] +[[package]] +name = "blake2" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94cb07b0da6a73955f8fb85d24c466778e70cda767a568229b104f0264089330" +dependencies = [ + "byte-tools", + "crypto-mac 0.7.0", + "digest 0.8.1", + "opaque-debug 0.2.3", +] + [[package]] name = "blake2" version = "0.10.6" @@ -290,7 +309,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" dependencies = [ "block-padding", - "generic-array", + "generic-array 0.14.6", ] [[package]] @@ -299,7 +318,7 @@ version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" dependencies = [ - "generic-array", + "generic-array 0.14.6", ] [[package]] @@ -318,7 +337,7 @@ dependencies = [ "group", "pairing", "rand_core 0.6.4", - "subtle", + "subtle 2.4.1", "zeroize", ] @@ -368,6 +387,12 @@ version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" +[[package]] +name = "byte-tools" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" + [[package]] name = "bytemuck" version = "1.13.0" @@ -469,13 +494,23 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chacha" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddf3c081b5fba1e5615640aae998e0fbd10c24cbd897ee39ed754a77601a4862" +dependencies = [ + "byteorder", + "keystream", +] + [[package]] name = "cipher" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" dependencies = [ - "generic-array", + "generic-array 0.14.6", ] [[package]] @@ -785,9 +820,9 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03c6a1d5fa1de37e071642dfa44ec552ca5b299adb128fab16138e24b548fd21" dependencies = [ - "generic-array", + "generic-array 0.14.6", "rand_core 0.6.4", - "subtle", + "subtle 2.4.1", "zeroize", ] @@ -797,18 +832,28 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ - "generic-array", + "generic-array 0.14.6", "typenum", ] +[[package]] +name = "crypto-mac" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4434400df11d95d556bac068ddfedd482915eb18fe8bea89bc80b6e4b1c179e5" +dependencies = [ + "generic-array 0.12.4", + "subtle 1.0.0", +] + [[package]] name = "crypto-mac" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" dependencies = [ - "generic-array", - "subtle", + "generic-array 0.14.6", + "subtle 2.4.1", ] [[package]] @@ -881,7 +926,7 @@ dependencies = [ "byteorder", "digest 0.9.0", "rand_core 0.5.1", - "subtle", + "subtle 2.4.1", "zeroize", ] @@ -1017,13 +1062,22 @@ dependencies = [ "syn", ] +[[package]] +name = "digest" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" +dependencies = [ + "generic-array 0.12.4", +] + [[package]] name = "digest" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" dependencies = [ - "generic-array", + "generic-array 0.14.6", ] [[package]] @@ -1034,7 +1088,7 @@ checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" dependencies = [ "block-buffer 0.10.3", "crypto-common", - "subtle", + "subtle 2.4.1", ] [[package]] @@ -1146,6 +1200,8 @@ checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" dependencies = [ "curve25519-dalek", "ed25519", + "rand 0.7.3", + "serde", "sha2 0.9.9", "zeroize", ] @@ -1181,11 +1237,11 @@ dependencies = [ "crypto-bigint", "der", "ff", - "generic-array", + "generic-array 0.14.6", "group", "rand_core 0.6.4", "sec1", - "subtle", + "subtle 2.4.1", "zeroize", ] @@ -1294,7 +1350,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "131655483be284720a17d74ff97592b8e76576dc25563148601df2d7c9080924" dependencies = [ "rand_core 0.6.4", - "subtle", + "subtle 2.4.1", ] [[package]] @@ -1580,6 +1636,15 @@ dependencies = [ "windows 0.39.0", ] +[[package]] +name = "generic-array" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" +dependencies = [ + "typenum", +] + [[package]] name = "generic-array" version = "0.14.6" @@ -1597,8 +1662,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.9.0+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -1632,7 +1699,7 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1583cc1656d7839fd3732b80cf4f38850336cdb9b8ded1cd399ca62958de3c99" dependencies = [ - "opaque-debug", + "opaque-debug 0.3.0", "polyval", ] @@ -1763,7 +1830,7 @@ dependencies = [ "byteorder", "ff", "rand_core 0.6.4", - "subtle", + "subtle 2.4.1", ] [[package]] @@ -1939,13 +2006,23 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ebdb29d2ea9ed0083cd8cece49bbd968021bd99b0849edb4a9a7ee0fdf6a4e0" +[[package]] +name = "hkdf" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01706d578d5c281058480e673ae4086a9f4710d8df1ad80a5b03e39ece5f886b" +dependencies = [ + "digest 0.9.0", + "hmac", +] + [[package]] name = "hmac" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" dependencies = [ - "crypto-mac", + "crypto-mac 0.11.1", "digest 0.9.0", ] @@ -2338,6 +2415,12 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "keystream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33070833c9ee02266356de0c43f723152bd38bd96ddf52c82b3af10c9138b28" + [[package]] name = "kuchiki" version = "0.8.1" @@ -2374,6 +2457,12 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "libm" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "348108ab3fba42ec82ff6e9564fc4ca0247bdccdc68dd8af9764bbc79c3c8ffb" + [[package]] name = "libz-sys" version = "1.1.8" @@ -2401,6 +2490,18 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" +[[package]] +name = "lioness" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae926706ba42c425c9457121178330d75e273df2e82e28b758faf3de3a9acb9" +dependencies = [ + "arrayref", + "blake2 0.8.1", + "chacha", + "keystream", +] + [[package]] name = "lock_api" version = "0.4.9" @@ -2650,6 +2751,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" dependencies = [ "autocfg 1.1.0", + "libm", ] [[package]] @@ -2797,6 +2899,20 @@ dependencies = [ "zeroize", ] +[[package]] +name = "nym-crypto" +version = "0.1.0" +dependencies = [ + "bs58", + "ed25519-dalek", + "nym-pemstore", + "nym-sphinx-types", + "rand 0.7.3", + "subtle-encoding", + "thiserror", + "x25519-dalek", +] + [[package]] name = "nym-execute" version = "0.1.0" @@ -2866,6 +2982,13 @@ dependencies = [ "pem", ] +[[package]] +name = "nym-sphinx-types" +version = "0.1.0" +dependencies = [ + "sphinx-packet", +] + [[package]] name = "nym-types" version = "1.0.0" @@ -2961,6 +3084,7 @@ version = "1.1.11" dependencies = [ "aes-gcm", "argon2 0.3.4", + "async-trait", "base64 0.13.1", "bip39", "cfg-if", @@ -2977,6 +3101,8 @@ dependencies = [ "log", "nym-coconut-interface", "nym-config", + "nym-contracts-common", + "nym-crypto", "nym-mixnet-contract-common", "nym-types", "nym-vesting-contract", @@ -2985,6 +3111,7 @@ dependencies = [ "once_cell", "pretty_env_logger", "rand 0.6.5", + "rand_chacha 0.2.2", "reqwest", "serde", "serde_json", @@ -3071,6 +3198,12 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" +[[package]] +name = "opaque-debug" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" + [[package]] name = "opaque-debug" version = "0.3.0" @@ -3209,7 +3342,7 @@ checksum = "1d791538a6dcc1e7cb7fe6f6b58aca40e7f79403c45b2bc274008b5e647af1d8" dependencies = [ "base64ct", "rand_core 0.6.4", - "subtle", + "subtle 2.4.1", ] [[package]] @@ -3220,7 +3353,7 @@ checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" dependencies = [ "base64ct", "rand_core 0.6.4", - "subtle", + "subtle 2.4.1", ] [[package]] @@ -3241,7 +3374,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f05894bce6a1ba4be299d0c5f29563e08af2bc18bb7d48313113bed71e904739" dependencies = [ - "crypto-mac", + "crypto-mac 0.11.1", ] [[package]] @@ -3513,7 +3646,7 @@ checksum = "8419d2b623c7c0896ff2d5d96e2cb4ede590fed28fcc34934f4c33c036e620a1" dependencies = [ "cfg-if", "cpufeatures", - "opaque-debug", + "opaque-debug 0.3.0", "universal-hash", ] @@ -3764,6 +3897,16 @@ dependencies = [ "getrandom 0.2.8", ] +[[package]] +name = "rand_distr" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e9532ada3929fb8b2e9dbe28d1e06c9b2cc65813f074fcb6bd5fbefeff9d56" +dependencies = [ + "num-traits", + "rand 0.7.3", +] + [[package]] name = "rand_hc" version = "0.1.0" @@ -4012,7 +4155,7 @@ checksum = "2eca4ecc81b7f313189bf73ce724400a07da2a6dac19588b03c8bd76a2dcc251" dependencies = [ "block-buffer 0.9.0", "digest 0.9.0", - "opaque-debug", + "opaque-debug 0.3.0", ] [[package]] @@ -4162,9 +4305,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08da66b8b0965a5555b6bd6639e68ccba85e1e2506f5fbb089e93f8a04e1a2d1" dependencies = [ "der", - "generic-array", + "generic-array 0.14.6", "pkcs8", - "subtle", + "subtle 2.4.1", "zeroize", ] @@ -4396,7 +4539,7 @@ dependencies = [ "cfg-if", "cpufeatures", "digest 0.9.0", - "opaque-debug", + "opaque-debug 0.3.0", ] [[package]] @@ -4419,7 +4562,7 @@ dependencies = [ "block-buffer 0.9.0", "digest 0.9.0", "keccak", - "opaque-debug", + "opaque-debug 0.3.0", ] [[package]] @@ -4518,6 +4661,30 @@ dependencies = [ "system-deps 5.0.0", ] +[[package]] +name = "sphinx-packet" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc43eda802856ee82a7555c7b75ceb9e07451741c7a2f5f23d036020e01189d4" +dependencies = [ + "aes", + "arrayref", + "blake2 0.8.1", + "bs58", + "byteorder", + "chacha", + "curve25519-dalek", + "digest 0.9.0", + "hkdf", + "hmac", + "lioness", + "log", + "rand 0.7.3", + "rand_distr", + "sha2 0.9.9", + "subtle 2.4.1", +] + [[package]] name = "spin" version = "0.5.2" @@ -4609,6 +4776,12 @@ dependencies = [ "syn", ] +[[package]] +name = "subtle" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d67a5a62ba6e01cb2192ff309324cb4875d0c451d55fe2319433abe7a05a8ee" + [[package]] name = "subtle" version = "2.4.1" @@ -4951,7 +5124,7 @@ dependencies = [ "serde_repr", "sha2 0.9.9", "signature", - "subtle", + "subtle 2.4.1", "subtle-encoding", "tendermint-proto", "time", @@ -5369,8 +5542,8 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f214e8f697e925001e66ec2c6e37a4ef93f0f78c2eed7814394e10c62025b05" dependencies = [ - "generic-array", - "subtle", + "generic-array 0.14.6", + "subtle 2.4.1", ] [[package]] @@ -6029,6 +6202,17 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "x25519-dalek" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a0c105152107e3b96f6a00a65e86ce82d9b125230e1c4302940eca58ff71f4f" +dependencies = [ + "curve25519-dalek", + "rand_core 0.5.1", + "zeroize", +] + [[package]] name = "xattr" version = "0.2.3" diff --git a/nym-wallet/src-tauri/Cargo.toml b/nym-wallet/src-tauri/Cargo.toml index 406e9fb4c1..00a6f7f63b 100644 --- a/nym-wallet/src-tauri/Cargo.toml +++ b/nym-wallet/src-tauri/Cargo.toml @@ -19,6 +19,7 @@ tauri-codegen = "=1.2.1" tauri-macros = "=1.2.1" [dependencies] +async-trait = "0.1.64" bip39 = "1.0" cfg-if = "1.0.0" colored = "2.0" @@ -57,6 +58,8 @@ cosmrs = { git = "https://github.com/neacsu/cosmos-rust", branch = "neacsu/feegr validator-client = { path = "../../common/client-libs/validator-client", features = [ "nyxd-client", ] } +nym-crypto = { path = "../../common/crypto", features = ["asymmetric"] } +nym-contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common" } nym-mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract" } nym-vesting-contract-common = { path = "../../common/cosmwasm-smart-contracts/vesting-contract" } nym-config = { path = "../../common/config" } @@ -67,6 +70,8 @@ nym-types = { path = "../../common/types" } nym-wallet-types = { path = "../nym-wallet-types" } [dev-dependencies] +nym-crypto = { path = "../../common/crypto", features = ["rand"] } +rand_chacha = "0.2" tempfile = "3.3.0" ts-rs = "6.1.2" diff --git a/nym-wallet/src-tauri/src/error.rs b/nym-wallet/src-tauri/src/error.rs index 0166435c3e..3684e91a38 100644 --- a/nym-wallet/src-tauri/src/error.rs +++ b/nym-wallet/src-tauri/src/error.rs @@ -1,3 +1,5 @@ +use nym_contracts_common::signing::SigningAlgorithm; +use nym_crypto::asymmetric::identity::Ed25519RecoveryError; use nym_types::error::TypesError; use nym_wallet_types::network::Network; use serde::{Serialize, Serializer}; @@ -122,6 +124,14 @@ pub enum BackendError { #[error("Unable to open a new window")] NewWindowError, + #[error("received unexpected signing algorithm: {received:?}. Expected to get {expected:?}")] + UnexpectedSigningAlgorithm { + received: SigningAlgorithm, + expected: SigningAlgorithm, + }, + #[error(transparent)] + Ed25519Recovery(#[from] Ed25519RecoveryError), + #[error("This command ({name}) has been removed. Please try to use {alternative} instead.")] RemovedCommand { name: String, alternative: String }, } diff --git a/nym-wallet/src-tauri/src/main.rs b/nym-wallet/src-tauri/src/main.rs index 15f03fc481..cda784a8b7 100644 --- a/nym-wallet/src-tauri/src/main.rs +++ b/nym-wallet/src-tauri/src/main.rs @@ -169,6 +169,10 @@ fn main() { simulate::mixnet::simulate_claim_operator_reward, signatures::sign::sign, signatures::sign::verify, + signatures::ed25519_signing_payload::generate_mixnode_bonding_msg_payload, + signatures::ed25519_signing_payload::vesting_generate_mixnode_bonding_msg_payload, + signatures::ed25519_signing_payload::generate_gateway_bonding_msg_payload, + signatures::ed25519_signing_payload::vesting_generate_gateway_bonding_msg_payload, help::log::help_log_toggle_window, ]) .menu(Menu::os_default(&context.package_info().name).add_default_app_submenus()) diff --git a/nym-wallet/src-tauri/src/operations/helpers.rs b/nym-wallet/src-tauri/src/operations/helpers.rs new file mode 100644 index 0000000000..2e828d3999 --- /dev/null +++ b/nym-wallet/src-tauri/src/operations/helpers.rs @@ -0,0 +1,391 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::BackendError; +use async_trait::async_trait; +use cosmwasm_std::Addr; +use nym_contracts_common::signing::{ + ContractMessageContent, MessageSignature, Nonce, SignableMessage, SigningAlgorithm, +}; +use nym_crypto::asymmetric::identity; +use nym_mixnet_contract_common::{ + construct_mixnode_bonding_sign_payload, Gateway, GatewayBondingPayload, MixNode, + MixNodeCostParams, SignableGatewayBondingMsg, SignableMixNodeBondingMsg, +}; +use validator_client::nyxd::error::NyxdError; +use validator_client::nyxd::traits::MixnetQueryClient; +use validator_client::nyxd::{Coin, SigningNyxdClient}; +use validator_client::Client; + +// define this as a separate trait for mocking purposes +#[async_trait] +pub(crate) trait AddressAndNonceProvider { + async fn get_signing_nonce(&self) -> Result; + fn vesting_contract_address(&self) -> Addr; + fn cw_address(&self) -> Addr; +} + +#[async_trait] +impl AddressAndNonceProvider for Client { + async fn get_signing_nonce(&self) -> Result { + self.nyxd.get_signing_nonce(self.nyxd.address()).await + } + + fn vesting_contract_address(&self) -> Addr { + // the call to unchecked is fine here as we're converting directly from `AccountId` + // which must have been a valid bech32 address + Addr::unchecked(self.nyxd.vesting_contract_address().as_ref()) + } + + fn cw_address(&self) -> Addr { + self.nyxd.cw_address() + } +} + +fn proxy(client: &P, vesting: bool) -> Option { + if vesting { + Some(client.vesting_contract_address()) + } else { + None + } +} + +// since the message has to go back to the user due to the increasing nonce, we might as well sign the entire payload +pub(crate) async fn create_mixnode_bonding_sign_payload( + client: &P, + mix_node: MixNode, + cost_params: MixNodeCostParams, + pledge: Coin, + vesting: bool, +) -> Result { + let sender = client.cw_address(); + let proxy = proxy(client, vesting); + let nonce = client.get_signing_nonce().await?; + + Ok(construct_mixnode_bonding_sign_payload( + nonce, + sender, + proxy, + pledge.into(), + mix_node, + cost_params, + )) +} + +pub(crate) async fn verify_mixnode_bonding_sign_payload( + client: &P, + mix_node: &MixNode, + cost_params: &MixNodeCostParams, + pledge: &Coin, + vesting: bool, + msg_signature: &MessageSignature, +) -> Result<(), BackendError> { + let identity_key = identity::PublicKey::from_base58_string(&mix_node.identity_key)?; + let signature = identity::Signature::from_bytes(msg_signature.as_ref())?; + + // recreate the plaintext + let msg = create_mixnode_bonding_sign_payload( + client, + mix_node.clone(), + cost_params.clone(), + pledge.clone(), + vesting, + ) + .await?; + let plaintext = msg.to_plaintext()?; + + if !msg.algorithm.is_ed25519() { + return Err(BackendError::UnexpectedSigningAlgorithm { + received: msg.algorithm, + expected: SigningAlgorithm::Ed25519, + }); + } + + // TODO: possibly provide better error message if this check fails + identity_key.verify(&plaintext, &signature)?; + Ok(()) +} + +// since the message has to go back to the user due to the increasing nonce, we might as well sign the entire payload +pub(crate) async fn create_gateway_bonding_sign_payload( + client: &P, + gateway: Gateway, + pledge: Coin, + vesting: bool, +) -> Result { + let payload = GatewayBondingPayload::new(gateway); + let sender = client.cw_address(); + let proxy = proxy(client, vesting); + let content = ContractMessageContent::new(sender, proxy, vec![pledge.into()], payload); + let nonce = client.get_signing_nonce().await?; + + Ok(SignableMessage::new(nonce, content)) +} + +pub(crate) async fn verify_gateway_bonding_sign_payload( + client: &P, + gateway: &Gateway, + pledge: &Coin, + vesting: bool, + msg_signature: &MessageSignature, +) -> Result<(), BackendError> { + let identity_key = identity::PublicKey::from_base58_string(&gateway.identity_key)?; + let signature = identity::Signature::from_bytes(msg_signature.as_ref())?; + + // recreate the plaintext + let msg = create_gateway_bonding_sign_payload(client, gateway.clone(), pledge.clone(), vesting) + .await?; + let plaintext = msg.to_plaintext()?; + + if !msg.algorithm.is_ed25519() { + return Err(BackendError::UnexpectedSigningAlgorithm { + received: msg.algorithm, + expected: SigningAlgorithm::Ed25519, + }); + } + + // TODO: possibly provide better error message if this check fails + identity_key.verify(&plaintext, &signature)?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use cosmwasm_std::coin; + use nym_contracts_common::Percent; + use rand_chacha::rand_core::SeedableRng; + use rand_chacha::ChaCha20Rng; + + // use rng with constant seed for all tests so that they would be deterministic + pub fn test_rng() -> ChaCha20Rng { + let dummy_seed = [42u8; 32]; + rand_chacha::ChaCha20Rng::from_seed(dummy_seed) + } + + struct MockClient { + address: Addr, + vesting_contract: Addr, + signing_nonce: Nonce, + } + + #[async_trait] + impl AddressAndNonceProvider for MockClient { + async fn get_signing_nonce(&self) -> Result { + Ok(self.signing_nonce) + } + + fn vesting_contract_address(&self) -> Addr { + self.vesting_contract.clone() + } + + fn cw_address(&self) -> Addr { + self.address.clone() + } + } + + #[tokio::test] + async fn dummy_mix_bonding_signature_verification() { + let mut rng = test_rng(); + let identity_keypair = identity::KeyPair::new(&mut rng); + let dummy_mixnode = MixNode { + host: "1.2.3.4".to_string(), + mix_port: 1234, + verloc_port: 2345, + http_api_port: 3456, + sphinx_key: "totally-legit-sphinx-key".to_string(), + identity_key: identity_keypair.public_key().to_base58_string(), + version: "v1.2.3".to_string(), + }; + let dummy_cost_params = MixNodeCostParams { + profit_margin_percent: Percent::from_percentage_value(42).unwrap(), + interval_operating_cost: coin(1111111, "unym"), + }; + let dummy_pledge: Coin = coin(10000000000, "unym").into(); + + let dummy_account = Addr::unchecked("n16t2umcd83zjpl5puyuuq6lgmy4p3qedjd8ynn6"); + let dummy_client = MockClient { + address: dummy_account, + vesting_contract: Addr::unchecked("n17tj0a0w6v7r2dc54rnkzfza6s8hxs87rj273a5"), + signing_nonce: 42, + }; + + let signing_msg_liquid = create_mixnode_bonding_sign_payload( + &dummy_client, + dummy_mixnode.clone(), + dummy_cost_params.clone(), + dummy_pledge.clone(), + false, + ) + .await + .unwrap(); + + let plaintext_liquid = signing_msg_liquid.to_plaintext().unwrap(); + let sig_liquid: MessageSignature = identity_keypair + .private_key() + .sign(&plaintext_liquid) + .to_bytes() + .as_ref() + .into(); + + let signing_msg_vesting = create_mixnode_bonding_sign_payload( + &dummy_client, + dummy_mixnode.clone(), + dummy_cost_params.clone(), + dummy_pledge.clone(), + true, + ) + .await + .unwrap(); + + let plaintext_vesting = signing_msg_vesting.to_plaintext().unwrap(); + let sig_vesting: MessageSignature = identity_keypair + .private_key() + .sign(&plaintext_vesting) + .to_bytes() + .as_ref() + .into(); + + let res = verify_mixnode_bonding_sign_payload( + &dummy_client, + &dummy_mixnode, + &dummy_cost_params, + &dummy_pledge, + false, + &sig_liquid, + ) + .await; + assert!(res.is_ok()); + + let res = verify_mixnode_bonding_sign_payload( + &dummy_client, + &dummy_mixnode, + &dummy_cost_params, + &dummy_pledge, + true, + &sig_vesting, + ) + .await; + assert!(res.is_ok()); + + let res = verify_mixnode_bonding_sign_payload( + &dummy_client, + &dummy_mixnode, + &dummy_cost_params, + &dummy_pledge, + false, + &sig_vesting, + ) + .await; + assert!(res.is_err()); + + let res = verify_mixnode_bonding_sign_payload( + &dummy_client, + &dummy_mixnode, + &dummy_cost_params, + &dummy_pledge, + true, + &sig_liquid, + ) + .await; + assert!(res.is_err()) + } + + #[tokio::test] + async fn dummy_gateway_bonding_signature_verification() { + let mut rng = test_rng(); + let identity_keypair = identity::KeyPair::new(&mut rng); + let dummy_gateway = Gateway { + host: "1.2.3.4".to_string(), + mix_port: 1234, + clients_port: 2345, + location: "whatever".to_string(), + sphinx_key: "totally-legit-sphinx-key".to_string(), + identity_key: identity_keypair.public_key().to_base58_string(), + version: "v1.2.3".to_string(), + }; + + let dummy_pledge: Coin = coin(10000000000, "unym").into(); + + let dummy_account = Addr::unchecked("n16t2umcd83zjpl5puyuuq6lgmy4p3qedjd8ynn6"); + let dummy_client = MockClient { + address: dummy_account, + vesting_contract: Addr::unchecked("n17tj0a0w6v7r2dc54rnkzfza6s8hxs87rj273a5"), + signing_nonce: 42, + }; + + let signing_msg_liquid = create_gateway_bonding_sign_payload( + &dummy_client, + dummy_gateway.clone(), + dummy_pledge.clone(), + false, + ) + .await + .unwrap(); + + let plaintext_liquid = signing_msg_liquid.to_plaintext().unwrap(); + let sig_liquid: MessageSignature = identity_keypair + .private_key() + .sign(&plaintext_liquid) + .to_bytes() + .as_ref() + .into(); + + let signing_msg_vesting = create_gateway_bonding_sign_payload( + &dummy_client, + dummy_gateway.clone(), + dummy_pledge.clone(), + true, + ) + .await + .unwrap(); + + let plaintext_vesting = signing_msg_vesting.to_plaintext().unwrap(); + let sig_vesting: MessageSignature = identity_keypair + .private_key() + .sign(&plaintext_vesting) + .to_bytes() + .as_ref() + .into(); + + let res = verify_gateway_bonding_sign_payload( + &dummy_client, + &dummy_gateway, + &dummy_pledge, + false, + &sig_liquid, + ) + .await; + assert!(res.is_ok()); + + let res = verify_gateway_bonding_sign_payload( + &dummy_client, + &dummy_gateway, + &dummy_pledge, + true, + &sig_vesting, + ) + .await; + assert!(res.is_ok()); + + let res = verify_gateway_bonding_sign_payload( + &dummy_client, + &dummy_gateway, + &dummy_pledge, + false, + &sig_vesting, + ) + .await; + assert!(res.is_err()); + + let res = verify_gateway_bonding_sign_payload( + &dummy_client, + &dummy_gateway, + &dummy_pledge, + true, + &sig_liquid, + ) + .await; + assert!(res.is_err()) + } +} diff --git a/nym-wallet/src-tauri/src/operations/mixnet/bond.rs b/nym-wallet/src-tauri/src/operations/mixnet/bond.rs index 216dd12841..9884c88080 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/bond.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/bond.rs @@ -2,8 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 use crate::error::BackendError; +use crate::operations::helpers::{ + verify_gateway_bonding_sign_payload, verify_mixnode_bonding_sign_payload, +}; use crate::state::WalletState; use crate::{nyxd_client, Gateway, MixNode}; +use nym_contracts_common::signing::MessageSignature; use nym_mixnet_contract_common::{MixId, MixNodeConfigUpdate}; use nym_types::currency::DecCoin; use nym_types::gateway::GatewayBond; @@ -26,7 +30,7 @@ pub struct NodeDescription { pub async fn bond_gateway( gateway: Gateway, pledge: DecCoin, - owner_signature: String, + msg_signature: MessageSignature, fee: Option, state: tauri::State<'_, WalletState>, ) -> Result { @@ -41,10 +45,20 @@ pub async fn bond_gateway( pledge_base, fee, ); - let res = guard - .current_client()? + + let client = guard.current_client()?; + // check the signature to make sure the user copied it correctly + if let Err(err) = + verify_gateway_bonding_sign_payload(client, &gateway, &pledge_base, false, &msg_signature) + .await + { + log::warn!("failed to verify provided gateway bonding signature: {err}"); + return Err(err); + } + + let res = client .nyxd - .bond_gateway(gateway, owner_signature, pledge_base, fee) + .bond_gateway(gateway, msg_signature, pledge_base, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); log::trace!("<<< {:?}", res); @@ -73,7 +87,7 @@ pub async fn unbond_gateway( pub async fn bond_mixnode( mixnode: MixNode, cost_params: MixNodeCostParams, - owner_signature: String, + msg_signature: MessageSignature, pledge: DecCoin, fee: Option, state: tauri::State<'_, WalletState>, @@ -90,10 +104,26 @@ pub async fn bond_mixnode( pledge_base, fee, ); - let res = guard - .current_client()? + + let client = guard.current_client()?; + // check the signature to make sure the user copied it correctly + if let Err(err) = verify_mixnode_bonding_sign_payload( + client, + &mixnode, + &cost_params, + &pledge_base, + false, + &msg_signature, + ) + .await + { + log::warn!("failed to verify provided mixnode bonding signature: {err}"); + return Err(err); + } + + let res = client .nyxd - .bond_mixnode(mixnode, cost_params, owner_signature, pledge_base, fee) + .bond_mixnode(mixnode, cost_params, msg_signature, pledge_base, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); log::trace!("<<< {:?}", res); diff --git a/nym-wallet/src-tauri/src/operations/mod.rs b/nym-wallet/src-tauri/src/operations/mod.rs index c6c175ee12..70c6928b0e 100644 --- a/nym-wallet/src-tauri/src/operations/mod.rs +++ b/nym-wallet/src-tauri/src/operations/mod.rs @@ -1,4 +1,8 @@ +// Copyright 2021-2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + pub mod help; +pub(crate) mod helpers; pub mod mixnet; pub mod nym_api; pub mod signatures; diff --git a/nym-wallet/src-tauri/src/operations/signatures/ed25519_signing_payload.rs b/nym-wallet/src-tauri/src/operations/signatures/ed25519_signing_payload.rs new file mode 100644 index 0000000000..d539cf5eb3 --- /dev/null +++ b/nym-wallet/src-tauri/src/operations/signatures/ed25519_signing_payload.rs @@ -0,0 +1,100 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::BackendError; +use crate::operations::helpers::{ + create_gateway_bonding_sign_payload, create_mixnode_bonding_sign_payload, +}; +use crate::state::WalletState; +use nym_mixnet_contract_common::{Gateway, MixNode}; +use nym_types::currency::DecCoin; +use nym_types::mixnode::MixNodeCostParams; + +async fn mixnode_bonding_msg_payload( + mixnode: MixNode, + cost_params: MixNodeCostParams, + pledge: DecCoin, + vesting: bool, + state: tauri::State<'_, WalletState>, +) -> Result { + let guard = state.read().await; + let reg = guard.registered_coins()?; + let pledge_base = guard.attempt_convert_to_base_coin(pledge.clone())?; + let cost_params = cost_params.try_convert_to_mixnet_contract_cost_params(reg)?; + log::info!( + ">>> Bond mixnode bonding signature: identity_key = {}, pledge_display = {}, pledge_base = {}, vesting = {vesting}", + mixnode.identity_key, + pledge, + pledge_base, + ); + + let client = guard.current_client()?; + + // TODO: decide on exact structure here. Json? base58? some hash? + // to be determined + let msg = + create_mixnode_bonding_sign_payload(client, mixnode, cost_params, pledge_base, vesting) + .await?; + Ok(msg.to_base58_string()?) +} + +async fn gateway_bonding_msg_payload( + gateway: Gateway, + pledge: DecCoin, + vesting: bool, + state: tauri::State<'_, WalletState>, +) -> Result { + let guard = state.read().await; + let pledge_base = guard.attempt_convert_to_base_coin(pledge.clone())?; + log::info!( + ">>> Bond gateway bonding signature: identity_key = {}, pledge_display = {}, pledge_base = {}, vesting = {vesting}", + gateway.identity_key, + pledge, + pledge_base, + ); + + let client = guard.current_client()?; + + // TODO: decide on exact structure here. Json? base58? some hash? + // to be determined + let msg = create_gateway_bonding_sign_payload(client, gateway, pledge_base, vesting).await?; + Ok(msg.to_base58_string()?) +} + +#[tauri::command] +pub async fn generate_mixnode_bonding_msg_payload( + mixnode: MixNode, + cost_params: MixNodeCostParams, + pledge: DecCoin, + state: tauri::State<'_, WalletState>, +) -> Result { + mixnode_bonding_msg_payload(mixnode, cost_params, pledge, false, state).await +} + +#[tauri::command] +pub async fn vesting_generate_mixnode_bonding_msg_payload( + mixnode: MixNode, + cost_params: MixNodeCostParams, + pledge: DecCoin, + state: tauri::State<'_, WalletState>, +) -> Result { + mixnode_bonding_msg_payload(mixnode, cost_params, pledge, true, state).await +} + +#[tauri::command] +pub async fn generate_gateway_bonding_msg_payload( + gateway: Gateway, + pledge: DecCoin, + state: tauri::State<'_, WalletState>, +) -> Result { + gateway_bonding_msg_payload(gateway, pledge, false, state).await +} + +#[tauri::command] +pub async fn vesting_generate_gateway_bonding_msg_payload( + gateway: Gateway, + pledge: DecCoin, + state: tauri::State<'_, WalletState>, +) -> Result { + gateway_bonding_msg_payload(gateway, pledge, true, state).await +} diff --git a/nym-wallet/src-tauri/src/operations/signatures/mod.rs b/nym-wallet/src-tauri/src/operations/signatures/mod.rs index 66ccbc9eb6..5b98a61130 100644 --- a/nym-wallet/src-tauri/src/operations/signatures/mod.rs +++ b/nym-wallet/src-tauri/src/operations/signatures/mod.rs @@ -1 +1,2 @@ +pub mod ed25519_signing_payload; pub mod sign; diff --git a/nym-wallet/src-tauri/src/operations/simulate/mixnet.rs b/nym-wallet/src-tauri/src/operations/simulate/mixnet.rs index a971fea215..cd60f2687f 100644 --- a/nym-wallet/src-tauri/src/operations/simulate/mixnet.rs +++ b/nym-wallet/src-tauri/src/operations/simulate/mixnet.rs @@ -4,6 +4,7 @@ use crate::error::BackendError; use crate::operations::simulate::FeeDetails; use crate::WalletState; +use nym_contracts_common::signing::MessageSignature; use nym_mixnet_contract_common::MixNodeConfigUpdate; use nym_mixnet_contract_common::{ExecuteMsg, Gateway, MixId, MixNode}; use nym_types::currency::DecCoin; @@ -39,13 +40,13 @@ async fn simulate_mixnet_operation( pub async fn simulate_bond_gateway( gateway: Gateway, pledge: DecCoin, - owner_signature: String, + msg_signature: MessageSignature, state: tauri::State<'_, WalletState>, ) -> Result { simulate_mixnet_operation( ExecuteMsg::BondGateway { gateway, - owner_signature, + owner_signature: msg_signature, }, Some(pledge), &state, @@ -64,7 +65,7 @@ pub async fn simulate_unbond_gateway( pub async fn simulate_bond_mixnode( mixnode: MixNode, cost_params: MixNodeCostParams, - owner_signature: String, + msg_signature: MessageSignature, pledge: DecCoin, state: tauri::State<'_, WalletState>, ) -> Result { @@ -76,7 +77,7 @@ pub async fn simulate_bond_mixnode( ExecuteMsg::BondMixnode { mix_node: mixnode, cost_params, - owner_signature, + owner_signature: msg_signature, }, Some(pledge), &state, diff --git a/nym-wallet/src-tauri/src/operations/simulate/vesting.rs b/nym-wallet/src-tauri/src/operations/simulate/vesting.rs index b0616bb0ef..ecf1a53579 100644 --- a/nym-wallet/src-tauri/src/operations/simulate/vesting.rs +++ b/nym-wallet/src-tauri/src/operations/simulate/vesting.rs @@ -4,6 +4,7 @@ use crate::error::BackendError; use crate::operations::simulate::FeeDetails; use crate::WalletState; +use nym_contracts_common::signing::MessageSignature; use nym_mixnet_contract_common::MixNodeConfigUpdate; use nym_mixnet_contract_common::{Gateway, MixId, MixNode}; use nym_types::currency::DecCoin; @@ -40,7 +41,7 @@ async fn simulate_vesting_operation( pub async fn simulate_vesting_bond_gateway( gateway: Gateway, pledge: DecCoin, - owner_signature: String, + msg_signature: MessageSignature, state: tauri::State<'_, WalletState>, ) -> Result { let guard = state.read().await; @@ -49,7 +50,7 @@ pub async fn simulate_vesting_bond_gateway( simulate_vesting_operation( ExecuteMsg::BondGateway { gateway, - owner_signature, + owner_signature: msg_signature, amount, }, None, @@ -69,7 +70,7 @@ pub async fn simulate_vesting_unbond_gateway( pub async fn simulate_vesting_bond_mixnode( mixnode: MixNode, cost_params: MixNodeCostParams, - owner_signature: String, + msg_signature: MessageSignature, pledge: DecCoin, state: tauri::State<'_, WalletState>, ) -> Result { @@ -82,7 +83,7 @@ pub async fn simulate_vesting_bond_mixnode( ExecuteMsg::BondMixnode { mix_node: mixnode, cost_params, - owner_signature, + owner_signature: msg_signature, amount, }, None, diff --git a/nym-wallet/src-tauri/src/operations/vesting/bond.rs b/nym-wallet/src-tauri/src/operations/vesting/bond.rs index c884be5538..ad73b4ac6b 100644 --- a/nym-wallet/src-tauri/src/operations/vesting/bond.rs +++ b/nym-wallet/src-tauri/src/operations/vesting/bond.rs @@ -2,8 +2,12 @@ use crate::error::BackendError; use crate::nyxd_client; use crate::state::WalletState; use crate::{Gateway, MixNode}; +use nym_contracts_common::signing::MessageSignature; use nym_mixnet_contract_common::MixNodeConfigUpdate; +use crate::operations::helpers::{ + verify_gateway_bonding_sign_payload, verify_mixnode_bonding_sign_payload, +}; use nym_types::currency::DecCoin; use nym_types::mixnode::MixNodeCostParams; use nym_types::transaction::TransactionExecuteResult; @@ -13,7 +17,7 @@ use validator_client::nyxd::{Fee, VestingSigningClient}; pub async fn vesting_bond_gateway( gateway: Gateway, pledge: DecCoin, - owner_signature: String, + msg_signature: MessageSignature, fee: Option, state: tauri::State<'_, WalletState>, ) -> Result { @@ -28,10 +32,20 @@ pub async fn vesting_bond_gateway( pledge_base, fee, ); + + let client = guard.current_client()?; + // check the signature to make sure the user copied it correctly + if let Err(err) = + verify_gateway_bonding_sign_payload(client, &gateway, &pledge_base, true, &msg_signature) + .await + { + log::warn!("failed to verify provided gateway bonding signature: {err}"); + return Err(err); + } let res = guard .current_client()? .nyxd - .vesting_bond_gateway(gateway, &owner_signature, pledge_base, fee) + .vesting_bond_gateway(gateway, msg_signature, pledge_base, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); log::trace!("<<< {:?}", res); @@ -63,7 +77,7 @@ pub async fn vesting_unbond_gateway( pub async fn vesting_bond_mixnode( mixnode: MixNode, cost_params: MixNodeCostParams, - owner_signature: String, + msg_signature: MessageSignature, pledge: DecCoin, fee: Option, state: tauri::State<'_, WalletState>, @@ -81,10 +95,27 @@ pub async fn vesting_bond_mixnode( pledge_base, fee ); + + let client = guard.current_client()?; + // check the signature to make sure the user copied it correctly + if let Err(err) = verify_mixnode_bonding_sign_payload( + client, + &mixnode, + &cost_params, + &pledge_base, + true, + &msg_signature, + ) + .await + { + log::warn!("failed to verify provided mixnode bonding signature: {err}"); + return Err(err); + } + let res = guard .current_client()? .nyxd - .vesting_bond_mixnode(mixnode, cost_params, &owner_signature, pledge_base, fee) + .vesting_bond_mixnode(mixnode, cost_params, msg_signature, pledge_base, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); log::trace!("<<< {:?}", res); diff --git a/nym-wallet/src/components/Bonding/forms/BondGatewayForm.tsx b/nym-wallet/src/components/Bonding/forms/BondGatewayForm.tsx index 4ac0e47aaf..efb812d969 100644 --- a/nym-wallet/src/components/Bonding/forms/BondGatewayForm.tsx +++ b/nym-wallet/src/components/Bonding/forms/BondGatewayForm.tsx @@ -1,195 +1,11 @@ -import React, { useEffect, useState } from 'react'; -import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField'; -import { Box, Checkbox, FormControlLabel, Stack, TextField } from '@mui/material'; -import { useForm } from 'react-hook-form'; -import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField'; -import { NodeTypeSelector, TokenPoolSelector } from 'src/components'; -import { yupResolver } from '@hookform/resolvers/yup'; -import { checkHasEnoughFunds, checkHasEnoughLockedTokens } from 'src/utils'; +import React from 'react'; +import { Box } from '@mui/material'; +import { NodeTypeSelector } from 'src/components'; import { CurrencyDenom, TNodeType } from '@nymproject/types'; -import { GatewayAmount, GatewayData } from 'src/pages/bonding/types'; -import { gatewayValidationSchema, amountSchema } from './gatewayValidationSchema'; - -const NodeFormData = ({ gatewayData, onNext }: { gatewayData: GatewayData; onNext: (data: GatewayData) => void }) => { - const [showAdvancedOptions, setShowAdvancedOptions] = useState(false); - - const { - register, - formState: { errors }, - handleSubmit, - setValue, - } = useForm({ resolver: yupResolver(gatewayValidationSchema), defaultValues: gatewayData }); - - const handleRequestValidation = (event: { detail: { step: number } }) => { - if (event.detail.step === 1) { - handleSubmit(onNext)(); - } - }; - - useEffect(() => { - window.addEventListener('validate_bond_gateway_step' as any, handleRequestValidation); - return () => window.removeEventListener('validate_bond_gateway_step' as any, handleRequestValidation); - }, []); - - return ( - - setValue('identityKey', value)} - /> - - - - - - - - setShowAdvancedOptions((show) => !show)} checked={showAdvancedOptions} />} - label="Show advanced options" - /> - {showAdvancedOptions && ( - - - - - )} - - ); -}; - -const AmountFormData = ({ - denom, - amountData, - hasVestingTokens, - onNext, -}: { - denom: CurrencyDenom; - amountData: GatewayAmount; - hasVestingTokens: boolean; - onNext: (data: any) => void; -}) => { - const { - formState: { errors }, - handleSubmit, - setValue, - getValues, - setError, - } = useForm({ resolver: yupResolver(amountSchema), defaultValues: amountData }); - - const handleRequestValidation = async (event: { detail: { step: number } }) => { - let hasSufficientTokens = true; - const values = getValues(); - - if (values.tokenPool === 'balance') { - hasSufficientTokens = await checkHasEnoughFunds(values.amount.amount); - } - - if (values.tokenPool === 'locked') { - hasSufficientTokens = await checkHasEnoughLockedTokens(values.amount.amount); - } - - if (event.detail.step === 2 && hasSufficientTokens) { - handleSubmit(onNext)(); - } else { - setError('amount.amount', { message: 'Not enough tokens' }); - } - }; - - useEffect(() => { - window.addEventListener('validate_bond_gateway_step' as any, handleRequestValidation); - return () => window.removeEventListener('validate_bond_gateway_step' as any, handleRequestValidation); - }, []); - - return ( - - - {hasVestingTokens && setValue('tokenPool', pool)} />} - setValue('amount', newValue, { shouldValidate: true })} - validationError={errors.amount?.amount?.message} - denom={denom} - initialValue={amountData.amount.amount} - /> - setValue('operatorCost', newValue, { shouldValidate: true })} - validationError={errors.operatorCost?.amount?.message} - denom={denom} - initialValue={amountData.operatorCost.amount} - /> - - - ); -}; +import { GatewayAmount, GatewayData, Signature } from 'src/pages/bonding/types'; +import GatewayInitForm from './GatewayInitForm'; +import GatewayAmountForm from './GatewayAmountForm'; +import GatewaySignatureForm from './GatewaySignatureForm'; export const BondGatewayForm = ({ step, @@ -197,18 +13,20 @@ export const BondGatewayForm = ({ gatewayData, amountData, hasVestingTokens, + onSelectNodeType, onValidateGatewayData, onValidateAmountData, - onSelectNodeType, + onValidateSignature, }: { - step: 1 | 2 | 3; + step: 1 | 2 | 3 | 4; gatewayData: GatewayData; amountData: GatewayAmount; denom: CurrencyDenom; hasVestingTokens: boolean; + onSelectNodeType: (nodeType: TNodeType) => void; onValidateGatewayData: (data: GatewayData) => void; onValidateAmountData: (data: GatewayAmount) => Promise; - onSelectNodeType: (nodeType: TNodeType) => void; + onValidateSignature: (signature: Signature) => void; }) => ( <> {step === 1 && ( @@ -216,16 +34,17 @@ export const BondGatewayForm = ({ - + )} {step === 2 && ( - )} + {step === 3 && } ); diff --git a/nym-wallet/src/components/Bonding/forms/BondMixnodeForm.tsx b/nym-wallet/src/components/Bonding/forms/BondMixnodeForm.tsx index 2f8864a017..274878310f 100644 --- a/nym-wallet/src/components/Bonding/forms/BondMixnodeForm.tsx +++ b/nym-wallet/src/components/Bonding/forms/BondMixnodeForm.tsx @@ -1,233 +1,11 @@ -import React, { useContext, useEffect, useState } from 'react'; -import { useForm } from 'react-hook-form'; -import { yupResolver } from '@hookform/resolvers/yup'; -import { Box, Checkbox, FormControlLabel, FormHelperText, Stack, TextField, Typography } from '@mui/material'; -import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField'; -import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField'; +import React from 'react'; +import { Box } from '@mui/material'; import { CurrencyDenom, TNodeType } from '@nymproject/types'; -import { checkHasEnoughFunds, checkHasEnoughLockedTokens } from 'src/utils'; -import { NodeTypeSelector, TokenPoolSelector } from 'src/components'; -import { MixnodeAmount, MixnodeData } from 'src/pages/bonding/types'; -import { ModalListItem } from 'src/components/Modals/ModalListItem'; -import { AppContext } from 'src/context'; -import { amountSchema, mixnodeValidationSchema } from './mixnodeValidationSchema'; - -const NodeFormData = ({ mixnodeData, onNext }: { mixnodeData: MixnodeData; onNext: (data: any) => void }) => { - const [showAdvancedOptions, setShowAdvancedOptions] = useState(false); - - const { - register, - formState: { errors }, - handleSubmit, - setValue, - } = useForm({ resolver: yupResolver(mixnodeValidationSchema), defaultValues: mixnodeData }); - - const handleRequestValidation = (event: { detail: { step: number } }) => { - if (event.detail.step === 1) { - handleSubmit(onNext)(); - } - }; - - useEffect(() => { - window.addEventListener('validate_bond_mixnode_step' as any, handleRequestValidation); - return () => window.removeEventListener('validate_bond_mixnode_step' as any, handleRequestValidation); - }, []); - - return ( - - setValue('identityKey', value)} - /> - - - - - - - setShowAdvancedOptions((show) => !show)} checked={showAdvancedOptions} />} - label="Show advanced options" - /> - {showAdvancedOptions && ( - - - - - - )} - - ); -}; - -const AmountFormData = ({ - amountData, - hasVestingTokens, - denom, - onNext, -}: { - amountData: MixnodeAmount; - hasVestingTokens: boolean; - denom: CurrencyDenom; - onNext: (data: MixnodeAmount) => void; -}) => { - const { - register, - formState: { errors }, - handleSubmit, - setValue, - getValues, - setError, - } = useForm({ resolver: yupResolver(amountSchema), defaultValues: amountData }); - - const { userBalance } = useContext(AppContext); - - const handleRequestValidation = async (event: { detail: { step: number } }) => { - let hasSufficientTokens = true; - const values = getValues(); - - if (values.tokenPool === 'balance') { - hasSufficientTokens = await checkHasEnoughFunds(values.amount.amount); - } - - if (values.tokenPool === 'locked') { - hasSufficientTokens = await checkHasEnoughLockedTokens(values.amount.amount); - } - - if (event.detail.step === 2 && hasSufficientTokens) { - handleSubmit(onNext)(); - } else { - setError('amount.amount', { message: 'Not enough tokens' }); - } - }; - - useEffect(() => { - window.addEventListener('validate_bond_mixnode_step' as any, handleRequestValidation); - return () => window.removeEventListener('validate_bond_mixnode_step' as any, handleRequestValidation); - }, []); - - return ( - - - {hasVestingTokens && setValue('tokenPool', pool)} />} - { - setValue('amount', newValue, { shouldValidate: true }); - }} - validationError={errors.amount?.amount?.message} - denom={denom} - initialValue={amountData.amount.amount} - /> - - - { - setValue('operatorCost', newValue, { shouldValidate: true }); - }} - validationError={errors.operatorCost?.amount?.message} - denom={denom} - initialValue={amountData.operatorCost.amount} - /> - - Monthly operational costs of running your node. If your node is in the active set the amount will be paid back - to you from the rewards. - - - - - - The percentage of node rewards that you as the node operator take before rewards are distributed to operator - and delegators. - - - - {!hasVestingTokens && ( - - )} - Est. fee for this transaction will be calculated in the next page - - - ); -}; +import { NodeTypeSelector } from 'src/components'; +import { MixnodeAmount, MixnodeData, Signature } from 'src/pages/bonding/types'; +import MixnodeInitForm from './MixnodeInitForm'; +import MixnodeAmountForm from './MixnodeAmountForm'; +import MixnodeSignatureForm from './MixnodeSignatureForm'; export const BondMixnodeForm = ({ step, @@ -235,18 +13,20 @@ export const BondMixnodeForm = ({ mixnodeData, amountData, hasVestingTokens, + onSelectNodeType, onValidateMixnodeData, onValidateAmountData, - onSelectNodeType, + onValidateSignature, }: { - step: 1 | 2 | 3; + step: 1 | 2 | 3 | 4; mixnodeData: MixnodeData; amountData: MixnodeAmount; denom: CurrencyDenom; hasVestingTokens: boolean; + onSelectNodeType: (nodeType: TNodeType) => void; onValidateMixnodeData: (data: MixnodeData) => void; onValidateAmountData: (data: MixnodeAmount) => Promise; - onSelectNodeType: (nodeType: TNodeType) => void; + onValidateSignature: (signature: Signature) => void; }) => ( <> {step === 1 && ( @@ -254,16 +34,17 @@ export const BondMixnodeForm = ({ - + )} {step === 2 && ( - )} + {step === 3 && } ); diff --git a/nym-wallet/src/components/Bonding/forms/GatewayAmountForm.tsx b/nym-wallet/src/components/Bonding/forms/GatewayAmountForm.tsx new file mode 100644 index 0000000000..2cef9a59da --- /dev/null +++ b/nym-wallet/src/components/Bonding/forms/GatewayAmountForm.tsx @@ -0,0 +1,84 @@ +import React, { useEffect } from 'react'; +import { CurrencyDenom } from '@nymproject/types'; +import { useForm } from 'react-hook-form'; +import { yupResolver } from '@hookform/resolvers/yup/dist/yup'; +import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField'; +import { Box, Stack } from '@mui/material'; +import { amountSchema } from './gatewayValidationSchema'; +import { checkHasEnoughFunds, checkHasEnoughLockedTokens } from '../../../utils'; +import { GatewayAmount } from '../../../pages/bonding/types'; +import { TokenPoolSelector } from '../../TokenPoolSelector'; + +const GatewayAmountForm = ({ + denom, + amountData, + hasVestingTokens, + onNext, +}: { + denom: CurrencyDenom; + amountData: GatewayAmount; + hasVestingTokens: boolean; + onNext: (data: any) => void; +}) => { + const { + formState: { errors }, + handleSubmit, + setValue, + getValues, + setError, + } = useForm({ resolver: yupResolver(amountSchema), defaultValues: amountData }); + + const handleRequestValidation = async (event: { detail: { step: number } }) => { + let hasSufficientTokens = true; + const values = getValues(); + + if (values.tokenPool === 'balance') { + hasSufficientTokens = await checkHasEnoughFunds(values.amount.amount); + } + + if (values.tokenPool === 'locked') { + hasSufficientTokens = await checkHasEnoughLockedTokens(values.amount.amount); + } + + if (event.detail.step === 2 && hasSufficientTokens) { + handleSubmit(onNext)(); + } else { + setError('amount.amount', { message: 'Not enough tokens' }); + } + }; + + useEffect(() => { + window.addEventListener('validate_bond_gateway_step' as any, handleRequestValidation); + return () => window.removeEventListener('validate_bond_gateway_step' as any, handleRequestValidation); + }, []); + + return ( + + + {hasVestingTokens && setValue('tokenPool', pool)} />} + setValue('amount', newValue, { shouldValidate: true })} + validationError={errors.amount?.amount?.message} + denom={denom} + initialValue={amountData.amount.amount} + /> + setValue('operatorCost', newValue, { shouldValidate: true })} + validationError={errors.operatorCost?.amount?.message} + denom={denom} + initialValue={amountData.operatorCost.amount} + /> + + + ); +}; + +export default GatewayAmountForm; diff --git a/nym-wallet/src/components/Bonding/forms/GatewayInitForm.tsx b/nym-wallet/src/components/Bonding/forms/GatewayInitForm.tsx new file mode 100644 index 0000000000..960c1ca554 --- /dev/null +++ b/nym-wallet/src/components/Bonding/forms/GatewayInitForm.tsx @@ -0,0 +1,116 @@ +import React, { useEffect, useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { Checkbox, FormControlLabel, Stack, TextField } from '@mui/material'; +import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField'; +import { yupResolver } from '@hookform/resolvers/yup/dist/yup'; +import { GatewayData } from '../../../pages/bonding/types'; +import { gatewayValidationSchema } from './gatewayValidationSchema'; + +const GatewayInitForm = ({ + gatewayData, + onNext, +}: { + gatewayData: GatewayData; + onNext: (data: GatewayData) => void; +}) => { + const [showAdvancedOptions, setShowAdvancedOptions] = useState(false); + + const { + register, + formState: { errors }, + handleSubmit, + setValue, + } = useForm({ resolver: yupResolver(gatewayValidationSchema), defaultValues: gatewayData }); + + const handleRequestValidation = (event: { detail: { step: number } }) => { + if (event.detail.step === 1) { + handleSubmit(onNext)(); + } + }; + + useEffect(() => { + window.addEventListener('validate_bond_gateway_step' as any, handleRequestValidation); + return () => window.removeEventListener('validate_bond_gateway_step' as any, handleRequestValidation); + }, []); + + return ( + + setValue('identityKey', value)} + /> + + + + + + + setShowAdvancedOptions((show) => !show)} checked={showAdvancedOptions} />} + label="Show advanced options" + /> + {showAdvancedOptions && ( + + + + + )} + + ); +}; + +export default GatewayInitForm; diff --git a/nym-wallet/src/components/Bonding/forms/GatewaySignatureForm.tsx b/nym-wallet/src/components/Bonding/forms/GatewaySignatureForm.tsx new file mode 100644 index 0000000000..dbc9e6bb1c --- /dev/null +++ b/nym-wallet/src/components/Bonding/forms/GatewaySignatureForm.tsx @@ -0,0 +1,87 @@ +import React, { useEffect, useState } from 'react'; +import { Stack, TextField, Typography } from '@mui/material'; +import { useForm } from 'react-hook-form'; +import { gatewayToTauri } from '../utils'; +import { CopyToClipboard } from '../../CopyToClipboard'; +import { useBondingContext } from '../../../context'; +import { Console } from '../../../utils/console'; +import { ErrorModal } from '../../Modals/ErrorModal'; +import { GatewayData, GatewayAmount, Signature } from '../../../pages/bonding/types'; + +const GatewaySignatureForm = ({ + gateway, + amount, + onNext, +}: { + gateway: GatewayData; + amount: GatewayAmount; + onNext: (data: Signature) => void; +}) => { + const [message, setMessage] = useState(); + const [error, setError] = useState(); + const { generateGatewayMsgPayload } = useBondingContext(); + + const { register, handleSubmit } = useForm(); + + const handleOnNext = (event: { detail: { step: number } }) => { + if (event.detail.step === 3) { + handleSubmit(onNext)(); + } + }; + + useEffect(() => { + window.addEventListener('validate_bond_gateway_step' as any, handleOnNext); + return () => window.removeEventListener('validate_bond_gateway_step' as any, handleOnNext); + }, []); + + const generateMessage = async () => { + try { + setMessage( + await generateGatewayMsgPayload({ + pledge: amount.amount, + gateway: gatewayToTauri(gateway), + }), + ); + } catch (e) { + Console.error(e); + setError('Something went wrong while generating the payload signature'); + } + }; + + useEffect(() => { + generateMessage(); + }, [gateway, amount]); + + if (error) { + return {}} />; + } + + return ( + + + Copy below message and sign it with your gateway using the following command +
+ nym-gateway sign --id <your-node-id> --contract-msg <payload-generated-by-the-wallet> +
+ Then paste the signature in the next field. +
+ + + Copy Message + {message && } + + +
+ ); +}; + +export default GatewaySignatureForm; diff --git a/nym-wallet/src/components/Bonding/forms/MixnodeAmountForm.tsx b/nym-wallet/src/components/Bonding/forms/MixnodeAmountForm.tsx new file mode 100644 index 0000000000..42a8d88061 --- /dev/null +++ b/nym-wallet/src/components/Bonding/forms/MixnodeAmountForm.tsx @@ -0,0 +1,123 @@ +import React, { useContext, useEffect } from 'react'; +import { useForm } from 'react-hook-form'; +import { Box, FormHelperText, Stack, TextField, Typography } from '@mui/material'; +import { yupResolver } from '@hookform/resolvers/yup/dist/yup'; +import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField'; +import { CurrencyDenom } from '@nymproject/types'; +import { amountSchema } from './mixnodeValidationSchema'; +import { MixnodeAmount } from '../../../pages/bonding/types'; +import { AppContext } from '../../../context'; +import { checkHasEnoughFunds, checkHasEnoughLockedTokens } from '../../../utils'; +import { TokenPoolSelector } from '../../TokenPoolSelector'; +import { ModalListItem } from '../../Modals/ModalListItem'; + +const MixnodeAmountForm = ({ + amountData, + hasVestingTokens, + denom, + onNext, +}: { + amountData: MixnodeAmount; + hasVestingTokens: boolean; + denom: CurrencyDenom; + onNext: (data: MixnodeAmount) => void; +}) => { + const { + register, + formState: { errors }, + handleSubmit, + setValue, + getValues, + setError, + } = useForm({ resolver: yupResolver(amountSchema), defaultValues: amountData }); + + const { userBalance } = useContext(AppContext); + + const handleRequestValidation = async (event: { detail: { step: number } }) => { + let hasSufficientTokens = true; + const values = getValues(); + + if (values.tokenPool === 'balance') { + hasSufficientTokens = await checkHasEnoughFunds(values.amount.amount); + } + + if (values.tokenPool === 'locked') { + hasSufficientTokens = await checkHasEnoughLockedTokens(values.amount.amount); + } + + if (event.detail.step === 2 && hasSufficientTokens) { + handleSubmit(onNext)(); + } else { + setError('amount.amount', { message: 'Not enough tokens' }); + } + }; + + useEffect(() => { + window.addEventListener('validate_bond_mixnode_step' as any, handleRequestValidation); + return () => window.removeEventListener('validate_bond_mixnode_step' as any, handleRequestValidation); + }, []); + + return ( + + + {hasVestingTokens && setValue('tokenPool', pool)} />} + { + setValue('amount', newValue, { shouldValidate: true }); + }} + validationError={errors.amount?.amount?.message} + denom={denom} + initialValue={amountData.amount.amount} + /> + + + { + setValue('operatorCost', newValue, { shouldValidate: true }); + }} + validationError={errors.operatorCost?.amount?.message} + denom={denom} + initialValue={amountData.operatorCost.amount} + /> + + Monthly operational costs of running your node. If your node is in the active set the amount will be paid back + to you from the rewards. + + + + + + The percentage of node rewards that you as the node operator take before rewards are distributed to operator + and delegators. + + + + {!hasVestingTokens && ( + + )} + Est. fee for this transaction will be calculated in the next page + + + ); +}; + +export default MixnodeAmountForm; diff --git a/nym-wallet/src/components/Bonding/forms/MixnodeInitForm.tsx b/nym-wallet/src/components/Bonding/forms/MixnodeInitForm.tsx new file mode 100644 index 0000000000..bdb10e91dc --- /dev/null +++ b/nym-wallet/src/components/Bonding/forms/MixnodeInitForm.tsx @@ -0,0 +1,109 @@ +import React, { useEffect, useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { Checkbox, FormControlLabel, Stack, TextField } from '@mui/material'; +import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField'; +import { yupResolver } from '@hookform/resolvers/yup/dist/yup'; +import { mixnodeValidationSchema } from './mixnodeValidationSchema'; +import { MixnodeData } from '../../../pages/bonding/types'; + +const MixnodeInitForm = ({ mixnodeData, onNext }: { mixnodeData: MixnodeData; onNext: (data: any) => void }) => { + const [showAdvancedOptions, setShowAdvancedOptions] = useState(false); + + const { + register, + formState: { errors }, + handleSubmit, + setValue, + } = useForm({ resolver: yupResolver(mixnodeValidationSchema), defaultValues: mixnodeData }); + + const handleRequestValidation = (event: { detail: { step: number } }) => { + if (event.detail.step === 1) { + handleSubmit(onNext)(); + } + }; + + useEffect(() => { + window.addEventListener('validate_bond_mixnode_step' as any, handleRequestValidation); + return () => window.removeEventListener('validate_bond_mixnode_step' as any, handleRequestValidation); + }, []); + + return ( + + setValue('identityKey', value)} + /> + + + + + + setShowAdvancedOptions((show) => !show)} checked={showAdvancedOptions} />} + label="Show advanced options" + /> + {showAdvancedOptions && ( + + + + + + )} + + ); +}; + +export default MixnodeInitForm; diff --git a/nym-wallet/src/components/Bonding/forms/MixnodeSignatureForm.tsx b/nym-wallet/src/components/Bonding/forms/MixnodeSignatureForm.tsx new file mode 100644 index 0000000000..294c478d59 --- /dev/null +++ b/nym-wallet/src/components/Bonding/forms/MixnodeSignatureForm.tsx @@ -0,0 +1,88 @@ +import React, { useEffect, useState } from 'react'; +import { Stack, TextField, Typography } from '@mui/material'; +import { useForm } from 'react-hook-form'; +import { costParamsToTauri, mixnodeToTauri } from '../utils'; +import { CopyToClipboard } from '../../CopyToClipboard'; +import { useBondingContext } from '../../../context'; +import { Console } from '../../../utils/console'; +import { ErrorModal } from '../../Modals/ErrorModal'; +import { MixnodeAmount, MixnodeData, Signature } from '../../../pages/bonding/types'; + +const MixnodeSignatureForm = ({ + mixnode, + amount, + onNext, +}: { + mixnode: MixnodeData; + amount: MixnodeAmount; + onNext: (data: Signature) => void; +}) => { + const [message, setMessage] = useState(''); + const [error, setError] = useState(); + const { generateMixnodeMsgPayload } = useBondingContext(); + + const { register, handleSubmit } = useForm(); + + const handleOnNext = (event: { detail: { step: number } }) => { + if (event.detail.step === 3) { + handleSubmit(onNext)(); + } + }; + + useEffect(() => { + window.addEventListener('validate_bond_mixnode_step' as any, handleOnNext); + return () => window.removeEventListener('validate_bond_mixnode_step' as any, handleOnNext); + }, []); + + const generateMessage = async () => { + try { + setMessage( + (await generateMixnodeMsgPayload({ + pledge: amount.amount, + mixnode: mixnodeToTauri(mixnode), + costParams: costParamsToTauri(amount), + })) as string, + ); + } catch (e) { + Console.error(e); + setError('Something went wrong while generating the payload signature'); + } + }; + + useEffect(() => { + generateMessage(); + }, [mixnode, amount]); + + if (error) { + return {}} />; + } + + return ( + + + Copy below message and sign it with your mix node using the following command +
+ nym-mixnode sign --id <your-node-id> --contract-msg <payload-generated-by-the-wallet> +
+ Then paste the signature in the next field. +
+ + + Copy Message + {message && } + + +
+ ); +}; + +export default MixnodeSignatureForm; diff --git a/nym-wallet/src/components/Bonding/forms/gatewayValidationSchema.ts b/nym-wallet/src/components/Bonding/forms/gatewayValidationSchema.ts index 82de5cd3a2..5f70b7bf6e 100644 --- a/nym-wallet/src/components/Bonding/forms/gatewayValidationSchema.ts +++ b/nym-wallet/src/components/Bonding/forms/gatewayValidationSchema.ts @@ -18,10 +18,6 @@ export const gatewayValidationSchema = Yup.object().shape({ .required('A sphinx key is required') .test('valid-sphinx-key', 'A valid sphinx key is required', (value) => validateKey(value || '', 32)), - ownerSignature: Yup.string() - .required('Signature is required') - .test('valid-signature', 'A valid signature is required', (value) => validateKey(value || '', 64)), - host: Yup.string() .required('A host is required') .test('valid-host', 'A valid host is required', (value) => (value ? isValidHostname(value) : false)), diff --git a/nym-wallet/src/components/Bonding/forms/mixnodeValidationSchema.ts b/nym-wallet/src/components/Bonding/forms/mixnodeValidationSchema.ts index 2289760520..601731bd3e 100644 --- a/nym-wallet/src/components/Bonding/forms/mixnodeValidationSchema.ts +++ b/nym-wallet/src/components/Bonding/forms/mixnodeValidationSchema.ts @@ -10,10 +10,6 @@ export const mixnodeValidationSchema = Yup.object().shape({ .required('A sphinx key is required') .test('valid-sphinx-key', 'A valid sphinx key is required', (value) => validateKey(value || '', 32)), - ownerSignature: Yup.string() - .required('Signature is required') - .test('valid-signature', 'A valid signature is required', (value) => validateKey(value || '', 64)), - host: Yup.string() .required('A host is required') .test('valid-host', 'A valid host is required', (value) => (value ? isValidHostname(value) : false)), diff --git a/nym-wallet/src/components/Bonding/modals/BondGatewayModal.tsx b/nym-wallet/src/components/Bonding/modals/BondGatewayModal.tsx index d983da0f51..0a3cd718a5 100644 --- a/nym-wallet/src/components/Bonding/modals/BondGatewayModal.tsx +++ b/nym-wallet/src/components/Bonding/modals/BondGatewayModal.tsx @@ -6,10 +6,11 @@ import { ModalListItem } from 'src/components/Modals/ModalListItem'; import { SimpleModal } from 'src/components/Modals/SimpleModal'; import { TPoolOption } from 'src/components/TokenPoolSelector'; import { useGetFee } from 'src/hooks/useGetFee'; -import { GatewayAmount, GatewayData } from 'src/pages/bonding/types'; +import { GatewayAmount, GatewayData, Signature } from 'src/pages/bonding/types'; import { simulateBondGateway, simulateVestingBondGateway } from 'src/requests'; import { TBondGatewayArgs } from 'src/types'; import { BondGatewayForm } from '../forms/BondGatewayForm'; +import { gatewayToTauri } from '../utils'; const defaultGatewayValues: GatewayData = { identityKey: '', @@ -46,6 +47,7 @@ export const BondGatewayModal = ({ const [step, setStep] = useState<1 | 2 | 3>(1); const [gatewayData, setGatewayData] = useState(defaultGatewayValues); const [amountData, setAmountData] = useState(defaultAmountValues(denom)); + const [signature, setSignature] = useState(); const { fee, getFee, resetFeeState, feeError } = useGetFee(); @@ -61,7 +63,11 @@ export const BondGatewayModal = ({ }; const handleBack = () => { - setStep(1); + if (step === 2) { + setStep(1); + } else if (step === 3) { + setStep(2); + } }; const handleUpdateGatwayData = (data: GatewayData) => { @@ -71,22 +77,19 @@ export const BondGatewayModal = ({ const handleUpdateAmountData = async (data: GatewayAmount) => { setAmountData(data); + setStep(3); + }; + + const handleUpdateSignature = async (data: Signature) => { + setSignature(data.signature); + const payload = { - pledge: data.amount, - ownerSignature: gatewayData.ownerSignature, - gateway: { - ...gatewayData, - host: gatewayData.host, - version: gatewayData.version, - mix_port: gatewayData.mixPort, - clients_port: gatewayData.clientsPort, - sphinx_key: gatewayData.sphinxKey, - identity_key: gatewayData.identityKey, - location: gatewayData.location, - }, + pledge: amountData.amount, + msgSignature: data.signature, + gateway: gatewayToTauri(gatewayData), }; - if (data.tokenPool === 'balance') { + if (amountData.tokenPool === 'balance') { await getFee(simulateBondGateway, payload); } else { await getFee(simulateVestingBondGateway, payload); @@ -97,17 +100,8 @@ export const BondGatewayModal = ({ await onBondGateway( { pledge: amountData.amount, - ownerSignature: gatewayData.ownerSignature, - gateway: { - ...gatewayData, - host: gatewayData.host, - version: gatewayData.version, - mix_port: gatewayData.mixPort, - clients_port: gatewayData.clientsPort, - sphinx_key: gatewayData.sphinxKey, - identity_key: gatewayData.identityKey, - location: gatewayData.location, - }, + msgSignature: signature as string, + gateway: gatewayToTauri(gatewayData), }, amountData.tokenPool as TPoolOption, ); @@ -142,7 +136,7 @@ export const BondGatewayModal = ({ onBack={step === 2 ? handleBack : undefined} onClose={onClose} header="Bond gateway" - subHeader={`Step ${step}/2`} + subHeader={`Step ${step}/3`} okLabel="Next" > @@ -154,6 +148,7 @@ export const BondGatewayModal = ({ hasVestingTokens={hasVestingTokens} onValidateGatewayData={handleUpdateGatwayData} onValidateAmountData={handleUpdateAmountData} + onValidateSignature={handleUpdateSignature} onSelectNodeType={onSelectNodeType} /> diff --git a/nym-wallet/src/components/Bonding/modals/BondMixnodeModal.tsx b/nym-wallet/src/components/Bonding/modals/BondMixnodeModal.tsx index fcc7685f87..6cad10f9ab 100644 --- a/nym-wallet/src/components/Bonding/modals/BondMixnodeModal.tsx +++ b/nym-wallet/src/components/Bonding/modals/BondMixnodeModal.tsx @@ -5,11 +5,11 @@ import { ModalListItem } from 'src/components/Modals/ModalListItem'; import { SimpleModal } from 'src/components/Modals/SimpleModal'; import { TPoolOption } from 'src/components/TokenPoolSelector'; import { useGetFee } from 'src/hooks/useGetFee'; -import { MixnodeAmount, MixnodeData } from 'src/pages/bonding/types'; +import { MixnodeAmount, MixnodeData, Signature } from 'src/pages/bonding/types'; import { simulateBondMixnode, simulateVestingBondMixnode } from 'src/requests'; import { TBondMixNodeArgs } from 'src/types'; import { BondMixnodeForm } from '../forms/BondMixnodeForm'; -import { toPercentFloatString } from '../../../utils'; +import { costParamsToTauri, mixnodeToTauri } from '../utils'; const defaultMixnodeValues: MixnodeData = { identityKey: '', @@ -44,9 +44,10 @@ export const BondMixnodeModal = ({ onClose: () => void; onError: (e: string) => void; }) => { - const [step, setStep] = useState<1 | 2 | 3>(1); + const [step, setStep] = useState<1 | 2 | 3 | 4>(1); const [mixnodeData, setMixnodeData] = useState(defaultMixnodeValues); const [amountData, setAmountData] = useState(defaultAmountValues(denom)); + const [signature, setSignature] = useState(); const { fee, getFee, resetFeeState, feeError } = useGetFee(); @@ -62,7 +63,11 @@ export const BondMixnodeModal = ({ }; const handleBack = () => { - setStep(1); + if (step === 2) { + setStep(1); + } else if (step === 3) { + setStep(2); + } }; const handleUpdateMixnodeData = (data: MixnodeData) => { @@ -72,28 +77,20 @@ export const BondMixnodeModal = ({ const handleUpdateAmountData = async (data: MixnodeAmount) => { setAmountData({ ...data }); + setStep(3); + }; + + const handleUpdateSignature = async (data: Signature) => { + setSignature(data.signature); const payload = { - pledge: data.amount, - ownerSignature: mixnodeData.ownerSignature, - mixnode: { - ...mixnodeData, - mix_port: mixnodeData.mixPort, - http_api_port: mixnodeData.httpApiPort, - verloc_port: mixnodeData.verlocPort, - sphinx_key: mixnodeData.sphinxKey, - identity_key: mixnodeData.identityKey, - }, - costParams: { - profit_margin_percent: toPercentFloatString(data.profitMargin), - interval_operating_cost: { - amount: data.operatorCost.amount.toString(), - denom: data.operatorCost.denom, - }, - }, + pledge: amountData.amount, + msgSignature: data.signature, + mixnode: mixnodeToTauri(mixnodeData), + costParams: costParamsToTauri(amountData), }; - if (data.tokenPool === 'balance') { + if (amountData.tokenPool === 'balance') { await getFee(simulateBondMixnode, payload); } else { await getFee(simulateVestingBondMixnode, payload); @@ -104,22 +101,9 @@ export const BondMixnodeModal = ({ await onBondMixnode( { pledge: amountData.amount, - ownerSignature: mixnodeData.ownerSignature, - mixnode: { - ...mixnodeData, - mix_port: mixnodeData.mixPort, - http_api_port: mixnodeData.httpApiPort, - verloc_port: mixnodeData.verlocPort, - sphinx_key: mixnodeData.sphinxKey, - identity_key: mixnodeData.identityKey, - }, - costParams: { - profit_margin_percent: toPercentFloatString(amountData.profitMargin), - interval_operating_cost: { - amount: amountData.operatorCost.amount, - denom: amountData.operatorCost.denom, - }, - }, + msgSignature: signature as string, + mixnode: mixnodeToTauri(mixnodeData), + costParams: costParamsToTauri(amountData), }, amountData.tokenPool as TPoolOption, ); @@ -151,10 +135,10 @@ export const BondMixnodeModal = ({ onOk={async () => { await validateStep(step); }} - onBack={step === 2 ? handleBack : undefined} + onBack={step === 2 || step === 3 ? handleBack : undefined} onClose={onClose} header="Bond mixnode" - subHeader={`Step ${step}/2`} + subHeader={`Step ${step}/3`} okLabel="Next" > diff --git a/nym-wallet/src/components/Bonding/utils.ts b/nym-wallet/src/components/Bonding/utils.ts new file mode 100644 index 0000000000..d3285215f6 --- /dev/null +++ b/nym-wallet/src/components/Bonding/utils.ts @@ -0,0 +1,37 @@ +import { Gateway, MixNode, MixNodeCostParams } from '@nymproject/types'; +import { GatewayData, MixnodeAmount, MixnodeData } from '../../pages/bonding/types'; +import { toPercentFloatString } from '../../utils'; + +export function mixnodeToTauri(data: MixnodeData): MixNode { + return { + ...data, + mix_port: data.mixPort, + http_api_port: data.httpApiPort, + verloc_port: data.verlocPort, + sphinx_key: data.sphinxKey, + identity_key: data.identityKey, + }; +} + +export function costParamsToTauri(data: MixnodeAmount): MixNodeCostParams { + return { + profit_margin_percent: toPercentFloatString(data.profitMargin), + interval_operating_cost: { + amount: data.operatorCost.amount.toString(), + denom: data.operatorCost.denom, + }, + }; +} + +export function gatewayToTauri(data: GatewayData): Gateway { + return { + ...data, + host: data.host, + version: data.version, + mix_port: data.mixPort, + clients_port: data.clientsPort, + sphinx_key: data.sphinxKey, + identity_key: data.identityKey, + location: data.location, + }; +} diff --git a/nym-wallet/src/context/bonding.tsx b/nym-wallet/src/context/bonding.tsx index 48d8d8409d..6ce7a58a53 100644 --- a/nym-wallet/src/context/bonding.tsx +++ b/nym-wallet/src/context/bonding.tsx @@ -9,7 +9,15 @@ import { } from '@nymproject/types'; import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; import Big from 'big.js'; -import { isGateway, isMixnode, TBondGatewayArgs, TBondMixNodeArgs, TBondMoreArgs } from 'src/types'; +import { + isGateway, + isMixnode, + TBondGatewayArgs, + TBondGatewaySignatureArgs, + TBondMixNodeArgs, + TBondMixnodeSignatureArgs, + TBondMoreArgs, +} from 'src/types'; import { Console } from 'src/utils/console'; import { bondGateway as bondGatewayRequest, @@ -37,6 +45,10 @@ import { getMixnodeRewardEstimation, getGatewayReport, getMixnodeUptime, + vestingGenerateMixnodeMsgPayload as vestingGenerateMixnodeMsgPayloadReq, + generateMixnodeMsgPayload as generateMixnodeMsgPayloadReq, + vestingGenerateGatewayMsgPayload as vestingGenerateGatewayMsgPayloadReq, + generateGatewayMsgPayload as generateGatewayMsgPayloadReq, } from '../requests'; import { useCheckOwnership } from '../hooks/useCheckOwnership'; import { AppContext } from './main'; @@ -109,6 +121,8 @@ export type TBondingContext = { redeemRewards: (fee?: FeeDetails) => Promise; updateMixnode: (pm: string, fee?: FeeDetails) => Promise; checkOwnership: () => Promise; + generateMixnodeMsgPayload: (data: TBondMixnodeSignatureArgs) => Promise; + generateGatewayMsgPayload: (data: TBondGatewaySignatureArgs) => Promise; }; export const BondingContext = createContext({ @@ -135,6 +149,12 @@ export const BondingContext = createContext({ checkOwnership(): Promise { throw new Error('Not implemented'); }, + generateMixnodeMsgPayload: async () => { + throw new Error('Not implemented'); + }, + generateGatewayMsgPayload: async () => { + throw new Error('Not implemented'); + }, }); export const BondingContextProvider: FCWithChildren = ({ children }): JSX.Element => { @@ -465,6 +485,42 @@ export const BondingContextProvider: FCWithChildren = ({ children }): JSX.Elemen return undefined; }; + const generateMixnodeMsgPayload = async (data: TBondMixnodeSignatureArgs) => { + let message; + setIsLoading(true); + try { + if (isVesting) { + message = await vestingGenerateMixnodeMsgPayloadReq(data); + } else { + message = await generateMixnodeMsgPayloadReq(data); + } + } catch (e) { + Console.warn(e); + setError(`an error occurred: ${e}`); + } finally { + setIsLoading(false); + } + return message; + }; + + const generateGatewayMsgPayload = async (data: TBondGatewaySignatureArgs) => { + let message; + setIsLoading(true); + try { + if (isVesting) { + message = await vestingGenerateGatewayMsgPayloadReq(data); + } else { + message = await generateGatewayMsgPayloadReq(data); + } + } catch (e) { + Console.warn(e); + setError(`an error occurred: ${e}`); + } finally { + setIsLoading(false); + } + return message; + }; + const memoizedValue = useMemo( () => ({ isLoading: isLoading || isOwnershipLoading, @@ -478,6 +534,8 @@ export const BondingContextProvider: FCWithChildren = ({ children }): JSX.Elemen redeemRewards, bondMore, checkOwnership, + generateMixnodeMsgPayload, + generateGatewayMsgPayload, }), [isLoading, isOwnershipLoading, error, bondedNode, isVesting], ); diff --git a/nym-wallet/src/context/mocks/bonding.tsx b/nym-wallet/src/context/mocks/bonding.tsx index 610b9ee299..1361e11700 100644 --- a/nym-wallet/src/context/mocks/bonding.tsx +++ b/nym-wallet/src/context/mocks/bonding.tsx @@ -3,6 +3,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import type { Network } from 'src/types'; import { BondingContext, TBondedGateway, TBondedMixnode } from '../bonding'; import { mockSleep } from './utils'; +import { TBondGatewaySignatureArgs, TBondMixnodeSignatureArgs } from '../../types'; const SLEEP_MS = 1000; @@ -170,6 +171,22 @@ export const MockBondingContextProvider = ({ return feeMock; }; + const generateMixnodeMsgPayload = async (_data: TBondMixnodeSignatureArgs) => { + setIsLoading(true); + await mockSleep(SLEEP_MS); + triggerStateUpdate(); + setIsLoading(false); + return '77dcaba7f41409984f4ebce4a386f59b10f1e65ed5514d1acdccae30174bd84b'; + }; + + const generateGatewayMsgPayload = async (_data: TBondGatewaySignatureArgs) => { + setIsLoading(true); + await mockSleep(SLEEP_MS); + triggerStateUpdate(); + setIsLoading(false); + return '77dcaba7f41409984f4ebce4a386f59b10f1e65ed5514d1acdccae30174bd84b'; + }; + const resetFeeState = () => {}; const memoizedValue = useMemo( @@ -188,6 +205,8 @@ export const MockBondingContextProvider = ({ updateMixnode, bondMore, checkOwnership, + generateMixnodeMsgPayload, + generateGatewayMsgPayload, }), [isLoading, error, bondedMixnode, bondedGateway, trigger, fee], ); diff --git a/nym-wallet/src/pages/bonding/types.ts b/nym-wallet/src/pages/bonding/types.ts index a15ac8b522..0e2b4a6c8d 100644 --- a/nym-wallet/src/pages/bonding/types.ts +++ b/nym-wallet/src/pages/bonding/types.ts @@ -2,23 +2,8 @@ import { DecCoin, MixNodeCostParams, TNodeType, TransactionExecuteResult } from import { TPoolOption } from 'src/components'; export type FormStep = 1 | 2 | 3 | 4; -export type NodeType = TNodeType; export type BondStatus = 'init' | 'success' | 'error' | 'loading'; -export type ACTIONTYPE = - | { type: 'change_bond_type'; payload: NodeType } - | { type: 'set_node_data'; payload: NodeData } - | { type: 'set_amount_data'; payload: AmountData } - | { type: 'set_step'; payload: FormStep } - | { type: 'set_tx'; payload: TransactionExecuteResult | undefined } - | { type: 'set_error'; payload: string | null | undefined } - | { type: 'set_bond_status'; payload: BondStatus } - | { type: 'next_step' } - | { type: 'prev_step' } - | { type: 'show_modal' } - | { type: 'close_modal' } - | { type: 'reset' }; - export type NodeIdentity = { identityKey: string; sphinxKey: string; @@ -45,6 +30,10 @@ export type MixnodeAmount = Amount & { profitMargin: string; }; +export type Signature = { + signature: string; +}; + export type GatewayData = NodeIdentity & { location: string; clientsPort: number; diff --git a/nym-wallet/src/requests/actions.ts b/nym-wallet/src/requests/actions.ts index db4e715fb4..b233ab3b24 100644 --- a/nym-wallet/src/requests/actions.ts +++ b/nym-wallet/src/requests/actions.ts @@ -6,17 +6,30 @@ import { MixNodeConfigUpdate, MixNodeCostParams, } from '@nymproject/types'; -import { EnumNodeType, TBondGatewayArgs, TBondMixNodeArgs, TBondMoreArgs } from '../types'; +import { + EnumNodeType, + TBondGatewayArgs, + TBondGatewaySignatureArgs, + TBondMixNodeArgs, + TBondMixnodeSignatureArgs, + TBondMoreArgs, +} from '../types'; import { invokeWrapper } from './wrapper'; export const bondGateway = async (args: TBondGatewayArgs) => invokeWrapper('bond_gateway', args); +export const generateGatewayMsgPayload = async (args: TBondGatewaySignatureArgs) => + invokeWrapper('generate_gateway_bonding_msg_payload', args); + export const unbondGateway = async (fee?: Fee) => invokeWrapper('unbond_gateway', { fee }); export const bondMixNode = async (args: TBondMixNodeArgs) => invokeWrapper('bond_mixnode', args); +export const generateMixnodeMsgPayload = async (args: TBondMixnodeSignatureArgs) => + invokeWrapper('generate_mixnode_bonding_msg_payload', args); + export const unbondMixNode = async (fee?: Fee) => invokeWrapper('unbond_mixnode', { fee }); export const updateMixnodeCostParams = async (newCosts: MixNodeCostParams, fee?: Fee) => diff --git a/nym-wallet/src/requests/simulate.ts b/nym-wallet/src/requests/simulate.ts index d5aeacc2eb..3d9929d6ee 100644 --- a/nym-wallet/src/requests/simulate.ts +++ b/nym-wallet/src/requests/simulate.ts @@ -33,7 +33,7 @@ export const simulateVestingClaimDelegatorReward = async (mixId: number) => export const simulateVestingUndelegateFromMixnode = async (args: any) => invokeWrapper('simulate_vesting_undelegate_from_mixnode', args); -export const simulateVestingBondGateway = async (args: { gateway: Gateway; pledge: DecCoin; ownerSignature: string }) => +export const simulateVestingBondGateway = async (args: { gateway: Gateway; pledge: DecCoin; msgSignature: string }) => invokeWrapper('simulate_vesting_bond_gateway', args); export const simulateVestingUnbondGateway = async (args: any) => diff --git a/nym-wallet/src/requests/vesting.ts b/nym-wallet/src/requests/vesting.ts index 993910dd14..6932ab93ee 100644 --- a/nym-wallet/src/requests/vesting.ts +++ b/nym-wallet/src/requests/vesting.ts @@ -13,6 +13,7 @@ import { } from '@nymproject/types'; import { Fee } from '@nymproject/types/dist/types/rust/Fee'; import { invokeWrapper } from './wrapper'; +import { TBondGatewaySignatureArgs, TBondMixnodeSignatureArgs } from '../types'; export const getLockedCoins = async (): Promise => invokeWrapper('locked_coins'); @@ -35,12 +36,15 @@ export const getCurrentVestingPeriod = async (address: string) => export const vestingBondGateway = async ({ gateway, pledge, - ownerSignature, + msgSignature, }: { gateway: Gateway; pledge: DecCoin; - ownerSignature: string; -}) => invokeWrapper('vesting_bond_gateway', { gateway, ownerSignature, pledge }); + msgSignature: string; +}) => invokeWrapper('vesting_bond_gateway', { gateway, msgSignature, pledge }); + +export const vestingGenerateGatewayMsgPayload = async (args: TBondGatewaySignatureArgs) => + invokeWrapper('vesting_generate_gateway_bonding_msg_payload', args); export const vestingUnbondGateway = async (fee?: Fee) => invokeWrapper('vesting_unbond_gateway', { fee }); @@ -49,13 +53,16 @@ export const vestingBondMixNode = async ({ mixnode, costParams, pledge, - ownerSignature, + msgSignature, }: { mixnode: MixNode; costParams: MixNodeCostParams; pledge: DecCoin; - ownerSignature: string; -}) => invokeWrapper('vesting_bond_mixnode', { mixnode, costParams, ownerSignature, pledge }); + msgSignature: string; +}) => invokeWrapper('vesting_bond_mixnode', { mixnode, costParams, msgSignature, pledge }); + +export const vestingGenerateMixnodeMsgPayload = async (args: TBondMixnodeSignatureArgs) => + invokeWrapper('vesting_generate_mixnode_bonding_msg_payload', args); export const vestingUnbondMixnode = async (fee?: Fee) => invokeWrapper('vesting_unbond_mixnode', { fee }); diff --git a/nym-wallet/src/types/global.ts b/nym-wallet/src/types/global.ts index c5f0fc3a95..397636a288 100644 --- a/nym-wallet/src/types/global.ts +++ b/nym-wallet/src/types/global.ts @@ -29,7 +29,7 @@ export type TDelegation = { export type TBondGatewayArgs = { gateway: Gateway; pledge: DecCoin; - ownerSignature: string; + msgSignature: string; fee?: Fee; }; @@ -37,10 +37,21 @@ export type TBondMixNodeArgs = { mixnode: MixNode; costParams: MixNodeCostParams; pledge: DecCoin; - ownerSignature: string; + msgSignature: string; fee?: Fee; }; +export type TBondMixnodeSignatureArgs = { + mixnode: MixNode; + costParams: MixNodeCostParams; + pledge: DecCoin; +}; + +export type TBondGatewaySignatureArgs = { + gateway: Gateway; + pledge: DecCoin; +}; + export type TBondMoreArgs = { additionalPledge: DecCoin; fee?: Fee; diff --git a/tools/nym-cli/src/validator/mixnet/operators/gateways/mod.rs b/tools/nym-cli/src/validator/mixnet/operators/gateways/mod.rs index b7bde45cf3..42f7cd6e0b 100644 --- a/tools/nym-cli/src/validator/mixnet/operators/gateways/mod.rs +++ b/tools/nym-cli/src/validator/mixnet/operators/gateways/mod.rs @@ -16,6 +16,9 @@ pub(crate) async fn execute( nym_cli_commands::validator::mixnet::operators::gateway::MixnetOperatorsGatewayCommands::Unbound(_args) => { nym_cli_commands::validator::mixnet::operators::gateway::unbond_gateway::unbond_gateway(create_signing_client(global_args, network_details)?).await }, + nym_cli_commands::validator::mixnet::operators::gateway::MixnetOperatorsGatewayCommands::CreateGatewayBondingSignPayload(args) => { + nym_cli_commands::validator::mixnet::operators::gateway::gateway_bonding_sign_payload::create_payload(args,create_signing_client(global_args, network_details)?).await + } _ => unreachable!(), } 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 fbe07679fb..e3dcd26d16 100644 --- a/tools/nym-cli/src/validator/mixnet/operators/mixnodes/mod.rs +++ b/tools/nym-cli/src/validator/mixnet/operators/mixnodes/mod.rs @@ -29,6 +29,9 @@ pub(crate) async fn execute( nym_cli_commands::validator::mixnet::operators::mixnode::MixnetOperatorsMixnodeCommands::Unbound(args) => { nym_cli_commands::validator::mixnet::operators::mixnode::unbond_mixnode::unbond_mixnode(args, create_signing_client(global_args, network_details)?).await } + nym_cli_commands::validator::mixnet::operators::mixnode::MixnetOperatorsMixnodeCommands::CreateMixnodeBondingSignPayload(args) => { + nym_cli_commands::validator::mixnet::operators::mixnode::mixnode_bonding_sign_payload::create_payload(args,create_signing_client(global_args, network_details)?).await + } _ => unreachable!(), } Ok(()) diff --git a/ts-packages/mock-nym-api/package.json b/ts-packages/mock-nym-api/package.json index 10a5a7d3e5..a712fb99b9 100644 --- a/ts-packages/mock-nym-api/package.json +++ b/ts-packages/mock-nym-api/package.json @@ -43,7 +43,7 @@ "run:caddy": "caddy run", "build": "tsc --noEmit false", "watch": "tsc --noEmit false -w", - "lint": "eslint src .storybook", - "lint:fix": "eslint src .storybook --fix" + "lint": "eslint src", + "lint:fix": "eslint src --fix" } }