diff --git a/common/cosmwasm-smart-contracts/coconut-dkg-contract/src/msg.rs b/common/cosmwasm-smart-contracts/coconut-dkg-contract/src/msg.rs index 4512f48913..31464b554a 100644 --- a/common/cosmwasm-smart-contracts/coconut-dkg-contract/src/msg.rs +++ b/common/cosmwasm-smart-contracts/coconut-dkg-contract/src/msg.rs @@ -1,6 +1,7 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::types::{EncodedBTEPublicKeyWithProof, EncodedEd25519PublicKey}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -10,7 +11,13 @@ pub struct InstantiateMsg {} #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case")] -pub enum ExecuteMsg {} +pub enum ExecuteMsg { + RegisterDealer { + ed25519_key: EncodedEd25519PublicKey, + bte_key_with_proof: EncodedBTEPublicKeyWithProof, + owner_signature: String, + }, +} #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case")] diff --git a/common/cosmwasm-smart-contracts/coconut-dkg-contract/src/types.rs b/common/cosmwasm-smart-contracts/coconut-dkg-contract/src/types.rs index ea59bf4942..28a000c7be 100644 --- a/common/cosmwasm-smart-contracts/coconut-dkg-contract/src/types.rs +++ b/common/cosmwasm-smart-contracts/coconut-dkg-contract/src/types.rs @@ -4,20 +4,54 @@ use serde::{Deserialize, Serialize}; use std::fmt::{Display, Formatter}; +pub type BlockHeight = u64; +pub type EncodedEd25519PublicKey = String; +pub type EncodedEd25519PublicKeyRef<'a> = &'a str; +pub type EncodedBTEPublicKeyWithProof = String; +pub type NodeIndex = u64; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +#[serde(rename_all = "snake_case")] +pub struct DealerDetails { + pub joined_at: BlockHeight, + pub left_at: Option, + pub blacklisting: Option, + pub ed25519_public_key: EncodedEd25519PublicKey, + pub bte_public_key_with_proof: EncodedBTEPublicKeyWithProof, + pub assigned_index: NodeIndex, +} + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] #[serde(rename_all = "snake_case")] pub struct Blacklisting { - reason: BlacklistingReason, - height: u64, + pub reason: BlacklistingReason, + pub height: BlockHeight, + pub expiration: Option, +} + +impl Blacklisting { + pub fn has_expired(&self, current_block: BlockHeight) -> bool { + self.expiration + .map(|expiration| expiration <= current_block) + .unwrap_or_default() + } } impl Display for Blacklisting { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!( - f, - "blacklisted at block height {}. reason given: {}", - self.height, self.height - ) + if let Some(expiration) = self.expiration { + write!( + f, + "blacklisted at block height {}. reason given: {}. Expires at: {}", + self.height, self.height, expiration + ) + } else { + write!( + f, + "blacklisted at block height {}. reason given: {}", + self.height, self.height + ) + } } } @@ -25,6 +59,10 @@ impl Display for Blacklisting { #[serde(rename_all = "snake_case")] pub enum BlacklistingReason { InactiveForConsecutiveEpochs, + MalformedBTEPublicKey, + InvalidBTEPublicKey, + MalformedEd25519PublicKey, + Ed25519PossessionVerificationFailure, } impl Display for BlacklistingReason { @@ -33,6 +71,64 @@ impl Display for BlacklistingReason { BlacklistingReason::InactiveForConsecutiveEpochs => { write!(f, "has been inactive for multiple consecutive epochs") } + BlacklistingReason::MalformedBTEPublicKey => { + write!(f, "provided malformed BTE Public Key") + } + BlacklistingReason::InvalidBTEPublicKey => write!(f, "provided invalid BTE Public Key"), + BlacklistingReason::MalformedEd25519PublicKey => { + write!(f, "provided malformed ed25519 Public Key") + } + BlacklistingReason::Ed25519PossessionVerificationFailure => { + write!( + f, + "failed to verify possession of provided ed25519 Public Key" + ) + } } } } + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +#[serde(rename_all = "snake_case")] +pub struct Epoch { + pub id: u64, + pub state: EpochState, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum EpochState { + InProgress { + begun_at: BlockHeight, + // not entirely sure about that one yet. we'll see how it works out when we get to epoch transition + finish_by: Option, + }, + PublicKeySubmission { + begun_at: BlockHeight, + finish_by: BlockHeight, + }, + DealingExchange { + begun_at: BlockHeight, + finish_by: BlockHeight, + }, + ComplaintSubmission { + begun_at: BlockHeight, + finish_by: BlockHeight, + }, + ComplaintVoting { + begun_at: BlockHeight, + finish_by: BlockHeight, + }, + VerificationKeySubmission { + begun_at: BlockHeight, + finish_by: BlockHeight, + }, + VerificationKeyMismatchSubmission { + begun_at: BlockHeight, + finish_by: BlockHeight, + }, + VerificationKeyMismatchVoting { + begun_at: BlockHeight, + finish_by: BlockHeight, + }, +} diff --git a/common/network-defaults/src/lib.rs b/common/network-defaults/src/lib.rs index dc55029d6c..28e912f57a 100644 --- a/common/network-defaults/src/lib.rs +++ b/common/network-defaults/src/lib.rs @@ -18,6 +18,7 @@ cfg_if::cfg_if! { if #[cfg(network = "mainnet")] { pub const DEFAULT_NETWORK: all::Network = all::Network::MAINNET; pub const DENOM: &str = mainnet::DENOM; + pub const STAKE_DENOM: &str = mainnet::STAKE_DENOM; pub const ETH_CONTRACT_ADDRESS: [u8; 20] = mainnet::_ETH_CONTRACT_ADDRESS; pub const ETH_ERC20_CONTRACT_ADDRESS: [u8; 20] = mainnet::_ETH_ERC20_CONTRACT_ADDRESS; @@ -25,6 +26,7 @@ cfg_if::cfg_if! { } else if #[cfg(network = "qa")] { pub const DEFAULT_NETWORK: all::Network = all::Network::QA; pub const DENOM: &str = qa::DENOM; + pub const STAKE_DENOM: &str = qa::STAKE_DENOM; pub const ETH_CONTRACT_ADDRESS: [u8; 20] = qa::_ETH_CONTRACT_ADDRESS; pub const ETH_ERC20_CONTRACT_ADDRESS: [u8; 20] = qa::_ETH_ERC20_CONTRACT_ADDRESS; @@ -32,6 +34,7 @@ cfg_if::cfg_if! { } else if #[cfg(network = "sandbox")] { pub const DEFAULT_NETWORK: all::Network = all::Network::SANDBOX; pub const DENOM: &str = sandbox::DENOM; + pub const STAKE_DENOM: &str = sandbox::STAKE_DENOM; pub const ETH_CONTRACT_ADDRESS: [u8; 20] = sandbox::_ETH_CONTRACT_ADDRESS; pub const ETH_ERC20_CONTRACT_ADDRESS: [u8; 20] = sandbox::_ETH_ERC20_CONTRACT_ADDRESS; diff --git a/common/network-defaults/src/mainnet.rs b/common/network-defaults/src/mainnet.rs index c8f46de121..338bdcaf3b 100644 --- a/common/network-defaults/src/mainnet.rs +++ b/common/network-defaults/src/mainnet.rs @@ -5,6 +5,7 @@ use crate::ValidatorDetails; pub(crate) const BECH32_PREFIX: &str = "n"; pub const DENOM: &str = "unym"; +pub const STAKE_DENOM: &str = "unyx"; pub(crate) const MIXNET_CONTRACT_ADDRESS: &str = "n14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9sjyvg3g"; diff --git a/common/network-defaults/src/qa.rs b/common/network-defaults/src/qa.rs index ea004fc221..6eec8e2163 100644 --- a/common/network-defaults/src/qa.rs +++ b/common/network-defaults/src/qa.rs @@ -5,6 +5,7 @@ use crate::ValidatorDetails; pub(crate) const BECH32_PREFIX: &str = "nymt"; pub const DENOM: &str = "unymt"; +pub const STAKE_DENOM: &str = "unyxt"; pub(crate) const MIXNET_CONTRACT_ADDRESS: &str = "nymt17x6pt4msccvawgxjeg5nmnygttu56tftg5l6j3"; pub(crate) const VESTING_CONTRACT_ADDRESS: &str = "nymt1t4dmskxea0avvrj8xtmu66hv7dkyg9s8059t3c"; diff --git a/common/network-defaults/src/sandbox.rs b/common/network-defaults/src/sandbox.rs index 28c25c792b..3ec1f746a9 100644 --- a/common/network-defaults/src/sandbox.rs +++ b/common/network-defaults/src/sandbox.rs @@ -5,6 +5,7 @@ use crate::ValidatorDetails; pub(crate) const BECH32_PREFIX: &str = "nymt"; pub const DENOM: &str = "unymt"; +pub const STAKE_DENOM: &str = "unyxt"; pub(crate) const MIXNET_CONTRACT_ADDRESS: &str = "nymt1ghd753shjuwexxywmgs4xz7x2q732vcnstz02j"; pub(crate) const VESTING_CONTRACT_ADDRESS: &str = "nymt14ejqjyq8um4p3xfqj74yld5waqljf88fn549lh"; diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock index 3c34783af9..058c5bef06 100644 --- a/contracts/Cargo.lock +++ b/contracts/Cargo.lock @@ -217,6 +217,7 @@ dependencies = [ name = "coconut-dkg" version = "0.1.0" dependencies = [ + "bs58", "coconut-dkg-common", "config", "cosmwasm-std", diff --git a/contracts/coconut-dkg/Cargo.toml b/contracts/coconut-dkg/Cargo.toml index 6c21c36b92..a83aca344b 100644 --- a/contracts/coconut-dkg/Cargo.toml +++ b/contracts/coconut-dkg/Cargo.toml @@ -12,6 +12,7 @@ crate-type = ["cdylib", "rlib"] coconut-dkg-common = { path = "../../common/cosmwasm-smart-contracts/coconut-dkg-contract" } config = { path = "../../common/config"} +bs58 = "0.4" cosmwasm-std = { version = "1.0.0-beta8", features = ["staking"] } cosmwasm-storage = "1.0.0-beta8" cw-storage-plus = "0.13.2" diff --git a/contracts/coconut-dkg/src/constants.rs b/contracts/coconut-dkg/src/constants.rs new file mode 100644 index 0000000000..3dac3b944d --- /dev/null +++ b/contracts/coconut-dkg/src/constants.rs @@ -0,0 +1,10 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_std::Uint128; + +// to be determined whether those should be constants or exist as contract state +pub(crate) const MINIMUM_DEPOSIT: Uint128 = Uint128::new(1_000_000_000); + +// if submitted invalid keys/signatures, get blacklisted for approximately 1 day (with ~5s per block) +pub(crate) const INVALID_ED25519_BLACKLISTING_EXPIRATION: u64 = 17280; diff --git a/contracts/coconut-dkg/src/error.rs b/contracts/coconut-dkg/src/error.rs index e182198ca9..5679cee0d3 100644 --- a/contracts/coconut-dkg/src/error.rs +++ b/contracts/coconut-dkg/src/error.rs @@ -2,26 +2,46 @@ // SPDX-License-Identifier: Apache-2.0 use coconut_dkg_common::types::Blacklisting; -use cosmwasm_std::StdError; +use config::defaults::STAKE_DENOM; +use cosmwasm_std::{StdError, VerificationError}; use thiserror::Error; -use config::defaults::DENOM; - /// Custom errors for contract failure conditions. #[derive(Error, Debug, PartialEq)] pub enum ContractError { #[error("{0}")] Std(#[from] StdError), + #[error("No coin was sent for the deposit, you must send {}", STAKE_DENOM)] + NoDepositFound, + #[error("Received multiple coin types")] MultipleDenoms, - #[error("Wrong coin denomination, you must send {}", DENOM)] + #[error("Wrong coin denomination, you must send {}", STAKE_DENOM)] WrongDenom, + #[error("Not enough funds sent for deposit. (received {received}, minimum {minimum})")] + InsufficientDeposit { received: u128, minimum: u128 }, + + #[error("Failed to recover ed25519 public key from its base58 representation - {0}. This dealer will be temporarily blacklisted now.")] + MalformedEd25519PublicKey(bs58::decode::Error), + + #[error("Failed to recover ed25519 signature from its base58 representation - {0}. This dealer will be temporarily blacklisted now.")] + MalformedEd25519Signature(bs58::decode::Error), + + #[error("Failed to perform ed25519 signature verification - {0}. This dealer will be temporarily blacklisted now.")] + Ed25519VerificationError(#[from] VerificationError), + + #[error("Provided ed25519 signature did not verify correctly. This dealer will be temporarily blacklisted now.")] + InvalidEd25519Signature, + #[error("This dealer has been blacklisted - {reason}")] BlacklistedDealer { reason: Blacklisting }, #[error("This potential dealer is not a validator")] NotAValidator, + + #[error("This sender is already a dealer for the epoch")] + AlreadyADealer, } diff --git a/contracts/coconut-dkg/src/lib.rs b/contracts/coconut-dkg/src/lib.rs index c5765037f1..11a8bd6dca 100644 --- a/contracts/coconut-dkg/src/lib.rs +++ b/contracts/coconut-dkg/src/lib.rs @@ -5,6 +5,7 @@ use crate::error::ContractError; use coconut_dkg_common::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; use cosmwasm_std::{entry_point, Deps, DepsMut, Env, MessageInfo, QueryResponse, Response}; +mod constants; mod error; mod queries; mod storage; @@ -29,16 +30,25 @@ pub fn instantiate( /// Handle an incoming message #[entry_point] pub fn execute( - _deps: DepsMut<'_>, - _env: Env, - _info: MessageInfo, + deps: DepsMut<'_>, + env: Env, + info: MessageInfo, msg: ExecuteMsg, ) -> Result { match msg { - _ => (), + ExecuteMsg::RegisterDealer { + ed25519_key, + bte_key_with_proof, + owner_signature, + } => transactions::try_add_dealer( + deps, + env, + info, + ed25519_key, + bte_key_with_proof, + owner_signature, + ), } - - Ok(Default::default()) } #[entry_point] diff --git a/contracts/coconut-dkg/src/storage.rs b/contracts/coconut-dkg/src/storage.rs index f5b5eb530b..5cf5227eb3 100644 --- a/contracts/coconut-dkg/src/storage.rs +++ b/contracts/coconut-dkg/src/storage.rs @@ -1,8 +1,23 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use coconut_dkg_common::types::Blacklisting; -use cosmwasm_std::Addr; -use cw_storage_plus::Map; +use coconut_dkg_common::types::{Blacklisting, DealerDetails, NodeIndex}; +use cosmwasm_std::{Addr, StdResult, Storage}; +use cw_storage_plus::{Item, Map}; pub(crate) const BLACKLISTED_DEALERS: Map<'_, &'_ Addr, Blacklisting> = Map::new("bld"); + +pub(crate) const NODE_INDEX_COUNTER: Item = Item::new("node_index_counter"); + +// TODO: this is an interesting question: should dealers be addressed by their addresses +// or maybe node indices? +// perhaps this should be UniqueIndex? +pub(crate) const CURRENT_DEALERS: Map<'_, &'_ Addr, DealerDetails> = Map::new("crd"); +pub(crate) const PAST_DEALERS: Map<'_, &'_ Addr, DealerDetails> = Map::new("ptd"); + +pub(crate) fn next_node_index(store: &mut dyn Storage) -> StdResult { + // make sure we don't start from 0! + let id: NodeIndex = NODE_INDEX_COUNTER.may_load(store)?.unwrap_or_default() + 1; + NODE_INDEX_COUNTER.save(store, &id)?; + Ok(id) +} diff --git a/contracts/coconut-dkg/src/transactions.rs b/contracts/coconut-dkg/src/transactions.rs index a5e8611476..fa83b2a4c3 100644 --- a/contracts/coconut-dkg/src/transactions.rs +++ b/contracts/coconut-dkg/src/transactions.rs @@ -1,19 +1,42 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::storage::BLACKLISTED_DEALERS; +use crate::constants::{INVALID_ED25519_BLACKLISTING_EXPIRATION, MINIMUM_DEPOSIT}; +use crate::storage::{next_node_index, BLACKLISTED_DEALERS, CURRENT_DEALERS, PAST_DEALERS}; use crate::ContractError; -use cosmwasm_std::{ensure, Addr, Deps, DepsMut, Env, MessageInfo, Response, Storage}; +use coconut_dkg_common::types::{ + Blacklisting, BlacklistingReason, BlockHeight, DealerDetails, EncodedBTEPublicKeyWithProof, + EncodedEd25519PublicKey, EncodedEd25519PublicKeyRef, +}; +use config::defaults::STAKE_DENOM; +use cosmwasm_std::{Addr, Coin, Deps, DepsMut, Env, MessageInfo, Response}; // currently we only require that // a) it's a validator // b) it wasn't blacklisted -fn verify_dealer(deps: Deps<'_>, dealer: &Addr) -> Result<(), ContractError> { +// c) it isn't already a dealer +fn verify_dealer( + deps: DepsMut<'_>, + current_height: BlockHeight, + dealer: &Addr, +) -> Result<(), ContractError> { if let Some(blacklisting) = BLACKLISTED_DEALERS.may_load(deps.storage, dealer)? { - return Err(ContractError::BlacklistedDealer { - reason: blacklisting, - }); + // TODO: perhaps whenever we touch anything to do with blacklisting, each should check for + // expiration and auto-magically clear stale ones + if !blacklisting.has_expired(current_height) { + return Err(ContractError::BlacklistedDealer { + reason: blacklisting, + }); + } else { + // remove the expired blacklisting + BLACKLISTED_DEALERS.remove(deps.storage, dealer) + } } + + if CURRENT_DEALERS.has(deps.storage, dealer) { + return Err(ContractError::AlreadyADealer); + } + let all_validators = deps.querier.query_all_validators()?; if !all_validators .iter() @@ -25,12 +48,114 @@ fn verify_dealer(deps: Deps<'_>, dealer: &Addr) -> Result<(), ContractError> { Ok(()) } +fn validate_dealer_deposit(mut deposit: Vec) -> Result { + // check if anything was put as deposit + if deposit.is_empty() { + return Err(ContractError::NoDepositFound); + } + + if deposit.len() > 1 { + return Err(ContractError::MultipleDenoms); + } + + // check that the denomination is correct + if deposit[0].denom != STAKE_DENOM { + return Err(ContractError::WrongDenom {}); + } + + // check that we have at least MINIMUM_DEPOSIT coins in our deposit + if deposit[0].amount < MINIMUM_DEPOSIT { + return Err(ContractError::InsufficientDeposit { + received: deposit[0].amount.into(), + minimum: MINIMUM_DEPOSIT.into(), + }); + } + + // the unwrap would have been safe here under all circumstances, since we checked whether the vector is empty + // but in case something did change, change option into an error + deposit.pop().ok_or(ContractError::NoDepositFound) +} + +pub(crate) fn validate_key_possession_signature( + deps: Deps<'_>, + owner: &Addr, + signature: String, + encoded_key: EncodedEd25519PublicKeyRef<'_>, +) -> Result<(), ContractError> { + let mut key_bytes = [0u8; 32]; + let mut signature_bytes = [0u8; 64]; + + bs58::decode(encoded_key) + .into(&mut key_bytes) + .map_err(ContractError::MalformedEd25519PublicKey)?; + bs58::decode(signature) + .into(&mut signature_bytes) + .map_err(ContractError::MalformedEd25519Signature)?; + + let res = deps + .api + .ed25519_verify(owner.as_bytes(), &signature_bytes, &key_bytes)?; + + if !res { + Err(ContractError::InvalidEd25519Signature) + } else { + Ok(()) + } +} + pub fn try_add_dealer( - deps: DepsMut<'_>, + mut deps: DepsMut<'_>, env: Env, info: MessageInfo, + ed25519_key: EncodedEd25519PublicKey, + bte_key_with_proof: EncodedBTEPublicKeyWithProof, + owner_signature: String, ) -> Result { - verify_dealer(deps.as_ref(), &info.sender)?; + // check whether this sender is eligible to become a dealer + verify_dealer(deps.branch(), env.block.height, &info.sender)?; - todo!() + // check if this dealer actually has control of his ed25519 key + // (BTE key has a proof assigned so if a malformed key is provided, somebody should complaint + // and then get this dealers deposit for themselves) + if let Err(err) = validate_key_possession_signature( + deps.as_ref(), + &info.sender, + owner_signature, + &ed25519_key, + ) { + // TODO: it looks like blacklisting could use a constructor + let blacklisting = Blacklisting { + reason: BlacklistingReason::Ed25519PossessionVerificationFailure, + height: env.block.height, + expiration: Some(env.block.height + INVALID_ED25519_BLACKLISTING_EXPIRATION), + }; + BLACKLISTED_DEALERS.save(deps.storage, &info.sender, &blacklisting)?; + return Err(err); + } + + // validate and extract sent deposit + let _deposit = validate_dealer_deposit(info.funds)?; + + // if it was already a dealer in the past, assign the same node index + let node_index = + if let Some(prior_details) = PAST_DEALERS.may_load(deps.storage, &info.sender)? { + // since this dealer is going to become active now, remove it from the past dealers + PAST_DEALERS.remove(deps.storage, &info.sender); + prior_details.assigned_index + } else { + next_node_index(deps.storage)? + }; + + // save the dealer into the storage + let dealer_details = DealerDetails { + joined_at: env.block.height, + left_at: None, + blacklisting: None, + ed25519_public_key: ed25519_key, + bte_public_key_with_proof: bte_key_with_proof, + assigned_index: node_index, + }; + CURRENT_DEALERS.save(deps.storage, &info.sender, &dealer_details)?; + + Ok(Response::new().set_data(node_index.to_be_bytes())) }