Files
nym/nym-api/src/coconut/tests/mod.rs
T
2024-03-27 16:04:51 +00:00

2130 lines
70 KiB
Rust

// Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::coconut::error::{CoconutError, Result};
use crate::coconut::keys::KeyPairWithEpoch;
use crate::coconut::state::State;
use crate::coconut::storage::CoconutStorageExt;
use crate::support::storage::NymApiStorage;
use async_trait::async_trait;
use cosmwasm_std::testing::{mock_env, mock_info};
use cosmwasm_std::{
coin, from_binary, to_binary, Addr, Binary, BlockInfo, CosmosMsg, Decimal, MessageInfo, WasmMsg,
};
use cw3::{Proposal, ProposalResponse, Vote, VoteInfo, VoteResponse, Votes};
use cw4::{Cw4Contract, MemberResponse};
use nym_api_requests::coconut::models::{IssuedCredentialBody, IssuedCredentialResponse};
use nym_api_requests::coconut::{BlindSignRequestBody, BlindedSignatureResponse};
use nym_coconut::{BlindedSignature, Parameters, VerificationKey};
use nym_coconut_bandwidth_contract_common::events::{
DEPOSITED_FUNDS_EVENT_TYPE, DEPOSIT_ENCRYPTION_KEY, DEPOSIT_IDENTITY_KEY, DEPOSIT_INFO,
DEPOSIT_VALUE,
};
use nym_coconut_bandwidth_contract_common::spend_credential::SpendCredentialResponse;
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_contracts_common::IdentityKey;
use nym_credentials::coconut::bandwidth::voucher::BandwidthVoucherIssuanceData;
use nym_credentials::coconut::bandwidth::CredentialType;
use nym_credentials::IssuanceBandwidthCredential;
use nym_crypto::asymmetric::{encryption, identity};
use nym_dkg::{NodeIndex, Threshold};
use nym_validator_client::nym_api::routes::{
API_VERSION, BANDWIDTH, COCONUT_BLIND_SIGN, COCONUT_ROUTES,
};
use nym_validator_client::nyxd::cosmwasm_client::logs::Log;
use nym_validator_client::nyxd::cosmwasm_client::types::ExecuteResult;
use nym_validator_client::nyxd::Coin;
use nym_validator_client::nyxd::{
AccountId, Algorithm, Event, EventAttribute, ExecTxResult, Fee, Hash, TxResponse,
};
use rand_07::rngs::OsRng;
use rand_07::RngCore;
use rocket::http::Status;
use rocket::local::asynchronous::Client;
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 tempfile::{tempdir, TempDir};
pub(crate) mod fixtures;
pub(crate) mod helpers;
mod issued_credentials;
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,
}
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())
}
}
#[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: Option<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) {
self.threshold = None;
}
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 {
pub(crate) fn reset_votes(&mut self) {
self.votes = HashMap::new()
}
}
#[derive(Debug)]
pub(crate) struct FakeBandwidthContractState {
pub(crate) address: Addr,
pub(crate) admin: Option<AccountId>,
pub(crate) spent_credentials: HashMap<String, SpendCredentialResponse>,
}
#[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) bandwidth_contract: FakeBandwidthContractState,
}
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 bandwidth_contract =
Addr::unchecked("n16a32stm6kknhq5cc8rx77elr66pygf2hfszw7wvpq746x3uffylqkjar4l");
let bandwidth_contract_admin =
"n1ahg0erc2fs6xx3j5m8sfx3ryuzdjh6kf6qm9plsf865fltekyrfsesac6a"
.parse()
.unwrap();
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: None,
},
group_contract: FakeGroupContractState {
address: group_contract,
members: Default::default(),
},
multisig_contract: FakeMultisigContractState {
address: multisig_contract,
proposals: Default::default(),
votes: Default::default(),
},
bandwidth_contract: FakeBandwidthContractState {
address: bandwidth_contract,
admin: Some(bandwidth_contract_admin),
spent_credentials: 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)
}
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_binary(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 {
unimplemented!("no shares for epoch")
};
let Some(share) = shares.get_mut(owner.as_str()) else {
unimplemented!("no shares for owner")
};
share.verified = true
}
other => unimplemented!("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 {
unimplemented!("group contract exec")
}
if contract == &self.multisig_contract.address {
unimplemented!("multisig contract exec")
}
if contract == &self.bandwidth_contract.address {
unimplemented!("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 = mock_info(sender_address.as_ref(), funds);
self.execute_contract_msg(contract_addr, msg, sender)
}
other => unimplemented!("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 => unimplemented!("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()
}
// pub fn with_tx_db(mut self, tx_db: &Arc<RwLock<HashMap<Hash, TxResponse>>>) -> Self {
// todo!()
// // self.tx_db = Arc::clone(tx_db);
// // self
// }
//
// pub fn with_proposal_db(
// mut self,
// proposal_db: &Arc<RwLock<HashMap<u64, ProposalResponse>>>,
// ) -> Self {
// todo!()
// // self.proposal_db = Arc::clone(proposal_db);
// // self
// }
//
// pub fn with_spent_credential_db(
// mut self,
// spent_credential_db: &Arc<RwLock<HashMap<String, SpendCredentialResponse>>>,
// ) -> Self {
// todo!()
// // self.spent_credential_db = Arc::clone(spent_credential_db);
// // self
// }
//
// pub fn _with_epoch(mut self, epoch: &Arc<RwLock<Epoch>>) -> Self {
// todo!()
// // self.epoch = Arc::clone(epoch);
// // self
// }
//
// pub fn with_dealer_details(
// mut self,
// dealer_details: &Arc<RwLock<HashMap<String, (DealerDetails, bool)>>>,
// ) -> Self {
// todo!()
// // self.dealer_details = Arc::clone(dealer_details);
// // self
// }
//
// pub fn with_threshold(mut self, threshold: &Arc<RwLock<Option<Threshold>>>) -> Self {
// todo!()
// // self.threshold = Arc::clone(threshold);
// // self
// }
//
// // it's a really bad practice, but I'm not going to be changing it now...
// #[allow(clippy::type_complexity)]
// pub fn with_dealings(
// mut self,
// dealings: &Arc<RwLock<HashMap<EpochId, HashMap<String, Vec<PartialContractDealing>>>>>,
// ) -> Self {
// todo!()
// // self.dealings = Arc::clone(dealings);
// // self
// }
//
// pub fn with_verification_share(
// mut self,
// verification_share: &Arc<RwLock<HashMap<String, ContractVKShare>>>,
// ) -> Self {
// todo!()
// // self.verification_share = Arc::clone(verification_share);
// // self
// }
//
// pub fn _with_group_db(
// mut self,
// group_db: &Arc<RwLock<HashMap<String, MemberResponse>>>,
// ) -> Self {
// todo!()
// // self.group_db = Arc::clone(group_db);
// // self
// }
//
// pub fn with_initial_dealers_db(
// mut self,
// initial_dealers: &Arc<RwLock<Option<InitialReplacementData>>>,
// ) -> Self {
// todo!()
// // self.initial_dealers_db = Arc::clone(initial_dealers);
// // self
// }
// async fn get_dealer_by_address(&self, address: &str) -> Option<DealerDetails> {
// let guard = self.state.lock().unwrap();
// for dealer in guard.dkg_contract.dealers.values() {
// if dealer.address.as_str() == address {
// return Some(dealer.clone());
// }
// }
// None
// }
//
// async fn get_past_dealer_by_address(&self, address: &str) -> Option<DealerDetails> {
// let guard = self.state.lock().unwrap();
// for dealer in guard.dkg_contract.past_dealers.values() {
// if dealer.address.as_str() == address {
// return Some(dealer.clone());
// }
// }
// None
// }
}
#[async_trait]
impl super::client::Client for DummyClient {
async fn address(&self) -> AccountId {
self.validator_address.clone()
}
async fn dkg_contract_address(&self) -> Result<AccountId> {
Ok(self.state.lock().unwrap().dkg_contract.address.clone())
}
async fn bandwidth_contract_admin(&self) -> Result<Option<AccountId>> {
Ok(self.state.lock().unwrap().bandwidth_contract.admin.clone())
}
async fn get_tx(&self, tx_hash: Hash) -> Result<TxResponse> {
Ok(self
.state
.lock()
.unwrap()
.txs
.get(&tx_hash)
.cloned()
.unwrap())
}
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()
.ok_or(CoconutError::IncorrectProposal {
reason: String::from("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_spent_credential(
&self,
blinded_serial_number: String,
) -> Result<SpendCredentialResponse> {
self.state
.lock()
.unwrap()
.bandwidth_contract
.spent_credentials
.get(&blinded_serial_number)
.cloned()
.ok_or(CoconutError::InvalidCredentialStatus {
status: String::from("spent credential not found"),
})
}
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>> {
Ok(self.state.lock().unwrap().dkg_contract.threshold)
}
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(&current_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 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) {
return Err(CoconutError::IncorrectProposal {
reason: String::from("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))
{
todo!("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 {
return Err(CoconutError::ProposalIdError {
reason: String::from("proposal id not found"),
});
};
if proposal.status != cw3::Status::Passed {
unimplemented!("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 {
unimplemented!("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())],
}],
data: 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![],
data: 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(&current_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![],
data: 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(CoconutError::DepositEncrKeyNotFound);
};
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_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())],
}],
data: Default::default(),
transaction_hash,
gas_info: Default::default(),
})
}
}
#[derive(Clone, Debug)]
pub struct DummyCommunicationChannel {
current_epoch: Arc<AtomicU64>,
aggregated_verification_key: VerificationKey,
}
impl DummyCommunicationChannel {
pub fn new(aggregated_verification_key: VerificationKey) -> Self {
DummyCommunicationChannel {
current_epoch: Arc::new(AtomicU64::new(1)),
aggregated_verification_key,
}
}
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 aggregated_verification_key(&self, _epoch_id: EpochId) -> Result<VerificationKey> {
Ok(self.aggregated_verification_key.clone())
}
}
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 deposit_tx_fixture(voucher_data: &BandwidthVoucherIssuanceData) -> TxResponse {
TxResponse {
hash: voucher_data.tx_hash(),
height: Default::default(),
index: 0,
tx_result: ExecTxResult {
code: Default::default(),
data: Default::default(),
log: "".to_string(),
info: "".to_string(),
gas_wanted: 0,
gas_used: 0,
events: vec![Event {
kind: format!("wasm-{}", DEPOSITED_FUNDS_EVENT_TYPE),
attributes: vec![
EventAttribute {
key: DEPOSIT_VALUE.to_string(),
value: voucher_data.value_plain(),
index: false,
},
EventAttribute {
key: DEPOSIT_INFO.to_string(),
value: CredentialType::Voucher.to_string(),
index: false,
},
EventAttribute {
key: DEPOSIT_IDENTITY_KEY.to_string(),
value: voucher_data.identity_key().public_key().to_base58_string(),
index: false,
},
EventAttribute {
key: DEPOSIT_ENCRYPTION_KEY.parse().unwrap(),
value: voucher_data
.encryption_key()
.public_key()
.to_base58_string(),
index: false,
},
],
}],
codespace: "".to_string(),
},
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<C: Into<Coin>>(
amount: C,
tx_hash: Option<String>,
) -> IssuanceBandwidthCredential {
let mut rng = OsRng;
let tx_hash = if let Some(provided) = &tx_hash {
provided.parse().unwrap()
} else {
Hash::from_str("6B27412050B823E58BB38447D7870BBC8CBE3C51C905BEA89D459ACCDA80A00E").unwrap()
};
let identity_keypair = identity::KeyPair::new(&mut rng);
let encryption_keypair = encryption::KeyPair::new(&mut rng);
let id_priv =
identity::PrivateKey::from_bytes(&identity_keypair.private_key().to_bytes()).unwrap();
let enc_priv =
encryption::PrivateKey::from_bytes(&encryption_keypair.private_key().to_bytes()).unwrap();
IssuanceBandwidthCredential::new_voucher(amount.into(), tx_hash, id_priv, enc_priv)
}
fn dummy_signature() -> identity::Signature {
"3vUCc6MCN5AC2LNgDYjRB1QeErZSN1S8f6K14JHjpUcKWXbjGYFExA8DbwQQBki9gyUqrpBF94Drttb4eMcGQXkp"
.parse()
.unwrap()
}
struct TestFixture {
rocket: Client,
storage: NymApiStorage,
chain_state: SharedFakeChain,
epoch: Arc<AtomicU64>,
_tmp_dir: TempDir,
}
impl TestFixture {
async fn new() -> Self {
let mut rng = crate::coconut::tests::fixtures::test_rng_07([69u8; 32]);
let params = Parameters::new(4).unwrap();
let coconut_keypair = nym_coconut::ttp_keygen(&params, 1, 1).unwrap().remove(0);
let identity = identity::KeyPair::new(&mut rng);
let epoch = Arc::new(AtomicU64::new(1));
let comm_channel =
DummyCommunicationChannel::new(coconut_keypair.verification_key().clone())
.with_epoch(epoch.clone());
// 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::coconut::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(
AccountId::from_str(TEST_REWARDING_VALIDATOR_ADDRESS).unwrap(),
chain_state.clone(),
);
let rocket = rocket::build().attach(crate::coconut::stage(
nyxd_client,
TEST_COIN_DENOM.to_string(),
identity,
staged_key_pair,
comm_channel,
storage.clone(),
));
TestFixture {
rocket: Client::tracked(rocket)
.await
.expect("valid rocket instance"),
storage,
chain_state,
epoch,
_tmp_dir: tmp_dir,
}
}
fn set_epoch(&self, epoch: u64) {
self.epoch.store(epoch, Ordering::Relaxed)
}
fn add_tx(&self, hash: Hash, tx: TxResponse) {
self.chain_state.lock().unwrap().txs.insert(hash, tx);
}
fn add_deposit_tx(&self, voucher: &BandwidthVoucherIssuanceData) {
let mut guard = self.chain_state.lock().unwrap();
let fixture = deposit_tx_fixture(voucher);
guard.txs.insert(voucher.tx_hash(), fixture);
}
async fn issue_dummy_credential(&self) {
let mut rng = OsRng;
let mut tx_hash = [0u8; 32];
rng.fill_bytes(&mut tx_hash);
let tx_hash = Hash::from_bytes(Algorithm::Sha256, &tx_hash).unwrap();
let voucher = voucher_fixture(coin(1234, "unym"), Some(tx_hash.to_string()));
let signing_data = voucher.prepare_for_signing();
let voucher_data = voucher.get_variant_data().voucher_data().unwrap();
let req = voucher_data.create_blind_sign_request_body(&signing_data);
self.add_deposit_tx(voucher_data);
self.issue_credential(req).await;
}
async fn issue_credential(&self, req: BlindSignRequestBody) -> BlindedSignatureResponse {
let response = self
.rocket
.post(format!(
"/{API_VERSION}/{COCONUT_ROUTES}/{BANDWIDTH}/{COCONUT_BLIND_SIGN}",
))
.json(&req)
.dispatch()
.await;
assert_eq!(response.status(), Status::Ok);
serde_json::from_str(&response.into_string().await.unwrap()).unwrap()
}
async fn issued_credential(&self, id: i64) -> Option<IssuedCredentialResponse> {
let response = self
.rocket
.get(format!(
"/{API_VERSION}/{COCONUT_ROUTES}/{BANDWIDTH}/issued-credential/{id}"
))
.dispatch()
.await;
assert_eq!(response.status(), Status::Ok);
serde_json::from_str(&response.into_string().await.unwrap()).unwrap()
}
async fn issued_unchecked(&self, id: i64) -> IssuedCredentialBody {
self.issued_credential(id)
.await
.unwrap()
.credential
.unwrap()
}
}
#[cfg(test)]
mod credential_tests {
use super::*;
use crate::coconut::tests::helpers::init_chain;
use nym_api_requests::coconut::{VerifyCredentialBody, VerifyCredentialResponse};
use nym_coconut::{blind_sign, hash_to_scalar, ttp_keygen};
use nym_coconut_bandwidth_contract_common::spend_credential::SpendCredential;
use nym_credentials::coconut::bandwidth::bandwidth_credential_params;
use nym_validator_client::nym_api::routes::COCONUT_VERIFY_BANDWIDTH_CREDENTIAL;
#[tokio::test]
async fn already_issued() {
let voucher = voucher_fixture(coin(1234, TEST_COIN_DENOM), None);
let signing_data = voucher.prepare_for_signing();
let voucher_data = voucher.get_variant_data().voucher_data().unwrap();
let request_body = voucher_data.create_blind_sign_request_body(&signing_data);
let tx_hash = request_body.tx_hash;
let tx_entry = tx_entry_fixture(tx_hash);
let test_fixture = TestFixture::new().await;
test_fixture.add_tx(tx_hash, tx_entry);
let sig = blinded_signature_fixture();
let commitments = request_body.encode_commitments();
let public = request_body.public_attributes_plain.clone();
test_fixture
.storage
.store_issued_credential(42, tx_hash, &sig, dummy_signature(), commitments, public)
.await
.unwrap();
let response = test_fixture
.rocket
.post(format!(
"/{}/{}/{}/{}",
API_VERSION, COCONUT_ROUTES, BANDWIDTH, COCONUT_BLIND_SIGN
))
.json(&request_body)
.dispatch()
.await;
assert_eq!(response.status(), Status::Ok);
let expected_response = BlindedSignatureResponse::new(sig);
// This is a more direct way, but there's a bug which makes it hang https://github.com/SergioBenitez/Rocket/issues/1893
// let blinded_signature_response = response
// .into_json::<BlindedSignatureResponse>()
// .await
// .unwrap();
let blinded_signature_response = serde_json::from_str::<BlindedSignatureResponse>(
&response.into_string().await.unwrap(),
)
.unwrap();
assert_eq!(
blinded_signature_response.to_bytes(),
expected_response.to_bytes()
);
}
#[tokio::test]
async fn state_functions() {
let mut rng = OsRng;
let identity = identity::KeyPair::new(&mut rng);
let nyxd_client = DummyClient::new(
AccountId::from_str(TEST_REWARDING_VALIDATOR_ADDRESS).unwrap(),
Default::default(),
);
let params = Parameters::new(4).unwrap();
let key_pair = ttp_keygen(&params, 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(key_pair.verification_key().clone());
let staged_key_pair = crate::coconut::KeyPair::new();
staged_key_pair
.set(KeyPairWithEpoch {
keys: key_pair,
issued_for_epoch: 1,
})
.await;
staged_key_pair.validate();
let state = State::new(
nyxd_client,
TEST_COIN_DENOM.to_string(),
identity,
staged_key_pair,
comm_channel,
storage.clone(),
);
let tx_hash = "6B27412050B823E58BB38447D7870BBC8CBE3C51C905BEA89D459ACCDA80A00E"
.parse()
.unwrap();
assert!(state.already_issued(tx_hash).await.unwrap().is_none());
let voucher = voucher_fixture(coin(1234, TEST_COIN_DENOM), None);
let signing_data = voucher.prepare_for_signing();
let voucher_data = voucher.get_variant_data().voucher_data().unwrap();
let request_body = voucher_data.create_blind_sign_request_body(&signing_data);
let commitments = request_body.encode_commitments();
let public = request_body.public_attributes_plain.clone();
let sig = blinded_signature_fixture();
storage
.store_issued_credential(
42,
tx_hash,
&sig,
dummy_signature(),
commitments.clone(),
public.clone(),
)
.await
.unwrap();
assert_eq!(
state
.already_issued(tx_hash)
.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_credential(
42,
tx_hash,
&blinded_signature,
dummy_signature(),
commitments.clone(),
public.clone(),
)
.await;
assert!(storage_err.is_err());
// And use a new hash to store a new signature
let tx_hash = "97D64C38D6601B1F0FD3A82E20D252685CB7A210AFB0261018590659AB82B0BF"
.parse()
.unwrap();
storage
.store_issued_credential(
42,
tx_hash,
&blinded_signature,
dummy_signature(),
commitments.clone(),
public.clone(),
)
.await
.unwrap();
// Check that the same value for tx_hash is returned
assert_eq!(
state
.already_issued(tx_hash)
.await
.unwrap()
.unwrap()
.to_bytes(),
blinded_signature.to_bytes()
);
}
#[tokio::test]
async fn blind_sign_correct() {
let tx_hash =
Hash::from_str("7C41AF8266D91DE55E1C8F4712E6A952A165ED3D8C27C7B00428CBD0DE00A52B")
.unwrap();
let params = Parameters::new(4).unwrap();
let mut rng = OsRng;
let nym_api_identity = identity::KeyPair::new(&mut rng);
let identity_keypair = identity::KeyPair::new(&mut rng);
let encryption_keypair = encryption::KeyPair::new(&mut rng);
let voucher = IssuanceBandwidthCredential::new_voucher(
coin(1234, "unym"),
tx_hash,
identity::PrivateKey::from_base58_string(
identity_keypair.private_key().to_base58_string(),
)
.unwrap(),
encryption::PrivateKey::from_bytes(&encryption_keypair.private_key().to_bytes())
.unwrap(),
);
let key_pair = ttp_keygen(&params, 1, 1).unwrap().remove(0);
let tmp_dir = tempdir().unwrap();
let storage = NymApiStorage::init(tmp_dir.path().join("storage.db"))
.await
.unwrap();
let chain = init_chain();
let voucher_data = voucher.get_variant_data().voucher_data().unwrap();
let tx_entry = deposit_tx_fixture(voucher_data);
chain.lock().unwrap().txs.insert(tx_hash, tx_entry.clone());
let nyxd_client = DummyClient::new(
AccountId::from_str(TEST_REWARDING_VALIDATOR_ADDRESS).unwrap(),
chain.clone(),
);
let comm_channel = DummyCommunicationChannel::new(key_pair.verification_key().clone());
let staged_key_pair = crate::coconut::KeyPair::new();
staged_key_pair
.set(KeyPairWithEpoch {
keys: key_pair,
issued_for_epoch: 1,
})
.await;
staged_key_pair.validate();
let rocket = rocket::build().attach(crate::coconut::stage(
nyxd_client,
TEST_COIN_DENOM.to_string(),
nym_api_identity,
staged_key_pair,
comm_channel,
storage.clone(),
));
let client = Client::tracked(rocket)
.await
.expect("valid rocket instance");
let signing_data = voucher.prepare_for_signing();
let voucher_data = voucher.get_variant_data().voucher_data().unwrap();
let request_body = voucher_data.create_blind_sign_request_body(&signing_data);
let response = client
.post(format!(
"/{}/{}/{}/{}",
API_VERSION, COCONUT_ROUTES, BANDWIDTH, COCONUT_BLIND_SIGN
))
.json(&request_body)
.dispatch()
.await;
assert_eq!(response.status(), Status::Ok);
// This is a more direct way, but there's a bug which makes it hang https://github.com/SergioBenitez/Rocket/issues/1893
// assert!(response.into_json::<BlindedSignatureResponse>().is_some());
let blinded_signature_response = serde_json::from_str::<BlindedSignatureResponse>(
&response.into_string().await.unwrap(),
);
assert!(blinded_signature_response.is_ok());
}
#[tokio::test]
async fn verification_of_bandwidth_credential() {
// Setup variables
let chain = init_chain();
let validator_address = AccountId::from_str(TEST_REWARDING_VALIDATOR_ADDRESS).unwrap();
chain
.lock()
.unwrap()
.add_member(validator_address.as_ref(), 100);
let nyxd_client = DummyClient::new(validator_address.clone(), chain.clone());
let db_dir = tempdir().unwrap();
// generate all the credential requests
let params = bandwidth_credential_params();
let key_pair = nym_coconut::keygen(params);
let epoch = 1;
let voucher_amount = coin(1234, "unym");
let issuance = voucher_fixture(coin(1234, "unym"), None);
let sig_req = issuance.prepare_for_signing();
let pub_attrs_hashed = sig_req
.public_attributes_plain
.iter()
.map(hash_to_scalar)
.collect::<Vec<_>>();
let pub_attrs = pub_attrs_hashed.iter().collect::<Vec<_>>();
let blind_sig = blind_sign(
params,
key_pair.secret_key(),
&sig_req.blind_sign_request,
&pub_attrs,
)
.unwrap();
let sig = blind_sig
.unblind(
key_pair.verification_key(),
&sig_req.pedersen_commitments_openings,
)
.unwrap();
let issued = issuance.into_issued_credential(sig, epoch);
let spending = issued
.prepare_for_spending(key_pair.verification_key())
.unwrap();
let storage1 = NymApiStorage::init(db_dir.path().join("storage.db"))
.await
.unwrap();
let comm_channel = DummyCommunicationChannel::new(key_pair.verification_key().clone());
let staged_key_pair = crate::coconut::KeyPair::new();
staged_key_pair
.set(KeyPairWithEpoch {
keys: key_pair,
issued_for_epoch: epoch,
})
.await;
staged_key_pair.validate();
let mut rng = OsRng;
let identity = identity::KeyPair::new(&mut rng);
let rocket = rocket::build().attach(crate::coconut::stage(
nyxd_client.clone(),
TEST_COIN_DENOM.to_string(),
identity,
staged_key_pair,
comm_channel.clone(),
storage1.clone(),
));
let client = Client::tracked(rocket)
.await
.expect("valid rocket instance");
let proposal_id = 42;
// The address is not used, so we can use a duplicate
let gateway_cosmos_addr = validator_address.clone();
let req =
VerifyCredentialBody::new(spending.clone(), proposal_id, gateway_cosmos_addr.clone());
// Test endpoint with not proposal for the proposal id
let response = client
.post(format!(
"/{}/{}/{}/{}",
API_VERSION, COCONUT_ROUTES, BANDWIDTH, COCONUT_VERIFY_BANDWIDTH_CREDENTIAL
))
.json(&req)
.dispatch()
.await;
assert_eq!(response.status(), Status::BadRequest);
assert_eq!(
response.into_string().await.unwrap(),
CoconutError::IncorrectProposal {
reason: "proposal not found".to_string()
}
.to_string()
);
let mut proposal = Proposal {
title: String::new(),
description: String::from("25mnnoCcUfeizfC85avvroFg2prpEZBgJbJM2SLtkgyyUkoAU3cqJiqWmg8cMHEPjfFf5sQF92SMAM2vbEoLZvUjenvXhadTLdA4TqMYArJpihyqirW2AhGoNehtcdcK5gnH"),
msgs: vec![],
status: cw3::Status::Open,
expires: cw_utils::Expiration::Never {},
threshold: cw_utils::Threshold::AbsolutePercentage {
percentage: Decimal::from_ratio(2u32, 3u32),
},
total_weight: chain.lock().unwrap().total_group_weight(),
votes: Votes::yes(0),
proposer: Addr::unchecked("proposer"),
deposit: None,
start_height: 0
};
// Test the endpoint with a different blinded serial number in the description
chain
.lock()
.unwrap()
.multisig_contract
.proposals
.insert(proposal_id, proposal.clone());
let response = client
.post(format!(
"/{}/{}/{}/{}",
API_VERSION, COCONUT_ROUTES, BANDWIDTH, COCONUT_VERIFY_BANDWIDTH_CREDENTIAL
))
.json(&req)
.dispatch()
.await;
assert_eq!(response.status(), Status::BadRequest);
assert_eq!(
response.into_string().await.unwrap(),
CoconutError::IncorrectProposal {
reason: "incorrect blinded serial number in description".to_string()
}
.to_string()
);
// Test the endpoint with no msg in the proposal action
proposal.description = spending
.verify_credential_request
.blinded_serial_number_bs58();
chain
.lock()
.unwrap()
.multisig_contract
.proposals
.insert(proposal_id, proposal.clone());
let response = client
.post(format!(
"/{}/{}/{}/{}",
API_VERSION, COCONUT_ROUTES, BANDWIDTH, COCONUT_VERIFY_BANDWIDTH_CREDENTIAL
))
.json(&req)
.dispatch()
.await;
assert_eq!(response.status(), Status::BadRequest);
assert_eq!(
response.into_string().await.unwrap(),
CoconutError::IncorrectProposal {
reason: "action is not to release funds".to_string()
}
.to_string()
);
// Test the endpoint without any credential recorded in the Coconut Bandwidth Contract
let funds = voucher_amount.clone();
let msg = nym_coconut_bandwidth_contract_common::msg::ExecuteMsg::ReleaseFunds {
funds: funds.clone(),
};
let cosmos_msg = CosmosMsg::Wasm(WasmMsg::Execute {
contract_addr: String::new(),
msg: to_binary(&msg).unwrap(),
funds: vec![],
});
proposal.msgs = vec![cosmos_msg];
chain
.lock()
.unwrap()
.multisig_contract
.proposals
.insert(proposal_id, proposal.clone());
let response = client
.post(format!(
"/{}/{}/{}/{}",
API_VERSION, COCONUT_ROUTES, BANDWIDTH, COCONUT_VERIFY_BANDWIDTH_CREDENTIAL
))
.json(&req)
.dispatch()
.await;
assert_eq!(response.status(), Status::BadRequest);
assert_eq!(
response.into_string().await.unwrap(),
CoconutError::InvalidCredentialStatus {
status: "spent credential not found".to_string()
}
.to_string()
);
chain
.lock()
.unwrap()
.bandwidth_contract
.spent_credentials
.insert(
spending
.verify_credential_request
.blinded_serial_number_bs58(),
SpendCredentialResponse::new(None),
);
let response = client
.post(format!(
"/{}/{}/{}/{}",
API_VERSION, COCONUT_ROUTES, BANDWIDTH, COCONUT_VERIFY_BANDWIDTH_CREDENTIAL
))
.json(&req)
.dispatch()
.await;
assert_eq!(response.status(), Status::BadRequest);
assert_eq!(
response.into_string().await.unwrap(),
CoconutError::InvalidCredentialStatus {
status: "Inexistent".to_string()
}
.to_string()
);
// Test the endpoint with a credential that doesn't verify correctly
let mut spent_credential = SpendCredential::new(
funds.clone(),
spending
.verify_credential_request
.blinded_serial_number_bs58(),
Addr::unchecked("unimportant"),
);
chain
.lock()
.unwrap()
.bandwidth_contract
.spent_credentials
.insert(
spending
.verify_credential_request
.blinded_serial_number_bs58(),
SpendCredentialResponse::new(Some(spent_credential.clone())),
);
// TODO: somehow restore that test
// let bad_credential = Credential::new(
// 4,
// theta.clone(),
// voucher_value,
// String::from("bad voucher info"),
// 0,
// );
// let bad_req = VerifyCredentialBody::new(
// bad_credential,
// epoch_id,
// proposal_id,
// gateway_cosmos_addr.clone(),
// );
// let response = client
// .post(format!(
// "/{}/{}/{}/{}",
// API_VERSION, COCONUT_ROUTES, BANDWIDTH, COCONUT_VERIFY_BANDWIDTH_CREDENTIAL
// ))
// .json(&bad_req)
// .dispatch()
// .await;
// assert_eq!(response.status(), Status::Ok);
// let verify_credential_response = serde_json::from_str::<VerifyCredentialResponse>(
// &response.into_string().await.unwrap(),
// )
// .unwrap();
// assert!(!verify_credential_response.verification_result);
// assert_eq!(
// cw3::Status::Rejected,
// chain
// .lock()
// .unwrap()
// .multisig_contract
// .proposals
// .get(&proposal_id)
// .unwrap()
// .status
// );
// Test the endpoint with a proposal that has a different value for the funds to be released
// then what's in the credential
let funds = Coin::new(voucher_amount.amount.u128() + 10, TEST_COIN_DENOM);
let msg = nym_coconut_bandwidth_contract_common::msg::ExecuteMsg::ReleaseFunds {
funds: funds.clone().into(),
};
let cosmos_msg = CosmosMsg::Wasm(WasmMsg::Execute {
contract_addr: String::new(),
msg: to_binary(&msg).unwrap(),
funds: vec![],
});
proposal.msgs = vec![cosmos_msg];
chain.lock().unwrap().reset_votes();
chain
.lock()
.unwrap()
.multisig_contract
.proposals
.insert(proposal_id, proposal.clone());
let response = client
.post(format!(
"/{}/{}/{}/{}",
API_VERSION, COCONUT_ROUTES, BANDWIDTH, COCONUT_VERIFY_BANDWIDTH_CREDENTIAL
))
.json(&req)
.dispatch()
.await;
assert_eq!(response.status(), Status::Ok);
let verify_credential_response = serde_json::from_str::<VerifyCredentialResponse>(
&response.into_string().await.unwrap(),
)
.unwrap();
assert!(!verify_credential_response.verification_result);
assert_eq!(
cw3::Status::Rejected,
chain
.lock()
.unwrap()
.multisig_contract
.proposals
.get(&proposal_id)
.unwrap()
.status
);
// Test the endpoint with every dependency met
let funds = voucher_amount;
let msg = nym_coconut_bandwidth_contract_common::msg::ExecuteMsg::ReleaseFunds {
funds: funds.clone(),
};
let cosmos_msg = CosmosMsg::Wasm(WasmMsg::Execute {
contract_addr: String::new(),
msg: to_binary(&msg).unwrap(),
funds: vec![],
});
proposal.msgs = vec![cosmos_msg];
chain.lock().unwrap().reset_votes();
chain
.lock()
.unwrap()
.multisig_contract
.proposals
.insert(proposal_id, proposal.clone());
let response = client
.post(format!(
"/{}/{}/{}/{}",
API_VERSION, COCONUT_ROUTES, BANDWIDTH, COCONUT_VERIFY_BANDWIDTH_CREDENTIAL
))
.json(&req)
.dispatch()
.await;
assert_eq!(response.status(), Status::Ok);
let verify_credential_response = serde_json::from_str::<VerifyCredentialResponse>(
&response.into_string().await.unwrap(),
)
.unwrap();
assert!(verify_credential_response.verification_result);
assert_eq!(
cw3::Status::Passed,
chain
.lock()
.unwrap()
.multisig_contract
.proposals
.get(&proposal_id)
.unwrap()
.status
);
// Test the endpoint with the credential marked as Spent in the Coconut Bandwidth Contract
spent_credential.mark_as_spent();
chain
.lock()
.unwrap()
.bandwidth_contract
.spent_credentials
.insert(
spending
.verify_credential_request
.blinded_serial_number_bs58(),
SpendCredentialResponse::new(Some(spent_credential)),
);
let response = client
.post(format!(
"/{}/{}/{}/{}",
API_VERSION, COCONUT_ROUTES, BANDWIDTH, COCONUT_VERIFY_BANDWIDTH_CREDENTIAL
))
.json(&req)
.dispatch()
.await;
assert_eq!(response.status(), Status::BadRequest);
assert_eq!(
response.into_string().await.unwrap(),
CoconutError::InvalidCredentialStatus {
status: "Spent".to_string()
}
.to_string()
);
}
}