d8c84cc4d6
* wip * wip: wrap node's sphinx key with a manager * wip: choosing correct key for packet processing * further propagation of key rotation information * attaching key rotation information to reply surbs * added basic key rotation information to mixnet contract * wip: introducing cached queries for key rotation info from nym api * unified nym-api contract cache refreshing * finish packet decoding * multi api client + retrieving rotation id * rotating sphinx key files * logic for migrating config file * wip: putting new sphinx keys to self described endpoints * processing loop of KeyRotationController * fixed sphinx key loading * rotating bloomfilters * wired up KeyRotationController * flushing bloomfilters to disk and loading * most of nym-node changes * post rebase fixes * fixes due to backwards compatible hostkeys * split http state.rs file * dont use deprecated fields * fixed backwards compatible deserialisation of host information * split up node describe cache * added a dedicated CacheRefresher listener to perform full refresh outside the set interval * controlling announced sphinx keys within nym-api * retrieving rotation id when pulling topology * split nym-nodes http handlers * v2 nym-api endpoints to retrieve nodes with additional metadata information * bug fixes... * additional bugfixes and guards against stuck epoch * testnet manager: set first nym-api as the rewarder * fixed host information deserialisation * fixed panic during first key rotation * post rebase fixes * clippy * more guards against stuck epochs * added helper method to reset node's sphinx key * instantiate mixnet contract with custom key rotation validity * additional bugfixes and debugging nym-api deadlock * passing shutdown to nym apis client * remove dead test * post rebasing fixes * missing MixnetQueryClient variants * remove usage of deprecated methods in sdk example * fix: incorrect method signature * post rebasing fixes * attempt to retrieve key rotation id before doing any config migration work * ignore tests relying on networking behaviour * allow networking failures in certain tests
1735 lines
55 KiB
Rust
1735 lines
55 KiB
Rust
// Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net>
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
use crate::circulating_supply_api::cache::CirculatingSupplyCache;
|
|
use crate::ecash::api_routes::handlers::ecash_routes;
|
|
use crate::ecash::error::{EcashError, Result};
|
|
use crate::ecash::keys::KeyPairWithEpoch;
|
|
use crate::ecash::state::EcashState;
|
|
use crate::network::models::NetworkDetails;
|
|
use crate::node_describe_cache::cache::DescribedNodes;
|
|
use crate::node_status_api::handlers::unstable;
|
|
use crate::node_status_api::NodeStatusCache;
|
|
use crate::nym_contract_cache::cache::NymContractCache;
|
|
use crate::status::ApiStatusState;
|
|
use crate::support::caching::cache::SharedCache;
|
|
use crate::support::config;
|
|
use crate::support::http::state::chain_status::ChainStatusCache;
|
|
use crate::support::http::state::force_refresh::ForcedRefresh;
|
|
use crate::support::http::state::AppState;
|
|
use crate::support::nyxd::Client;
|
|
use crate::support::storage::NymApiStorage;
|
|
use crate::unstable_routes::v1::account::cache::AddressInfoCache;
|
|
use async_trait::async_trait;
|
|
use axum::Router;
|
|
use axum_test::http::StatusCode;
|
|
use axum_test::{TestResponse, TestServer};
|
|
use cosmwasm_std::testing::{message_info, mock_env};
|
|
use cosmwasm_std::{
|
|
from_json, to_json_binary, Addr, Binary, BlockInfo, CosmosMsg, Decimal, MessageInfo, WasmMsg,
|
|
};
|
|
use cw3::{Proposal, ProposalResponse, Vote, VoteInfo, VoteResponse, Votes};
|
|
use cw4::{Cw4Contract, MemberResponse};
|
|
use nym_api_requests::ecash::models::{
|
|
IssuedTicketbooksChallengeCommitmentRequestBody, IssuedTicketbooksChallengeCommitmentResponse,
|
|
IssuedTicketbooksForResponse, SignableMessageBody,
|
|
};
|
|
use nym_api_requests::ecash::{BlindSignRequestBody, BlindedSignatureResponse};
|
|
use nym_coconut_dkg_common::dealer::{
|
|
DealerDetails, DealerDetailsResponse, DealerType, RegisteredDealerDetails,
|
|
};
|
|
use nym_coconut_dkg_common::dealing::{
|
|
DealerDealingsStatusResponse, DealingChunkInfo, DealingMetadata, DealingStatus,
|
|
DealingStatusResponse, PartialContractDealing,
|
|
};
|
|
use nym_coconut_dkg_common::event_attributes::{DKG_PROPOSAL_ID, NODE_INDEX};
|
|
use nym_coconut_dkg_common::types::{
|
|
ChunkIndex, DealerRegistrationDetails, DealingIndex, EncodedBTEPublicKeyWithProof, Epoch,
|
|
EpochId, EpochState, PartialContractDealingData, State as ContractState,
|
|
};
|
|
use nym_coconut_dkg_common::verification_key::{ContractVKShare, VerificationKeyShare};
|
|
use nym_compact_ecash::BlindedSignature;
|
|
use nym_compact_ecash::{ttp_keygen, VerificationKeyAuth};
|
|
use nym_config::defaults::{NymNetworkDetails, ValidatorDetails};
|
|
use nym_contracts_common::IdentityKey;
|
|
use nym_credentials::IssuanceTicketBook;
|
|
use nym_credentials_interface::TicketType;
|
|
use nym_crypto::asymmetric::ed25519;
|
|
use nym_dkg::{NodeIndex, Threshold};
|
|
use nym_ecash_contract_common::blacklist::{BlacklistedAccountResponse, Blacklisting};
|
|
use nym_ecash_contract_common::deposit::{Deposit, DepositId, DepositResponse};
|
|
use nym_task::TaskClient;
|
|
use nym_validator_client::nym_api::routes::{
|
|
ECASH_BLIND_SIGN, ECASH_ISSUED_TICKETBOOKS_CHALLENGE_COMMITMENT, ECASH_ISSUED_TICKETBOOKS_FOR,
|
|
ECASH_ROUTES, V1_API_VERSION,
|
|
};
|
|
use nym_validator_client::nyxd::cosmwasm_client::logs::Log;
|
|
use nym_validator_client::nyxd::cosmwasm_client::types::ExecuteResult;
|
|
use nym_validator_client::nyxd::{AccountId, ExecTxResult, Fee, Hash, TxResponse};
|
|
use nym_validator_client::{EcashApiClient, NymApiClient};
|
|
use rand::rngs::OsRng;
|
|
use rand::RngCore;
|
|
use std::collections::{BTreeMap, HashMap};
|
|
use std::ops::Deref;
|
|
use std::str::FromStr;
|
|
use std::sync::atomic::{AtomicU64, Ordering};
|
|
use std::sync::{Arc, Mutex};
|
|
use std::time::Duration;
|
|
use tempfile::{tempdir, TempDir};
|
|
use time::Date;
|
|
use tokio::sync::RwLock;
|
|
|
|
pub(crate) mod fixtures;
|
|
pub(crate) mod helpers;
|
|
mod issued_ticketbooks;
|
|
|
|
const TEST_COIN_DENOM: &str = "unym";
|
|
const TEST_REWARDING_VALIDATOR_ADDRESS: &str = "n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0";
|
|
|
|
#[derive(Default, Debug)]
|
|
struct InternalCounters {
|
|
node_index_counter: NodeIndex,
|
|
tx_hash_counter: u64,
|
|
proposal_id_counter: u64,
|
|
|
|
#[allow(dead_code)]
|
|
deposit_id_counter: u32,
|
|
}
|
|
|
|
impl InternalCounters {
|
|
fn next_proposal_id(&mut self) -> NodeIndex {
|
|
self.proposal_id_counter += 1;
|
|
self.proposal_id_counter
|
|
}
|
|
|
|
fn next_node_index(&mut self) -> NodeIndex {
|
|
self.node_index_counter += 1;
|
|
self.node_index_counter
|
|
}
|
|
|
|
fn next_tx_hash(&mut self) -> Hash {
|
|
use sha2::Digest;
|
|
|
|
// just hash the current counter
|
|
self.tx_hash_counter += 1;
|
|
Hash::Sha256(sha2::Sha256::digest(self.tx_hash_counter.to_be_bytes()).into())
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
fn next_deposit_id(&mut self) -> DepositId {
|
|
self.deposit_id_counter += 1;
|
|
self.deposit_id_counter
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub(crate) struct Dealing {
|
|
// fake entry is created whenever the metadata is submitted
|
|
pub(crate) metadata: DealingMetadata,
|
|
pub(crate) chunks: BTreeMap<ChunkIndex, PartialContractDealingData>,
|
|
}
|
|
|
|
impl Dealing {
|
|
pub(crate) fn new_metadata_submission(
|
|
dealing_index: DealingIndex,
|
|
chunks: Vec<DealingChunkInfo>,
|
|
) -> Self {
|
|
Dealing {
|
|
metadata: DealingMetadata::new(dealing_index, chunks),
|
|
chunks: Default::default(),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn unchecked_rebuild(&self) -> Vec<u8> {
|
|
let mut data = Vec::new();
|
|
|
|
for (chunk_index, partial) in self.chunks.iter() {
|
|
assert!(self
|
|
.metadata
|
|
.submitted_chunks
|
|
.get(chunk_index)
|
|
.unwrap()
|
|
.status
|
|
.submitted());
|
|
|
|
data.append(&mut partial.clone())
|
|
}
|
|
|
|
data
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub(crate) struct FakeDkgContractState {
|
|
pub(crate) address: AccountId,
|
|
|
|
// pub(crate) dealers: HashMap<NodeIndex, DealerDetails>,
|
|
// pub(crate) past_dealers: HashMap<NodeIndex, DealerDetails>,
|
|
// pub(crate) initial_dealers: Option<InitialReplacementData>,
|
|
pub(crate) dealer_indices: HashMap<String, NodeIndex>,
|
|
|
|
// map of epoch id -> dealer -> info
|
|
pub(crate) dealers: HashMap<EpochId, HashMap<String, DealerRegistrationDetails>>,
|
|
|
|
// map of epoch id -> dealer -> dealings
|
|
pub(crate) dealings: HashMap<EpochId, HashMap<String, HashMap<DealingIndex, Dealing>>>,
|
|
|
|
// map of epoch id -> dealer -> vk share
|
|
pub(crate) verification_shares: HashMap<EpochId, HashMap<String, ContractVKShare>>,
|
|
|
|
pub(crate) epoch: Epoch,
|
|
pub(crate) contract_state: ContractState,
|
|
pub(crate) threshold: HashMap<EpochId, Threshold>,
|
|
}
|
|
|
|
impl FakeDkgContractState {
|
|
// pub(crate) fn verified_dealers(&self) -> Vec<Addr> {
|
|
// let epoch_id = self.epoch.epoch_id;
|
|
// let Some(shares) = self.verification_shares.get(&epoch_id) else {
|
|
// return Vec::new();
|
|
// };
|
|
//
|
|
// shares
|
|
// .values()
|
|
// .filter(|s| s.verified)
|
|
// .map(|s| s.owner.clone())
|
|
// .collect()
|
|
// }
|
|
|
|
fn reset_dkg_state(&mut self) {}
|
|
|
|
pub(crate) fn reset_epoch_in_reshare_mode(&mut self) {
|
|
self.reset_dkg_state();
|
|
self.epoch.state = EpochState::PublicKeySubmission { resharing: true };
|
|
self.epoch.epoch_id += 1;
|
|
}
|
|
|
|
pub(crate) fn reset_dkg(&mut self) {
|
|
self.reset_dkg_state();
|
|
self.epoch.state = EpochState::PublicKeySubmission { resharing: false };
|
|
self.epoch.epoch_id += 1;
|
|
}
|
|
|
|
pub(crate) fn get_registration_details(
|
|
&self,
|
|
addr: &str,
|
|
epoch_id: EpochId,
|
|
) -> Option<DealerRegistrationDetails> {
|
|
self.dealers.get(&epoch_id)?.get(addr).cloned()
|
|
}
|
|
|
|
pub(crate) fn get_dealer_details(
|
|
&self,
|
|
addr: &str,
|
|
epoch_id: EpochId,
|
|
) -> Option<DealerDetails> {
|
|
let registration_details = self.get_registration_details(addr, epoch_id)?;
|
|
let assigned_index = self.get_dealer_index(addr)?;
|
|
|
|
Some(DealerDetails {
|
|
address: Addr::unchecked(addr),
|
|
bte_public_key_with_proof: registration_details.bte_public_key_with_proof,
|
|
ed25519_identity: registration_details.ed25519_identity,
|
|
announce_address: registration_details.announce_address,
|
|
assigned_index,
|
|
})
|
|
}
|
|
|
|
// implementation copied from our contract
|
|
pub(crate) fn query_dealer_details(&self, addr: &str) -> DealerDetailsResponse {
|
|
let current_epoch_id = self.epoch.epoch_id;
|
|
|
|
// if the address has registration data for the current epoch, it means it's an active dealer
|
|
if let Some(dealer_details) = self.get_dealer_details(addr, current_epoch_id) {
|
|
let assigned_index = dealer_details.assigned_index;
|
|
return DealerDetailsResponse::new(
|
|
Some(dealer_details),
|
|
DealerType::Current { assigned_index },
|
|
);
|
|
}
|
|
|
|
// and if has had an assigned index it must have been a dealer at some point in the past
|
|
if let Some(assigned_index) = self.get_dealer_index(addr) {
|
|
return DealerDetailsResponse::new(None, DealerType::Past { assigned_index });
|
|
}
|
|
|
|
DealerDetailsResponse::new(None, DealerType::Unknown)
|
|
}
|
|
|
|
pub(crate) fn get_dealer_index(&self, addr: &str) -> Option<NodeIndex> {
|
|
self.dealer_indices.get(addr).copied()
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub(crate) struct FakeGroupContractState {
|
|
pub(crate) address: Addr,
|
|
pub(crate) members: HashMap<String, MemberResponse>,
|
|
}
|
|
|
|
impl FakeGroupContractState {
|
|
pub(crate) fn total_weight(&self) -> u64 {
|
|
self.members
|
|
.values()
|
|
.map(|m| m.weight.unwrap_or_default())
|
|
.sum()
|
|
}
|
|
|
|
pub(crate) fn add_member<S: Into<String>>(&mut self, address: S, weight: u64) {
|
|
self.members.insert(
|
|
address.into(),
|
|
MemberResponse {
|
|
weight: Some(weight),
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub(crate) struct FakeMultisigContractState {
|
|
pub(crate) address: Addr,
|
|
pub(crate) proposals: HashMap<u64, Proposal>,
|
|
pub(crate) votes: HashMap<(String, u64), VoteInfo>,
|
|
}
|
|
|
|
impl FakeMultisigContractState {
|
|
#[allow(dead_code)]
|
|
pub(crate) fn reset_votes(&mut self) {
|
|
self.votes = HashMap::new()
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub(crate) struct FakeEcashContractState {
|
|
pub(crate) address: Addr,
|
|
pub(crate) deposits: HashMap<DepositId, Deposit>,
|
|
pub(crate) blacklist: HashMap<String, Blacklisting>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Default)]
|
|
pub(crate) struct SharedFakeChain(Arc<Mutex<FakeChainState>>);
|
|
|
|
impl Deref for SharedFakeChain {
|
|
type Target = Arc<Mutex<FakeChainState>>;
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
&self.0
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub(crate) struct FakeChainState {
|
|
_counters: InternalCounters,
|
|
|
|
pub(crate) block_info: BlockInfo,
|
|
|
|
pub(crate) txs: HashMap<Hash, TxResponse>,
|
|
pub(crate) dkg_contract: FakeDkgContractState,
|
|
pub(crate) group_contract: FakeGroupContractState,
|
|
pub(crate) multisig_contract: FakeMultisigContractState,
|
|
pub(crate) ecash_contract: FakeEcashContractState,
|
|
}
|
|
|
|
impl Default for FakeChainState {
|
|
fn default() -> Self {
|
|
let multisig_contract =
|
|
Addr::unchecked("n14ph4e660eyqz0j36zlkaey4zgzexm5twkmjlqaequxr2cjm9eprqsmad6k");
|
|
let group_contract =
|
|
Addr::unchecked("n1pd7kfgvr5tpcv0xnlv46c4jsq9jg2r799xxrcwqdm4l2jhq2pjwqrmz5ju");
|
|
let dkg_contract =
|
|
Addr::unchecked("n1ahg0erc2fs6xx3j5m8sfx3ryuzdjh6kf6qm9plsf865fltekyrfsesac6a");
|
|
let ecash_contract =
|
|
Addr::unchecked("n16a32stm6kknhq5cc8rx77elr66pygf2hfszw7wvpq746x3uffylqkjar4l");
|
|
|
|
FakeChainState {
|
|
_counters: Default::default(),
|
|
|
|
block_info: mock_env().block,
|
|
txs: HashMap::new(),
|
|
|
|
dkg_contract: FakeDkgContractState {
|
|
address: dkg_contract.as_ref().parse().unwrap(),
|
|
dealer_indices: Default::default(),
|
|
dealers: HashMap::new(),
|
|
|
|
epoch: Epoch::default(),
|
|
contract_state: ContractState {
|
|
mix_denom: TEST_COIN_DENOM.to_string(),
|
|
multisig_addr: multisig_contract.clone(),
|
|
group_addr: Cw4Contract::new(group_contract.clone()),
|
|
key_size: 5,
|
|
},
|
|
dealings: HashMap::new(),
|
|
verification_shares: HashMap::new(),
|
|
threshold: HashMap::new(),
|
|
},
|
|
group_contract: FakeGroupContractState {
|
|
address: group_contract,
|
|
members: Default::default(),
|
|
},
|
|
multisig_contract: FakeMultisigContractState {
|
|
address: multisig_contract,
|
|
proposals: Default::default(),
|
|
votes: Default::default(),
|
|
},
|
|
ecash_contract: FakeEcashContractState {
|
|
address: ecash_contract,
|
|
deposits: Default::default(),
|
|
blacklist: Default::default(),
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
impl FakeChainState {
|
|
pub(crate) fn get_or_assign_dealer(&mut self, addr: &str) -> NodeIndex {
|
|
if let Some(index) = self.dkg_contract.dealer_indices.get(addr) {
|
|
*index
|
|
} else {
|
|
let new = self._counters.next_node_index();
|
|
self.dkg_contract
|
|
.dealer_indices
|
|
.insert(addr.to_string(), new);
|
|
new
|
|
}
|
|
}
|
|
|
|
pub(crate) fn total_group_weight(&self) -> u64 {
|
|
self.group_contract.total_weight()
|
|
}
|
|
|
|
pub(crate) fn add_member<S: Into<String>>(&mut self, address: S, weight: u64) {
|
|
self.group_contract.add_member(address, weight)
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
pub(crate) fn reset_votes(&mut self) {
|
|
self.multisig_contract.reset_votes()
|
|
}
|
|
|
|
pub(crate) fn advance_epoch_in_reshare_mode(&mut self) {
|
|
self.dkg_contract.reset_epoch_in_reshare_mode()
|
|
}
|
|
|
|
#[allow(unused)]
|
|
pub(crate) fn advance_epoch_in_reset_mode(&mut self) {
|
|
self.dkg_contract.reset_dkg()
|
|
}
|
|
|
|
// TODO: make it return a result
|
|
fn execute_dkg_contract(&mut self, sender: MessageInfo, msg: &Binary) {
|
|
let exec_msg: nym_coconut_dkg_common::msg::ExecuteMsg = from_json(msg).unwrap();
|
|
match exec_msg {
|
|
nym_coconut_dkg_common::msg::ExecuteMsg::VerifyVerificationKeyShare {
|
|
owner,
|
|
resharing,
|
|
} => {
|
|
if sender.sender != self.multisig_contract.address {
|
|
panic!("not multisig")
|
|
}
|
|
assert_eq!(
|
|
self.dkg_contract.epoch.state,
|
|
EpochState::VerificationKeyFinalization { resharing }
|
|
);
|
|
let epoch_id = self.dkg_contract.epoch.epoch_id;
|
|
let Some(shares) = self.dkg_contract.verification_shares.get_mut(&epoch_id) else {
|
|
panic!("no shares for epoch")
|
|
};
|
|
let Some(share) = shares.get_mut(owner.as_str()) else {
|
|
panic!("no shares for owner")
|
|
};
|
|
share.verified = true
|
|
}
|
|
other => panic!("unimplemented exec of {other:?}"),
|
|
}
|
|
}
|
|
|
|
// TODO: make it return a result
|
|
fn execute_contract_msg(&mut self, contract: &String, msg: &Binary, sender: MessageInfo) {
|
|
if contract == self.group_contract.address.as_str() {
|
|
panic!("group contract exec")
|
|
}
|
|
if contract == self.multisig_contract.address.as_str() {
|
|
panic!("multisig contract exec")
|
|
}
|
|
if contract == self.ecash_contract.address.as_str() {
|
|
panic!("bandwidth contract exec")
|
|
}
|
|
if contract == self.dkg_contract.address.as_ref() {
|
|
return self.execute_dkg_contract(sender, msg);
|
|
}
|
|
panic!("unknown contract {contract}")
|
|
}
|
|
|
|
// TODO: make it return a result
|
|
fn execute_wasm_msg(&mut self, msg: &WasmMsg, sender_address: Addr) {
|
|
match msg {
|
|
WasmMsg::Execute {
|
|
contract_addr,
|
|
msg,
|
|
funds,
|
|
} => {
|
|
let sender = message_info(&sender_address, funds);
|
|
self.execute_contract_msg(contract_addr, msg, sender)
|
|
}
|
|
other => panic!("unimplemented wasm proposal for {other:?}"),
|
|
}
|
|
}
|
|
|
|
// TODO: make it return a result
|
|
pub(crate) fn execute_msg(&mut self, msg: &CosmosMsg, sender_address: AccountId) {
|
|
match msg {
|
|
CosmosMsg::Wasm(wasm_msg) => {
|
|
self.execute_wasm_msg(wasm_msg, Addr::unchecked(sender_address.as_ref()))
|
|
}
|
|
other => panic!("unimplemented proposal for {other:?}"),
|
|
};
|
|
}
|
|
}
|
|
|
|
fn proposal_to_response(
|
|
proposal_id: u64,
|
|
block: &BlockInfo,
|
|
proposal: Proposal,
|
|
) -> ProposalResponse {
|
|
// replicate behaviour from `query_proposal` of cw3
|
|
let status = proposal.current_status(block);
|
|
let threshold = proposal.threshold.to_response(proposal.total_weight);
|
|
ProposalResponse {
|
|
id: proposal_id,
|
|
title: proposal.title,
|
|
description: proposal.description,
|
|
msgs: proposal.msgs,
|
|
status,
|
|
expires: proposal.expires,
|
|
threshold,
|
|
proposer: proposal.proposer,
|
|
deposit: proposal.deposit,
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub(crate) struct DummyClient {
|
|
validator_address: AccountId,
|
|
|
|
state: SharedFakeChain,
|
|
}
|
|
|
|
impl DummyClient {
|
|
pub fn new(validator_address: AccountId, state: SharedFakeChain) -> Self {
|
|
Self {
|
|
validator_address,
|
|
state,
|
|
}
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
pub fn chain_state(&self) -> SharedFakeChain {
|
|
self.state.clone()
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl super::client::Client for DummyClient {
|
|
async fn address(&self) -> Result<AccountId> {
|
|
Ok(self.validator_address.clone())
|
|
}
|
|
|
|
async fn dkg_contract_address(&self) -> Result<AccountId> {
|
|
Ok(self.state.lock().unwrap().dkg_contract.address.clone())
|
|
}
|
|
|
|
async fn get_deposit(&self, deposit_id: DepositId) -> Result<DepositResponse> {
|
|
let deposit = self
|
|
.state
|
|
.lock()
|
|
.unwrap()
|
|
.ecash_contract
|
|
.deposits
|
|
.get(&deposit_id)
|
|
.cloned();
|
|
|
|
Ok(DepositResponse {
|
|
id: deposit_id,
|
|
deposit,
|
|
})
|
|
}
|
|
|
|
async fn get_proposal(&self, proposal_id: u64) -> Result<ProposalResponse> {
|
|
let chain = self.state.lock().unwrap();
|
|
let proposal = chain
|
|
.multisig_contract
|
|
.proposals
|
|
.get(&proposal_id)
|
|
.cloned()
|
|
.expect("proposal not found");
|
|
|
|
// replicate behaviour from `query_proposal` of cw3
|
|
Ok(proposal_to_response(
|
|
proposal_id,
|
|
&chain.block_info,
|
|
proposal,
|
|
))
|
|
}
|
|
|
|
async fn list_proposals(&self) -> Result<Vec<ProposalResponse>> {
|
|
let chain = self.state.lock().unwrap();
|
|
let block = &chain.block_info;
|
|
|
|
Ok(chain
|
|
.multisig_contract
|
|
.proposals
|
|
.iter()
|
|
.map(|(id, proposal)| proposal_to_response(*id, block, proposal.clone()))
|
|
.collect())
|
|
}
|
|
|
|
async fn get_vote(&self, proposal_id: u64, voter: String) -> Result<VoteResponse> {
|
|
let vote = self
|
|
.state
|
|
.lock()
|
|
.unwrap()
|
|
.multisig_contract
|
|
.votes
|
|
.get(&(voter, proposal_id))
|
|
.cloned();
|
|
|
|
Ok(VoteResponse { vote })
|
|
}
|
|
|
|
async fn get_blacklisted_account(
|
|
&self,
|
|
public_key: String,
|
|
) -> Result<BlacklistedAccountResponse> {
|
|
Ok(BlacklistedAccountResponse::new(
|
|
self.state
|
|
.lock()
|
|
.unwrap()
|
|
.ecash_contract
|
|
.blacklist
|
|
.get(&public_key)
|
|
.cloned(),
|
|
))
|
|
}
|
|
|
|
async fn contract_state(&self) -> Result<ContractState> {
|
|
Ok(self
|
|
.state
|
|
.lock()
|
|
.unwrap()
|
|
.dkg_contract
|
|
.contract_state
|
|
.clone())
|
|
}
|
|
|
|
async fn get_current_epoch(&self) -> Result<Epoch> {
|
|
Ok(self.state.lock().unwrap().dkg_contract.epoch)
|
|
}
|
|
|
|
async fn group_member(&self, addr: String) -> Result<MemberResponse> {
|
|
Ok(self
|
|
.state
|
|
.lock()
|
|
.unwrap()
|
|
.group_contract
|
|
.members
|
|
.get(&addr)
|
|
.cloned()
|
|
.unwrap_or(MemberResponse { weight: None }))
|
|
}
|
|
|
|
async fn get_current_epoch_threshold(&self) -> Result<Option<Threshold>> {
|
|
let guard = self.state.lock().unwrap();
|
|
let current_epoch = guard.dkg_contract.epoch.epoch_id;
|
|
Ok(guard.dkg_contract.threshold.get(¤t_epoch).cloned())
|
|
}
|
|
|
|
async fn get_epoch_threshold(&self, epoch_id: EpochId) -> Result<Option<Threshold>> {
|
|
Ok(self
|
|
.state
|
|
.lock()
|
|
.unwrap()
|
|
.dkg_contract
|
|
.threshold
|
|
.get(&epoch_id)
|
|
.cloned())
|
|
}
|
|
|
|
async fn get_self_registered_dealer_details(&self) -> Result<DealerDetailsResponse> {
|
|
let address = self.validator_address.as_ref();
|
|
Ok(self
|
|
.state
|
|
.lock()
|
|
.unwrap()
|
|
.dkg_contract
|
|
.query_dealer_details(address))
|
|
}
|
|
|
|
async fn get_registered_dealer_details(
|
|
&self,
|
|
epoch_id: EpochId,
|
|
dealer: String,
|
|
) -> Result<RegisteredDealerDetails> {
|
|
let details = self
|
|
.state
|
|
.lock()
|
|
.unwrap()
|
|
.dkg_contract
|
|
.dealers
|
|
.get(&epoch_id)
|
|
.and_then(|dealers| dealers.get(&dealer))
|
|
.cloned();
|
|
Ok(RegisteredDealerDetails { details })
|
|
}
|
|
|
|
async fn get_dealer_dealings_status(
|
|
&self,
|
|
epoch_id: EpochId,
|
|
dealer: String,
|
|
) -> Result<DealerDealingsStatusResponse> {
|
|
let guard = self.state.lock().unwrap();
|
|
let key_size = guard.dkg_contract.contract_state.key_size;
|
|
|
|
let dealer_addr = Addr::unchecked(&dealer);
|
|
|
|
let Some(epoch_dealings) = guard.dkg_contract.dealings.get(&epoch_id) else {
|
|
return Ok(DealerDealingsStatusResponse {
|
|
epoch_id,
|
|
dealer: dealer_addr,
|
|
all_dealings_fully_submitted: false,
|
|
dealing_submission_status: Default::default(),
|
|
});
|
|
};
|
|
|
|
let Some(dealer_dealings) = epoch_dealings.get(&dealer) else {
|
|
return Ok(DealerDealingsStatusResponse {
|
|
epoch_id,
|
|
dealer: dealer_addr,
|
|
all_dealings_fully_submitted: false,
|
|
dealing_submission_status: Default::default(),
|
|
});
|
|
};
|
|
|
|
let mut dealing_submission_status: BTreeMap<DealingIndex, DealingStatus> = BTreeMap::new();
|
|
for dealing_index in 0..key_size {
|
|
let metadata = dealer_dealings
|
|
.get(&dealing_index)
|
|
.map(|d| d.metadata.clone());
|
|
dealing_submission_status.insert(dealing_index, metadata.into());
|
|
}
|
|
|
|
Ok(DealerDealingsStatusResponse {
|
|
epoch_id,
|
|
dealer: Addr::unchecked(&dealer),
|
|
all_dealings_fully_submitted: dealing_submission_status
|
|
.values()
|
|
.all(|d| d.fully_submitted),
|
|
dealing_submission_status,
|
|
})
|
|
}
|
|
|
|
async fn get_dealing_status(
|
|
&self,
|
|
epoch_id: EpochId,
|
|
dealer: String,
|
|
dealing_index: DealingIndex,
|
|
) -> Result<DealingStatusResponse> {
|
|
let guard = self.state.lock().unwrap();
|
|
|
|
let metadata = guard
|
|
.dkg_contract
|
|
.dealings
|
|
.get(&epoch_id)
|
|
.and_then(|epoch_dealings| epoch_dealings.get(&dealer))
|
|
.and_then(|dealer_dealings| dealer_dealings.get(&dealing_index))
|
|
.map(|info| info.metadata.clone());
|
|
|
|
Ok(DealingStatusResponse {
|
|
epoch_id,
|
|
dealer: Addr::unchecked(dealer),
|
|
dealing_index,
|
|
status: metadata.into(),
|
|
})
|
|
}
|
|
|
|
async fn get_current_dealers(&self) -> Result<Vec<DealerDetails>> {
|
|
let chain = self.state.lock().unwrap();
|
|
let current_epoch_id = chain.dkg_contract.epoch.epoch_id;
|
|
|
|
let Some(epoch_dealers) = chain.dkg_contract.dealers.get(¤t_epoch_id) else {
|
|
return Ok(Vec::new());
|
|
};
|
|
|
|
Ok(epoch_dealers
|
|
.iter()
|
|
.map(|(address, details)| {
|
|
let assigned_index = chain.dkg_contract.get_dealer_index(address).unwrap();
|
|
DealerDetails {
|
|
address: Addr::unchecked(address),
|
|
bte_public_key_with_proof: details.bte_public_key_with_proof.clone(),
|
|
ed25519_identity: details.ed25519_identity.clone(),
|
|
announce_address: details.announce_address.clone(),
|
|
assigned_index,
|
|
}
|
|
})
|
|
.collect())
|
|
}
|
|
|
|
async fn get_dealing_metadata(
|
|
&self,
|
|
epoch_id: EpochId,
|
|
dealer: String,
|
|
dealing_index: DealingIndex,
|
|
) -> Result<Option<DealingMetadata>> {
|
|
let guard = self.state.lock().unwrap();
|
|
|
|
let Some(epoch_dealings) = guard.dkg_contract.dealings.get(&epoch_id) else {
|
|
return Ok(None);
|
|
};
|
|
|
|
let Some(dealer_dealings) = epoch_dealings.get(&dealer) else {
|
|
return Ok(None);
|
|
};
|
|
|
|
let Some(dealing) = dealer_dealings.get(&dealing_index) else {
|
|
return Ok(None);
|
|
};
|
|
|
|
Ok(Some(dealing.metadata.clone()))
|
|
}
|
|
|
|
async fn get_dealing_chunk(
|
|
&self,
|
|
epoch_id: EpochId,
|
|
dealer: &str,
|
|
dealing_index: DealingIndex,
|
|
chunk_index: ChunkIndex,
|
|
) -> Result<Option<PartialContractDealingData>> {
|
|
let guard = self.state.lock().unwrap();
|
|
|
|
let Some(epoch_dealings) = guard.dkg_contract.dealings.get(&epoch_id) else {
|
|
return Ok(None);
|
|
};
|
|
|
|
let Some(dealer_dealings) = epoch_dealings.get(dealer) else {
|
|
return Ok(None);
|
|
};
|
|
|
|
let Some(dealing) = dealer_dealings.get(&dealing_index) else {
|
|
return Ok(None);
|
|
};
|
|
|
|
Ok(dealing.chunks.get(&chunk_index).cloned())
|
|
}
|
|
|
|
async fn get_verification_key_share(
|
|
&self,
|
|
epoch_id: EpochId,
|
|
dealer: String,
|
|
) -> Result<Option<ContractVKShare>> {
|
|
let guard = self.state.lock().unwrap();
|
|
let epoch_shares = guard.dkg_contract.verification_shares.get(&epoch_id);
|
|
|
|
match epoch_shares {
|
|
None => Ok(None),
|
|
Some(epoch_shares) => Ok(epoch_shares.get(&dealer).cloned()),
|
|
}
|
|
}
|
|
|
|
async fn get_verification_key_shares(&self, epoch_id: EpochId) -> Result<Vec<ContractVKShare>> {
|
|
let guard = self.state.lock().unwrap();
|
|
let epoch_shares = guard.dkg_contract.verification_shares.get(&epoch_id);
|
|
|
|
match epoch_shares {
|
|
None => Ok(Vec::new()),
|
|
Some(epoch_shares) => Ok(epoch_shares.values().cloned().collect()),
|
|
}
|
|
}
|
|
|
|
async fn get_registered_ecash_clients(&self, epoch_id: EpochId) -> Result<Vec<EcashApiClient>> {
|
|
Ok(self
|
|
.get_verification_key_shares(epoch_id)
|
|
.await?
|
|
.into_iter()
|
|
.map(|s| s.try_into().unwrap())
|
|
.collect())
|
|
}
|
|
|
|
async fn vote_proposal(
|
|
&self,
|
|
proposal_id: u64,
|
|
vote_yes: bool,
|
|
_fee: Option<Fee>,
|
|
) -> Result<()> {
|
|
let voter = self.validator_address.to_string();
|
|
let mut chain = self.state.lock().unwrap();
|
|
if !chain.multisig_contract.proposals.contains_key(&proposal_id) {
|
|
panic!("proposal not found");
|
|
}
|
|
|
|
// for now we assume every group member is a voter
|
|
let weight = chain
|
|
.group_contract
|
|
.members
|
|
.get(&voter)
|
|
.expect("todo: not a voter")
|
|
.weight
|
|
.expect("no vote weight");
|
|
|
|
let vote = if vote_yes { Vote::Yes } else { Vote::No };
|
|
|
|
if chain
|
|
.multisig_contract
|
|
.votes
|
|
.contains_key(&(voter.clone(), proposal_id))
|
|
{
|
|
panic!("unhandled case: already voted");
|
|
}
|
|
chain.multisig_contract.votes.insert(
|
|
(voter.clone(), proposal_id),
|
|
VoteInfo {
|
|
proposal_id,
|
|
voter,
|
|
vote,
|
|
weight,
|
|
},
|
|
);
|
|
|
|
let block_info = chain.block_info.clone();
|
|
if let Some(proposal) = chain.multisig_contract.proposals.get_mut(&proposal_id) {
|
|
proposal.votes.add_vote(vote, weight);
|
|
proposal.update_status(&block_info)
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
async fn execute_proposal(&self, proposal_id: u64) -> Result<()> {
|
|
let mut chain = self.state.lock().unwrap();
|
|
let multisig_address: AccountId = chain.multisig_contract.address.as_str().parse().unwrap();
|
|
|
|
let Some(proposal) = chain.multisig_contract.proposals.get_mut(&proposal_id) else {
|
|
panic!("proposal not found");
|
|
};
|
|
|
|
if proposal.status != cw3::Status::Passed {
|
|
panic!("proposal hasn't been passed")
|
|
}
|
|
proposal.status = cw3::Status::Executed;
|
|
|
|
for msg in &proposal.msgs.clone() {
|
|
chain.execute_msg(msg, multisig_address.clone());
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn can_advance_epoch_state(&self) -> Result<bool> {
|
|
// TODO: incorporate the short-circuiting logic in here
|
|
let chain = self.state.lock().unwrap();
|
|
let epoch = chain.dkg_contract.epoch;
|
|
Ok(if let Some(finish_timestamp) = epoch.deadline {
|
|
finish_timestamp <= chain.block_info.time
|
|
} else {
|
|
false
|
|
})
|
|
}
|
|
|
|
async fn advance_epoch_state(&self) -> Result<()> {
|
|
todo!()
|
|
}
|
|
|
|
async fn register_dealer(
|
|
&self,
|
|
bte_public_key_with_proof: EncodedBTEPublicKeyWithProof,
|
|
identity_key: IdentityKey,
|
|
announce_address: String,
|
|
_resharing: bool,
|
|
) -> Result<ExecuteResult> {
|
|
let mut guard = self.state.lock().unwrap();
|
|
let assigned_index = guard.get_or_assign_dealer(self.validator_address.as_ref());
|
|
let epoch = guard.dkg_contract.epoch.epoch_id;
|
|
|
|
let dealer_details = DealerRegistrationDetails {
|
|
bte_public_key_with_proof,
|
|
ed25519_identity: identity_key,
|
|
announce_address,
|
|
};
|
|
|
|
let epoch_dealers = guard.dkg_contract.dealers.entry(epoch).or_default();
|
|
if !epoch_dealers.contains_key(self.validator_address.as_ref()) {
|
|
epoch_dealers.insert(self.validator_address.to_string(), dealer_details);
|
|
} else {
|
|
panic!("already registered")
|
|
}
|
|
|
|
let transaction_hash = guard._counters.next_tx_hash();
|
|
|
|
Ok(ExecuteResult {
|
|
logs: vec![Log {
|
|
msg_index: 0,
|
|
events: vec![cosmwasm_std::Event::new("wasm")
|
|
.add_attribute(NODE_INDEX, assigned_index.to_string())],
|
|
}],
|
|
msg_responses: Default::default(),
|
|
events: Default::default(),
|
|
transaction_hash,
|
|
gas_info: Default::default(),
|
|
})
|
|
}
|
|
|
|
async fn submit_dealing_metadata(
|
|
&self,
|
|
dealing_index: DealingIndex,
|
|
chunks: Vec<DealingChunkInfo>,
|
|
_resharing: bool,
|
|
) -> Result<ExecuteResult> {
|
|
let mut guard = self.state.lock().unwrap();
|
|
let current_epoch = guard.dkg_contract.epoch.epoch_id;
|
|
|
|
let epoch_dealings = guard
|
|
.dkg_contract
|
|
.dealings
|
|
.entry(current_epoch)
|
|
.or_default();
|
|
|
|
let dealer_dealings = epoch_dealings
|
|
.entry(self.validator_address.to_string())
|
|
.or_default();
|
|
dealer_dealings.insert(
|
|
dealing_index,
|
|
Dealing::new_metadata_submission(dealing_index, chunks),
|
|
);
|
|
|
|
let transaction_hash = guard._counters.next_tx_hash();
|
|
|
|
Ok(ExecuteResult {
|
|
logs: vec![],
|
|
msg_responses: Default::default(),
|
|
events: Default::default(),
|
|
transaction_hash,
|
|
gas_info: Default::default(),
|
|
})
|
|
}
|
|
|
|
async fn submit_dealing_chunk(&self, chunk: PartialContractDealing) -> Result<ExecuteResult> {
|
|
let mut guard = self.state.lock().unwrap();
|
|
let current_epoch = guard.dkg_contract.epoch.epoch_id;
|
|
let current_height = guard.block_info.height;
|
|
|
|
// normally we should do checks for existence, etc.
|
|
// but since this is a testing code, we assume everything is sent in order and the appropriate entries exist
|
|
let epoch_dealings = guard.dkg_contract.dealings.get_mut(¤t_epoch).unwrap();
|
|
|
|
let dealer_dealings = epoch_dealings
|
|
.get_mut(self.validator_address.as_ref())
|
|
.unwrap();
|
|
|
|
let dealing_chunks = dealer_dealings.get_mut(&chunk.dealing_index).unwrap();
|
|
dealing_chunks.chunks.insert(chunk.chunk_index, chunk.data);
|
|
|
|
dealing_chunks
|
|
.metadata
|
|
.submitted_chunks
|
|
.get_mut(&chunk.chunk_index)
|
|
.unwrap()
|
|
.status
|
|
.submission_height = Some(current_height);
|
|
|
|
let transaction_hash = guard._counters.next_tx_hash();
|
|
|
|
Ok(ExecuteResult {
|
|
logs: vec![],
|
|
msg_responses: Default::default(),
|
|
events: Default::default(),
|
|
transaction_hash,
|
|
gas_info: Default::default(),
|
|
})
|
|
}
|
|
|
|
async fn submit_verification_key_share(
|
|
&self,
|
|
share: VerificationKeyShare,
|
|
resharing: bool,
|
|
) -> Result<ExecuteResult> {
|
|
let mut chain = self.state.lock().unwrap();
|
|
|
|
let address = self.validator_address.to_string();
|
|
let epoch_id = chain.dkg_contract.epoch.epoch_id;
|
|
let Some(dealer_details) = chain.dkg_contract.get_dealer_details(&address, epoch_id) else {
|
|
// Just throw some error, not really the correct one
|
|
return Err(EcashError::NotASigner);
|
|
};
|
|
|
|
let dkg_contract = chain.dkg_contract.address.clone();
|
|
|
|
chain
|
|
.dkg_contract
|
|
.verification_shares
|
|
.entry(epoch_id)
|
|
.or_default()
|
|
.insert(
|
|
self.validator_address.to_string(),
|
|
ContractVKShare {
|
|
share,
|
|
announce_address: dealer_details.announce_address.clone(),
|
|
node_index: dealer_details.assigned_index,
|
|
owner: Addr::unchecked(&address),
|
|
epoch_id,
|
|
verified: false,
|
|
},
|
|
);
|
|
|
|
let proposal_id = chain._counters.next_proposal_id();
|
|
let verify_vk_share_req =
|
|
nym_coconut_dkg_common::msg::ExecuteMsg::VerifyVerificationKeyShare {
|
|
owner: address,
|
|
resharing,
|
|
};
|
|
let verify_vk_share_msg = CosmosMsg::Wasm(WasmMsg::Execute {
|
|
contract_addr: chain.dkg_contract.address.to_string(),
|
|
msg: to_json_binary(&verify_vk_share_req).unwrap(),
|
|
funds: vec![],
|
|
});
|
|
let proposal = Proposal {
|
|
title: String::new(),
|
|
description: String::new(),
|
|
msgs: vec![verify_vk_share_msg],
|
|
status: cw3::Status::Open,
|
|
expires: cw_utils::Expiration::Never {},
|
|
threshold: cw_utils::Threshold::AbsolutePercentage {
|
|
percentage: Decimal::from_ratio(2u32, 3u32),
|
|
},
|
|
total_weight: chain.total_group_weight(),
|
|
votes: Votes::yes(0),
|
|
proposer: Addr::unchecked(dkg_contract.as_ref()),
|
|
deposit: None,
|
|
start_height: 0,
|
|
};
|
|
chain
|
|
.multisig_contract
|
|
.proposals
|
|
.insert(proposal_id, proposal);
|
|
let transaction_hash = chain._counters.next_tx_hash();
|
|
Ok(ExecuteResult {
|
|
logs: vec![Log {
|
|
msg_index: 0,
|
|
events: vec![cosmwasm_std::Event::new("wasm")
|
|
.add_attribute(DKG_PROPOSAL_ID, proposal_id.to_string())],
|
|
}],
|
|
msg_responses: Default::default(),
|
|
events: Default::default(),
|
|
transaction_hash,
|
|
gas_info: Default::default(),
|
|
})
|
|
}
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
#[derive(Clone)]
|
|
pub struct DummyCommunicationChannel {
|
|
current_epoch: Arc<AtomicU64>,
|
|
ecash_clients: Arc<RwLock<HashMap<EpochId, Vec<EcashApiClient>>>>,
|
|
}
|
|
|
|
impl DummyCommunicationChannel {
|
|
pub fn new(ecash_clients: Vec<EcashApiClient>) -> Self {
|
|
let epoch_id = 1;
|
|
let mut ecash_clients_map = HashMap::new();
|
|
ecash_clients_map.insert(epoch_id, ecash_clients);
|
|
DummyCommunicationChannel {
|
|
current_epoch: Arc::new(AtomicU64::new(epoch_id)),
|
|
ecash_clients: Arc::new(RwLock::new(ecash_clients_map)),
|
|
}
|
|
}
|
|
|
|
pub fn new_single_dummy(
|
|
aggregated_verification_key: VerificationKeyAuth,
|
|
cosmos_address: AccountId,
|
|
) -> Self {
|
|
let client = EcashApiClient {
|
|
api_client: NymApiClient::new("http://localhost:1234".parse().unwrap()),
|
|
verification_key: aggregated_verification_key,
|
|
node_id: 1,
|
|
cosmos_address,
|
|
};
|
|
Self::new(vec![client])
|
|
}
|
|
|
|
pub fn clients_arc(&self) -> Arc<RwLock<HashMap<EpochId, Vec<EcashApiClient>>>> {
|
|
Arc::clone(&self.ecash_clients)
|
|
}
|
|
|
|
pub fn with_epoch(mut self, current_epoch: Arc<AtomicU64>) -> Self {
|
|
self.current_epoch = current_epoch;
|
|
self
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl super::comm::APICommunicationChannel for DummyCommunicationChannel {
|
|
async fn current_epoch(&self) -> Result<EpochId> {
|
|
Ok(self.current_epoch.load(Ordering::Relaxed))
|
|
}
|
|
|
|
async fn dkg_in_progress(&self) -> Result<bool> {
|
|
// deal with this later lol
|
|
Ok(false)
|
|
}
|
|
|
|
async fn ecash_clients(&self, epoch_id: EpochId) -> Result<Vec<EcashApiClient>> {
|
|
Ok(self
|
|
.ecash_clients
|
|
.read()
|
|
.await
|
|
.get(&epoch_id)
|
|
.cloned()
|
|
.unwrap_or_default())
|
|
}
|
|
|
|
async fn ecash_threshold(&self, _epoch_id: EpochId) -> Result<Threshold> {
|
|
todo!()
|
|
}
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
pub fn deposit_fixture() -> Deposit {
|
|
let mut rng = OsRng;
|
|
let identity_keypair = ed25519::KeyPair::new(&mut rng);
|
|
|
|
Deposit {
|
|
bs58_encoded_ed25519_pubkey: identity_keypair.public_key().to_base58_string(),
|
|
}
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
pub fn tx_entry_fixture(hash: Hash) -> TxResponse {
|
|
TxResponse {
|
|
hash,
|
|
height: Default::default(),
|
|
index: 0,
|
|
tx_result: ExecTxResult {
|
|
code: Default::default(),
|
|
data: Default::default(),
|
|
log: Default::default(),
|
|
info: Default::default(),
|
|
gas_wanted: Default::default(),
|
|
gas_used: Default::default(),
|
|
events: vec![],
|
|
codespace: Default::default(),
|
|
},
|
|
tx: vec![],
|
|
proof: None,
|
|
}
|
|
}
|
|
|
|
pub fn blinded_signature_fixture() -> BlindedSignature {
|
|
let gen1_bytes = [
|
|
151u8, 241, 211, 167, 49, 151, 215, 148, 38, 149, 99, 140, 79, 169, 172, 15, 195, 104, 140,
|
|
79, 151, 116, 185, 5, 161, 78, 58, 63, 23, 27, 172, 88, 108, 85, 232, 63, 249, 122, 26,
|
|
239, 251, 58, 240, 10, 219, 34, 198, 187,
|
|
];
|
|
|
|
let dummy_bytes = gen1_bytes
|
|
.iter()
|
|
.chain(gen1_bytes.iter())
|
|
.copied()
|
|
.collect::<Vec<_>>();
|
|
|
|
BlindedSignature::from_bytes(&dummy_bytes).unwrap()
|
|
}
|
|
|
|
pub fn voucher_fixture(deposit_id: Option<DepositId>) -> IssuanceTicketBook {
|
|
let mut rng = OsRng;
|
|
let deposit_id = deposit_id.unwrap_or(69);
|
|
|
|
let identity_keypair = ed25519::KeyPair::new(&mut rng);
|
|
|
|
let id_priv =
|
|
ed25519::PrivateKey::from_bytes(&identity_keypair.private_key().to_bytes()).unwrap();
|
|
let identifier = [44u8; 32];
|
|
// (voucher, request)
|
|
IssuanceTicketBook::new(deposit_id, identifier, id_priv, TicketType::V1MixnetEntry)
|
|
}
|
|
|
|
#[allow(unused)]
|
|
fn dummy_signature() -> ed25519::Signature {
|
|
"3vUCc6MCN5AC2LNgDYjRB1QeErZSN1S8f6K14JHjpUcKWXbjGYFExA8DbwQQBki9gyUqrpBF94Drttb4eMcGQXkp"
|
|
.parse()
|
|
.unwrap()
|
|
}
|
|
|
|
struct TestFixture {
|
|
axum: TestServer,
|
|
storage: NymApiStorage,
|
|
chain_state: SharedFakeChain,
|
|
epoch: Arc<AtomicU64>,
|
|
ecash_clients: Arc<RwLock<HashMap<EpochId, Vec<EcashApiClient>>>>,
|
|
|
|
_tmp_dir: TempDir,
|
|
}
|
|
|
|
impl TestFixture {
|
|
fn build_app_state(
|
|
storage: NymApiStorage,
|
|
ecash_state: EcashState,
|
|
nyxd_client: Client,
|
|
) -> AppState {
|
|
AppState {
|
|
nyxd_client,
|
|
chain_status_cache: ChainStatusCache::new(Duration::from_secs(42)),
|
|
address_info_cache: AddressInfoCache::new(Duration::from_secs(42), 1000),
|
|
forced_refresh: ForcedRefresh::new(true),
|
|
nym_contract_cache: NymContractCache::new(),
|
|
node_status_cache: NodeStatusCache::new(),
|
|
circulating_supply_cache: CirculatingSupplyCache::new("unym".to_owned()),
|
|
storage,
|
|
described_nodes_cache: SharedCache::<DescribedNodes>::new(),
|
|
network_details: NetworkDetails::new(
|
|
"localhost".to_string(),
|
|
NymNetworkDetails::new_empty(),
|
|
),
|
|
node_info_cache: unstable::NodeInfoCache::default(),
|
|
api_status: ApiStatusState::new(None),
|
|
ecash_state: Arc::new(ecash_state),
|
|
}
|
|
}
|
|
|
|
async fn new() -> Self {
|
|
let mut rng = crate::ecash::tests::fixtures::test_rng([69u8; 32]);
|
|
let coconut_keypair = ttp_keygen(1, 1).unwrap().remove(0);
|
|
let identity = ed25519::KeyPair::new(&mut rng);
|
|
let epoch = Arc::new(AtomicU64::new(1));
|
|
let address = AccountId::from_str(TEST_REWARDING_VALIDATOR_ADDRESS).unwrap();
|
|
let comm_channel = DummyCommunicationChannel::new_single_dummy(
|
|
coconut_keypair.verification_key().clone(),
|
|
address.clone(),
|
|
)
|
|
.with_epoch(epoch.clone());
|
|
let ecash_clients = comm_channel.clients_arc();
|
|
|
|
// TODO: it's AWFUL to test with actual storage, we should somehow abstract it away
|
|
let tmp_dir = tempdir().unwrap();
|
|
let storage = NymApiStorage::init(tmp_dir.path().join("TESTING_STORAGE.db"))
|
|
.await
|
|
.unwrap();
|
|
|
|
let staged_key_pair = crate::ecash::keys::KeyPair::new();
|
|
staged_key_pair
|
|
.set(KeyPairWithEpoch {
|
|
keys: coconut_keypair,
|
|
issued_for_epoch: 1,
|
|
})
|
|
.await;
|
|
staged_key_pair.validate();
|
|
|
|
let chain_state = SharedFakeChain::default();
|
|
let nyxd_client = DummyClient::new(address, chain_state.clone());
|
|
|
|
let ecash_contract = chain_state
|
|
.lock()
|
|
.unwrap()
|
|
.ecash_contract
|
|
.address
|
|
.clone()
|
|
.as_str()
|
|
.parse()
|
|
.unwrap();
|
|
|
|
let mut config = config::Config::new("test");
|
|
config.ecash_signer.enabled = true;
|
|
|
|
let ecash_state = EcashState::new(
|
|
&config,
|
|
ecash_contract,
|
|
nyxd_client,
|
|
identity,
|
|
staged_key_pair,
|
|
comm_channel,
|
|
storage.clone(),
|
|
TaskClient::dummy(),
|
|
);
|
|
|
|
// ideally this would have been generic, but that's way too much work
|
|
// since then `AppState` would have had to be made generic
|
|
// also, this is such a disgusting workaround to make it 'work'. yuck
|
|
let mut dummy = NymNetworkDetails::new_empty();
|
|
dummy.endpoints = vec![ValidatorDetails::new(
|
|
"http://127.0.0.1:26657",
|
|
Some("http://why-do-we-even-need-api-url-set-here.wtf"),
|
|
None,
|
|
)];
|
|
dummy.export_to_env();
|
|
let another_fake_nyxd_client = Client::new(&config).unwrap();
|
|
|
|
TestFixture {
|
|
axum: TestServer::new(Router::new().nest("/v1/ecash", ecash_routes()).with_state(
|
|
Self::build_app_state(storage.clone(), ecash_state, another_fake_nyxd_client),
|
|
))
|
|
.unwrap(),
|
|
storage,
|
|
chain_state,
|
|
epoch,
|
|
ecash_clients,
|
|
_tmp_dir: tmp_dir,
|
|
}
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
async fn set_epoch(&self, epoch: u64) {
|
|
let current_epoch = self.epoch.load(Ordering::Relaxed);
|
|
self.epoch.store(epoch, Ordering::Relaxed);
|
|
|
|
// copy the same epoch_signers as we had initially
|
|
let existing = self.ecash_clients.read().await.get(¤t_epoch).cloned();
|
|
if let Some(clients) = existing {
|
|
self.ecash_clients.write().await.insert(epoch, clients);
|
|
}
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
fn add_tx(&self, hash: Hash, tx: TxResponse) {
|
|
self.chain_state.lock().unwrap().txs.insert(hash, tx);
|
|
}
|
|
|
|
fn add_chain_deposit(&self, voucher_data: &IssuanceTicketBook) {
|
|
let mut chain = self.chain_state.lock().unwrap();
|
|
let deposit = Deposit {
|
|
bs58_encoded_ed25519_pubkey: voucher_data
|
|
.identity_key()
|
|
.public_key()
|
|
.to_base58_string(),
|
|
};
|
|
let existing = chain
|
|
.ecash_contract
|
|
.deposits
|
|
.insert(voucher_data.deposit_id(), deposit);
|
|
assert!(existing.is_none());
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
async fn issue_dummy_credential(&self) {
|
|
let mut rng = OsRng;
|
|
let deposit_id = rng.next_u32();
|
|
|
|
let voucher = voucher_fixture(Some(deposit_id));
|
|
|
|
let signing_data = voucher.prepare_for_signing();
|
|
let req = voucher.create_blind_sign_request_body(&signing_data);
|
|
|
|
self.add_chain_deposit(&voucher);
|
|
self.issue_ticketbook(req).await;
|
|
}
|
|
|
|
async fn issue_ticketbook(&self, req: BlindSignRequestBody) -> BlindedSignatureResponse {
|
|
let response = self
|
|
.axum
|
|
.post(&format!(
|
|
"/{V1_API_VERSION}/{ECASH_ROUTES}/{ECASH_BLIND_SIGN}"
|
|
))
|
|
.json(&req)
|
|
.await;
|
|
|
|
assert_eq!(response.status_code(), StatusCode::OK);
|
|
response.json()
|
|
}
|
|
|
|
async fn issued_ticketbooks_for_unchecked(
|
|
&self,
|
|
expiration_date: Date,
|
|
) -> IssuedTicketbooksForResponse {
|
|
let response = self
|
|
.axum
|
|
.get(&format!(
|
|
"/{V1_API_VERSION}/{ECASH_ROUTES}/{ECASH_ISSUED_TICKETBOOKS_FOR}/{expiration_date}"
|
|
))
|
|
.await;
|
|
|
|
assert_eq!(response.status_code(), StatusCode::OK);
|
|
response.json()
|
|
}
|
|
|
|
async fn issued_ticketbooks_challenge_commitment(
|
|
&self,
|
|
expiration_date: Date,
|
|
deposits: Vec<DepositId>,
|
|
) -> TestResponse {
|
|
let dummy_keypair = ed25519::KeyPair::new(&mut OsRng);
|
|
self.axum
|
|
.post(&format!(
|
|
"/{V1_API_VERSION}/{ECASH_ROUTES}/{ECASH_ISSUED_TICKETBOOKS_CHALLENGE_COMMITMENT}"
|
|
))
|
|
.json(
|
|
&IssuedTicketbooksChallengeCommitmentRequestBody {
|
|
expiration_date,
|
|
deposits,
|
|
}
|
|
.sign(dummy_keypair.private_key()),
|
|
)
|
|
.await
|
|
}
|
|
|
|
async fn issued_ticketbooks_challenge_commitment_unchecked(
|
|
&self,
|
|
expiration_date: Date,
|
|
deposits: Vec<DepositId>,
|
|
) -> IssuedTicketbooksChallengeCommitmentResponse {
|
|
let response = self
|
|
.issued_ticketbooks_challenge_commitment(expiration_date, deposits)
|
|
.await;
|
|
|
|
assert_eq!(response.status_code(), StatusCode::OK);
|
|
response.json()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod credential_tests {
|
|
use super::*;
|
|
use crate::ecash::storage::EcashStorageExt;
|
|
use axum::http::StatusCode;
|
|
use nym_task::TaskClient;
|
|
use nym_ticketbooks_merkle::MerkleLeaf;
|
|
|
|
#[tokio::test]
|
|
async fn already_issued() {
|
|
let voucher = voucher_fixture(None);
|
|
let signing_data = voucher.prepare_for_signing();
|
|
let request_body = voucher.create_blind_sign_request_body(&signing_data);
|
|
|
|
let deposit_id = request_body.deposit_id;
|
|
|
|
let test_fixture = TestFixture::new().await;
|
|
test_fixture.add_chain_deposit(&voucher);
|
|
|
|
let sig = blinded_signature_fixture();
|
|
let commitments = request_body.encode_join_commitments();
|
|
let expiration_date = request_body.expiration_date;
|
|
test_fixture
|
|
.storage
|
|
.store_issued_ticketbook(
|
|
deposit_id,
|
|
42,
|
|
&sig.to_bytes(),
|
|
&commitments,
|
|
expiration_date,
|
|
voucher.ticketbook_type(),
|
|
MerkleLeaf {
|
|
hash: vec![42u8; 32],
|
|
index: 0,
|
|
},
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
let response = test_fixture
|
|
.axum
|
|
.post(&format!(
|
|
"/{V1_API_VERSION}/{ECASH_ROUTES}/{ECASH_BLIND_SIGN}"
|
|
))
|
|
.json(&request_body)
|
|
.await;
|
|
|
|
assert_eq!(response.status_code(), StatusCode::OK);
|
|
let expected_response = BlindedSignatureResponse::new(sig);
|
|
let blinded_signature_response = response.json::<BlindedSignatureResponse>();
|
|
|
|
assert_eq!(
|
|
blinded_signature_response.to_bytes(),
|
|
expected_response.to_bytes()
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn state_functions() {
|
|
let mut rng = OsRng;
|
|
let identity = ed25519::KeyPair::new(&mut rng);
|
|
let address = AccountId::from_str(TEST_REWARDING_VALIDATOR_ADDRESS).unwrap();
|
|
|
|
let nyxd_client = DummyClient::new(address.clone(), Default::default());
|
|
let key_pair = ttp_keygen(1, 1).unwrap().remove(0);
|
|
let tmp_dir = tempdir().unwrap();
|
|
|
|
let storage = NymApiStorage::init(tmp_dir.path().join("storage.db"))
|
|
.await
|
|
.unwrap();
|
|
let comm_channel = DummyCommunicationChannel::new_single_dummy(
|
|
key_pair.verification_key().clone(),
|
|
address,
|
|
);
|
|
let staged_key_pair = crate::ecash::keys::KeyPair::new();
|
|
staged_key_pair
|
|
.set(KeyPairWithEpoch {
|
|
keys: key_pair,
|
|
issued_for_epoch: 1,
|
|
})
|
|
.await;
|
|
staged_key_pair.validate();
|
|
|
|
let mut config = config::Config::new("test");
|
|
config.ecash_signer.enabled = true;
|
|
|
|
let state = EcashState::new(
|
|
&config,
|
|
"n16a32stm6kknhq5cc8rx77elr66pygf2hfszw7wvpq746x3uffylqkjar4l"
|
|
.parse()
|
|
.unwrap(),
|
|
nyxd_client,
|
|
identity,
|
|
staged_key_pair,
|
|
comm_channel,
|
|
storage.clone(),
|
|
TaskClient::dummy(),
|
|
);
|
|
|
|
let deposit_id = 42;
|
|
assert!(state.already_issued(deposit_id).await.unwrap().is_none());
|
|
|
|
let voucher = voucher_fixture(None);
|
|
let signing_data = voucher.prepare_for_signing();
|
|
let request_body = voucher.create_blind_sign_request_body(&signing_data);
|
|
|
|
let commitments = request_body.encode_join_commitments();
|
|
let expiration_date = request_body.expiration_date;
|
|
let sig = blinded_signature_fixture();
|
|
storage
|
|
.store_issued_ticketbook(
|
|
deposit_id,
|
|
42,
|
|
&sig.to_bytes(),
|
|
&commitments,
|
|
expiration_date,
|
|
voucher.ticketbook_type(),
|
|
MerkleLeaf {
|
|
hash: vec![42u8; 32],
|
|
index: 0,
|
|
},
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
state
|
|
.already_issued(deposit_id)
|
|
.await
|
|
.unwrap()
|
|
.unwrap()
|
|
.to_bytes(),
|
|
blinded_signature_fixture().to_bytes()
|
|
);
|
|
|
|
let blinded_signature = BlindedSignature::from_bytes(&[
|
|
183, 217, 166, 113, 40, 123, 74, 25, 72, 31, 136, 19, 125, 95, 217, 228, 96, 113, 25,
|
|
240, 12, 102, 125, 11, 174, 20, 216, 82, 192, 71, 27, 194, 48, 20, 17, 95, 243, 179,
|
|
82, 21, 57, 143, 101, 19, 22, 186, 147, 13, 147, 238, 39, 119, 15, 36, 251, 131, 250,
|
|
38, 185, 113, 187, 40, 227, 107, 134, 190, 123, 183, 126, 176, 226, 173, 147, 137, 17,
|
|
175, 13, 115, 78, 222, 119, 93, 146, 116, 229, 0, 152, 51, 232, 2, 102, 204, 147, 202,
|
|
254, 243,
|
|
])
|
|
.unwrap();
|
|
|
|
// Check that the new payload is not stored if there was already something signed for tx_hash
|
|
let storage_err = storage
|
|
.store_issued_ticketbook(
|
|
deposit_id,
|
|
42,
|
|
&blinded_signature.to_bytes(),
|
|
&commitments,
|
|
expiration_date,
|
|
voucher.ticketbook_type(),
|
|
MerkleLeaf {
|
|
hash: vec![42u8; 32],
|
|
index: 1,
|
|
},
|
|
)
|
|
.await;
|
|
assert!(storage_err.is_err());
|
|
|
|
// And use a new deposit to store a new signature
|
|
let deposit_id = 69;
|
|
|
|
storage
|
|
.store_issued_ticketbook(
|
|
deposit_id,
|
|
42,
|
|
&blinded_signature.to_bytes(),
|
|
&commitments,
|
|
expiration_date,
|
|
voucher.ticketbook_type(),
|
|
MerkleLeaf {
|
|
hash: vec![42u8; 32],
|
|
index: 2,
|
|
},
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
// Check that the same value for tx_hash is returned
|
|
assert_eq!(
|
|
state
|
|
.already_issued(deposit_id)
|
|
.await
|
|
.unwrap()
|
|
.unwrap()
|
|
.to_bytes(),
|
|
blinded_signature.to_bytes()
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn blind_sign_correct() {
|
|
let deposit_id = 42;
|
|
|
|
let mut rng = OsRng;
|
|
let identity_keypair = ed25519::KeyPair::new(&mut rng);
|
|
let identifier = [42u8; 32];
|
|
let voucher = IssuanceTicketBook::new(
|
|
deposit_id,
|
|
identifier,
|
|
ed25519::PrivateKey::from_base58_string(
|
|
identity_keypair.private_key().to_base58_string(),
|
|
)
|
|
.unwrap(),
|
|
TicketType::V1MixnetEntry,
|
|
);
|
|
|
|
let deposit = Deposit {
|
|
bs58_encoded_ed25519_pubkey: voucher.identity_key().public_key().to_base58_string(),
|
|
};
|
|
|
|
let test = TestFixture::new().await;
|
|
test.chain_state
|
|
.lock()
|
|
.unwrap()
|
|
.ecash_contract
|
|
.deposits
|
|
.insert(voucher.deposit_id(), deposit);
|
|
|
|
let signing_data = voucher.prepare_for_signing();
|
|
let request_body = voucher.create_blind_sign_request_body(&signing_data);
|
|
|
|
let response = test
|
|
.axum
|
|
.post(&format!(
|
|
"/{V1_API_VERSION}/{ECASH_ROUTES}/{ECASH_BLIND_SIGN}"
|
|
))
|
|
.json(&request_body)
|
|
.await;
|
|
|
|
assert_eq!(response.status_code(), StatusCode::OK);
|
|
let _ = response.json::<BlindedSignatureResponse>();
|
|
}
|
|
|
|
#[test]
|
|
fn blind_sign_request_body_serde() {
|
|
let deposit_id = 123;
|
|
let issuance = voucher_fixture(Some(deposit_id));
|
|
let signing_data = issuance.prepare_for_signing();
|
|
let request = issuance.create_blind_sign_request_body(&signing_data);
|
|
|
|
let json_bytes = serde_json::to_vec(&request).unwrap();
|
|
let recovered: BlindSignRequestBody = serde_json::from_slice(&json_bytes).unwrap();
|
|
|
|
assert_eq!(recovered, request)
|
|
}
|
|
}
|