diff --git a/Cargo.lock b/Cargo.lock index 48e0705c85..72fb1ceb3c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3507,6 +3507,7 @@ dependencies = [ "mixnode-common", "nym-bin-common", "nym-config", + "nym-contracts-common", "nym-crypto", "nym-nonexhaustive-delayqueue", "nym-pemstore", 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.rs b/common/cosmwasm-smart-contracts/contracts-common/src/signing.rs new file mode 100644 index 0000000000..7648c5183e --- /dev/null +++ b/common/cosmwasm-smart-contracts/contracts-common/src/signing.rs @@ -0,0 +1,131 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_std::{from_slice, to_vec, Addr, Coin, MessageInfo, StdResult}; +use serde::de::DeserializeOwned; +use serde::{Deserialize, Serialize}; + +pub type Nonce = u32; + +#[derive(Serialize)] +#[serde(transparent)] +pub struct MessageType(String); + +impl From for MessageType +where + T: ToString, +{ + fn from(value: T) -> Self { + MessageType(value.to_string()) + } +} + +#[derive(Default, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum SigningAlgorithm { + #[default] + Ed25519, + Secp256k1, +} + +// TODO: maybe move this one to repo-wide common? +#[derive(Serialize, Deserialize)] +pub struct SignableMessage { + nonce: u32, + algorithm: SigningAlgorithm, + content: T, +} + +impl SignableMessage { + pub fn new(nonce: u32, content: T) -> Self { + SignableMessage { + nonce, + algorithm: SigningAlgorithm::Ed25519, + 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_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 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()) + } +} + +#[derive(Serialize)] +struct ContractMessageContent { + message_type: MessageType, + signer: Addr, + proxy: Option, + funds: Vec, + data: T, +} + +impl ContractMessageContent { + pub fn new( + message_type: MessageType, + signer: Addr, + proxy: Option, + funds: Vec, + data: T, + ) -> Self { + ContractMessageContent { + message_type, + signer, + proxy, + funds, + data, + } + } + + pub fn new_with_info( + message_type: MessageType, + info: MessageInfo, + signer: Addr, + data: T, + ) -> Self { + let proxy = if info.sender == signer { + None + } else { + Some(info.sender) + }; + + ContractMessageContent { + message_type, + signer, + proxy, + funds: info.funds, + data, + } + } +} diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs index 1af718afde..1d79802ee9 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::MessageType; use cosmwasm_std::Decimal; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -240,6 +241,54 @@ pub enum ExecuteMsg { } impl ExecuteMsg { + pub fn message_type(&self) -> MessageType { + match self { + ExecuteMsg::AssignNodeLayer { .. } => {} + ExecuteMsg::CreateFamily { .. } => {} + ExecuteMsg::JoinFamily { .. } => {} + ExecuteMsg::LeaveFamily { .. } => {} + ExecuteMsg::KickFamilyMember { .. } => {} + ExecuteMsg::CreateFamilyOnBehalf { .. } => {} + ExecuteMsg::JoinFamilyOnBehalf { .. } => {} + ExecuteMsg::LeaveFamilyOnBehalf { .. } => {} + ExecuteMsg::KickFamilyMemberOnBehalf { .. } => {} + ExecuteMsg::UpdateRewardingValidatorAddress { .. } => {} + ExecuteMsg::UpdateContractStateParams { .. } => {} + ExecuteMsg::UpdateActiveSetSize { .. } => {} + ExecuteMsg::UpdateRewardingParams { .. } => {} + ExecuteMsg::UpdateIntervalConfig { .. } => {} + ExecuteMsg::BeginEpochTransition => {} + ExecuteMsg::AdvanceCurrentEpoch { .. } => {} + ExecuteMsg::ReconcileEpochEvents { .. } => {} + ExecuteMsg::BondMixnode { .. } => {} + ExecuteMsg::BondMixnodeOnBehalf { .. } => {} + ExecuteMsg::PledgeMore { .. } => {} + ExecuteMsg::PledgeMoreOnBehalf { .. } => {} + ExecuteMsg::UnbondMixnode { .. } => {} + ExecuteMsg::UnbondMixnodeOnBehalf { .. } => {} + ExecuteMsg::UpdateMixnodeCostParams { .. } => {} + ExecuteMsg::UpdateMixnodeCostParamsOnBehalf { .. } => {} + ExecuteMsg::UpdateMixnodeConfig { .. } => {} + ExecuteMsg::UpdateMixnodeConfigOnBehalf { .. } => {} + ExecuteMsg::BondGateway { .. } => {} + ExecuteMsg::BondGatewayOnBehalf { .. } => {} + ExecuteMsg::UnbondGateway { .. } => {} + ExecuteMsg::UnbondGatewayOnBehalf { .. } => {} + ExecuteMsg::DelegateToMixnode { .. } => {} + ExecuteMsg::DelegateToMixnodeOnBehalf { .. } => {} + ExecuteMsg::UndelegateFromMixnode { .. } => {} + ExecuteMsg::UndelegateFromMixnodeOnBehalf { .. } => {} + ExecuteMsg::RewardMixnode { .. } => {} + ExecuteMsg::WithdrawOperatorReward { .. } => {} + ExecuteMsg::WithdrawOperatorRewardOnBehalf { .. } => {} + ExecuteMsg::WithdrawDelegatorReward { .. } => {} + ExecuteMsg::WithdrawDelegatorRewardOnBehalf { .. } => {} + #[cfg(feature = "contract-testing")] + ExecuteMsg::TestingResolveAllPendingEvents { .. } => {} + } + todo!() + } + pub fn default_memo(&self) -> String { match self { ExecuteMsg::AssignNodeLayer { mix_id, layer } => { 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/lib.rs b/contracts/mixnet/src/lib.rs index fb98e68fe0..ce38cdefd8 100644 --- a/contracts/mixnet/src/lib.rs +++ b/contracts/mixnet/src/lib.rs @@ -15,6 +15,7 @@ mod mixnodes; mod queued_migrations; mod rewards; mod support; +mod signing; #[cfg(feature = "contract-testing")] mod testing; diff --git a/contracts/mixnet/src/signing/helpers.rs b/contracts/mixnet/src/signing/helpers.rs new file mode 100644 index 0000000000..e630e69c22 --- /dev/null +++ b/contracts/mixnet/src/signing/helpers.rs @@ -0,0 +1,17 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::signing::storage; +use cosmwasm_std::{Addr, Storage}; +use mixnet_contract_common::error::MixnetContractError; + +fn construct_signed_message( + storage: &mut dyn Storage, + signer: Addr, + message: T, +) -> Result<(), MixnetContractError> { + // right now all signing is done with ed25519 so that simplifies things a bit + let nonce = storage::get_and_update_signing_nonce(storage, signer)?; + + todo!() +} diff --git a/contracts/mixnet/src/signing/mod.rs b/contracts/mixnet/src/signing/mod.rs new file mode 100644 index 0000000000..f4d6727d79 --- /dev/null +++ b/contracts/mixnet/src/signing/mod.rs @@ -0,0 +1,6 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod helpers; +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..46c0e0ce35 --- /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 try_get_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..1fa111bf72 --- /dev/null +++ b/contracts/mixnet/src/signing/storage.rs @@ -0,0 +1,32 @@ +// 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 get_and_update_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)?; + + Ok(nonce) +} 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..c9ace438ab 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,25 @@ 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) { + // 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 json + if serde_json::from_str::(raw_msg).is_err() { + println!("it seems you have incorrectly copied the message to sign. Make sure you didn't accidentally skip any characters") + } else { + let msg = raw_msg.trim(); + let signature = private_key.sign(msg.trim().as_bytes()).to_base58_string(); + println!("The base58-encoded signature on\n{msg}\nis:\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 +119,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) + } } }