Initial logic for adding new dealer

contains bunch of placeholder assumptions
This commit is contained in:
Jędrzej Stuczyński
2022-04-13 15:01:25 +01:00
parent 9d2b7d6940
commit 24f4b19e51
13 changed files with 321 additions and 30 deletions
@@ -1,6 +1,7 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// 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")]
@@ -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<BlockHeight>,
pub blacklisting: Option<Blacklisting>,
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<BlockHeight>,
}
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<BlockHeight>,
},
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,
},
}
+3
View File
@@ -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;
+1
View File
@@ -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";
+1
View File
@@ -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";
+1
View File
@@ -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";
+1
View File
@@ -217,6 +217,7 @@ dependencies = [
name = "coconut-dkg"
version = "0.1.0"
dependencies = [
"bs58",
"coconut-dkg-common",
"config",
"cosmwasm-std",
+1
View File
@@ -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"
+10
View File
@@ -0,0 +1,10 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// 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;
+24 -4
View File
@@ -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,
}
+16 -6
View File
@@ -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<Response, ContractError> {
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]
+18 -3
View File
@@ -1,8 +1,23 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// 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<NodeIndex> = 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<NodeIndex> {
// 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)
}
+134 -9
View File
@@ -1,19 +1,42 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// 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<Coin>) -> Result<Coin, ContractError> {
// 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<Response, ContractError> {
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()))
}