diff --git a/nym-api/nym-api-requests/Cargo.toml b/nym-api/nym-api-requests/Cargo.toml index c2000de824..6650381456 100644 --- a/nym-api/nym-api-requests/Cargo.toml +++ b/nym-api/nym-api-requests/Cargo.toml @@ -15,8 +15,7 @@ schemars = { workspace = true, features = ["preserve_order"] } serde = { workspace = true, features = ["derive"] } ts-rs = { workspace = true, optional = true } tendermint = { workspace = true } -time = { workspace = true, features = ["serde", "parsing", "formatting"] } - +time = { workspace = true } # for serde on secp256k1 signatures ecdsa = { version = "0.16", features = ["serde"] } diff --git a/nym-api/nym-api-requests/src/coconut/mod.rs b/nym-api/nym-api-requests/src/coconut/mod.rs index c6a3701f45..8e9f7a8a4f 100644 --- a/nym-api/nym-api-requests/src/coconut/mod.rs +++ b/nym-api/nym-api-requests/src/coconut/mod.rs @@ -7,4 +7,5 @@ pub mod models; pub use models::{ BlindSignRequestBody, BlindedSignatureResponse, CredentialsRequestBody, FreePassRequest, PartialCoinIndicesSignatureResponse, PartialExpirationDateSignatureResponse, + VerificationKeyResponse, VerifyCredentialResponse, VerifyEcashCredentialBody, }; diff --git a/nym-api/nym-api-requests/src/coconut/models.rs b/nym-api/nym-api-requests/src/coconut/models.rs index 40e0fd4e57..4440dce47d 100644 --- a/nym-api/nym-api-requests/src/coconut/models.rs +++ b/nym-api/nym-api-requests/src/coconut/models.rs @@ -4,18 +4,20 @@ use crate::coconut::helpers::issued_credential_plaintext; use cosmrs::AccountId; use nym_credentials_interface::{ - hash_to_scalar, Attribute, BlindSignRequest, BlindedSignature, Bytable, CoconutError, - CredentialSpendingData, VerificationKey, + BlindedSignature, CompactEcashError, CredentialSpendingData, OldCredentialSpendingData, + PartialCoinIndexSignature, PartialExpirationDateSignature, PublicKeyUser, VerificationKeyAuth, + WithdrawalRequest, }; use nym_crypto::asymmetric::identity; use serde::{Deserialize, Serialize}; -use std::collections::BTreeMap; +use std::{collections::BTreeMap, fmt::Display}; use tendermint::hash::Hash; +use time::OffsetDateTime; #[derive(Serialize, Deserialize)] pub struct VerifyCredentialBody { /// The cryptographic material required for spending the underlying credential. - pub credential_data: CredentialSpendingData, + pub credential_data: OldCredentialSpendingData, /// Multisig proposal for releasing funds for the provided bandwidth credential pub proposal_id: u64, @@ -26,7 +28,7 @@ pub struct VerifyCredentialBody { impl VerifyCredentialBody { pub fn new( - credential_data: CredentialSpendingData, + credential_data: OldCredentialSpendingData, proposal_id: u64, gateway_cosmos_addr: AccountId, ) -> VerifyCredentialBody { @@ -51,6 +53,64 @@ impl VerifyCredentialResponse { } } +#[derive(Serialize, Deserialize, Clone)] +pub struct VerifyEcashCredentialBody { + /// The cryptographic material required for spending the underlying credential. + pub credential: CredentialSpendingData, + + /// Cosmos address of the sender of the credential + pub gateway_cosmos_addr: AccountId, + + /// Multisig proposal for releasing funds for the provided bandwidth credential, None for freepasses + pub proposal_id: Option, +} + +impl VerifyEcashCredentialBody { + pub fn new( + credential: CredentialSpendingData, + gateway_cosmos_addr: AccountId, + proposal_id: Option, + ) -> VerifyEcashCredentialBody { + VerifyEcashCredentialBody { + credential, + gateway_cosmos_addr, + proposal_id, + } + } +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] +pub enum VerifyEcashCredentialResponse { + InvalidFormat(String), + DoubleSpend, + AlreadySent, + SubmittedTooLate(u64, u64), + Refused, + Accepted, +} + +impl Display for VerifyEcashCredentialResponse { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::InvalidFormat(reason) => { + write!(f, "Invalid format : {:?}", reason) + } + Self::DoubleSpend => write!(f, "Credential was already spent"), + Self::AlreadySent => write!(f, "This credential was already sent"), + Self::SubmittedTooLate(expected, actual) => { + write!( + f, + "Credential spent too late. Accepted from {:#?}, spent on {:#?}", + OffsetDateTime::from_unix_timestamp(*expected as i64), + OffsetDateTime::from_unix_timestamp(*actual as i64) + ) + } + Self::Refused => write!(f, "Credential failed to validate"), + Self::Accepted => write!(f, "Credential was accepted"), + } + } +} + // All strings are base58 encoded representations of structs #[derive(Clone, Serialize, Deserialize, Debug)] pub struct BlindSignRequestBody { diff --git a/nym-api/src/coconut/api_routes/mod.rs b/nym-api/src/coconut/api_routes/mod.rs index 99950f914e..a74b6f82ba 100644 --- a/nym-api/src/coconut/api_routes/mod.rs +++ b/nym-api/src/coconut/api_routes/mod.rs @@ -7,9 +7,10 @@ use crate::coconut::helpers::{accepted_vote_err, blind_sign}; use crate::coconut::state::State; use crate::coconut::storage::CoconutStorageExt; use k256::ecdsa::signature::Verifier; +use nym_api_requests::coconut::models::SpentCredentialsResponse; use nym_api_requests::coconut::models::{ CredentialsRequestBody, EpochCredentialsResponse, FreePassNonceResponse, FreePassRequest, - IssuedCredentialResponse, IssuedCredentialsResponse, + IssuedCredentialResponse, IssuedCredentialsResponse, VerifyEcashCredentialResponse, }; use nym_api_requests::coconut::{ BlindSignRequestBody, BlindedSignatureResponse, PartialCoinIndicesSignatureResponse, @@ -17,16 +18,18 @@ use nym_api_requests::coconut::{ }; use nym_coconut_dkg_common::types::EpochId; use nym_compact_ecash::error::CompactEcashError; +use nym_compact_ecash::identify::IdentifyResult; +use nym_credentials::coconut::bandwidth::bandwidth_credential_params; use nym_credentials::coconut::utils::{ cred_exp_date_timestamp, freepass_exp_date_timestamp, today_timestamp, }; -use nym_validator_client::nyxd::Coin; +use nym_ecash_contract_common::spend_credential::check_proposal; use rand::rngs::OsRng; use rand::RngCore; use rocket::serde::json::Json; use rocket::State as RocketState; use std::ops::Deref; -use time::OffsetDateTime; +use time::{Duration, OffsetDateTime}; mod helpers; @@ -221,70 +224,227 @@ pub async fn post_blind_sign( Ok(Json(BlindedSignatureResponse { blinded_signature })) } -#[post("/verify-bandwidth-credential", data = "")] -pub async fn verify_bandwidth_credential( - verify_credential_body: Json, +#[post("/verify-online-credential", data = "")] +pub async fn verify_online_credential( + verify_credential_body: Json, state: &RocketState, -) -> Result> { +) -> Result> { let proposal_id = verify_credential_body.proposal_id; - let credential_data = &verify_credential_body.credential_data; - let epoch_id = credential_data.epoch_id; - let theta = &credential_data.verify_credential_request; + let credential_data = &verify_credential_body.credential; + let payment = &credential_data.payment; + let today_date = today_timestamp(); - let voucher_value: u64 = if credential_data.typ.is_voucher() { - credential_data - .get_bandwidth_attribute() - .ok_or(CoconutError::MissingBandwidthValue)? - .parse() - .map_err(|source| CoconutError::VoucherValueParsingFailure { source })? - } else { - return Err(CoconutError::NotABandwidthVoucher { - typ: credential_data.typ, - }); - }; - - // TODO: introduce a check to make sure we haven't already voted for this proposal to prevent DDOS - - let proposal = state.client.get_proposal(proposal_id).await?; - - // Proposal description is the blinded serial number - if !theta.has_blinded_serial_number(&proposal.description)? { - return Err(CoconutError::IncorrectProposal { - reason: String::from("incorrect blinded serial number in description"), - }); + //tickets needs a proposal + if proposal_id.is_none() && credential_data.typ.is_ticketbook() { + return Ok(Json(VerifyEcashCredentialResponse::InvalidFormat( + "No proposal for a ticketbook".to_string(), + ))); } - let proposed_release_funds = - funds_from_cosmos_msgs(proposal.msgs).ok_or(CoconutError::IncorrectProposal { - reason: String::from("action is not to release funds"), - })?; - // Credential has not been spent before, and is on its way of being spent - let credential_status = state + //there should be no proposal on a freepass + if proposal_id.is_some() && credential_data.typ.is_free_pass() { + return Ok(Json(VerifyEcashCredentialResponse::InvalidFormat( + "Proposal present for a freepass".to_string(), + ))); + } + + if today_date != credential_data.spend_date { + state.refuse_proposal_online(proposal_id).await; + return Ok(Json(VerifyEcashCredentialResponse::SubmittedTooLate( + today_date, + credential_data.spend_date, + ))); + } + + //actual double spend detection with storage + if let Some(previous_payment) = state + .get_credential_by_sn(credential_data.serial_number_b58()) + .await? + { + match nym_compact_ecash::identify::identify( + credential_data.payment.clone(), + previous_payment.payment, + credential_data.pay_info.clone(), + previous_payment.pay_info, + ) { + IdentifyResult::NotADuplicatePayment => {} //SW NOTE This should never happen, quick message? + IdentifyResult::DuplicatePayInfo(_) => { + log::warn!("Identical payInfo"); + state.refuse_proposal_online(proposal_id).await; + return Ok(Json(VerifyEcashCredentialResponse::AlreadySent)); + } + IdentifyResult::DoubleSpendingPublicKeys(pub_key) => { + //Actual double spending + log::warn!( + "Double spending attempt for key {}", + pub_key.to_base58_string() + ); + state.refuse_proposal_online(proposal_id).await; + if credential_data.typ.is_ticketbook() { + state.blacklist(pub_key.to_base58_string()).await; + } + return Ok(Json(VerifyEcashCredentialResponse::DoubleSpend)); + } + } + } + //Double spend check with contract + if let Some(spent_credential) = state .client - .get_spent_credential(theta.blinded_serial_number_bs58()) + .get_spent_credential(payment.serial_number_bs58()) .await? .spend_credential - .ok_or(CoconutError::InvalidCredentialStatus { - status: String::from("Inexistent"), - })? - .status(); - if credential_status != SpendCredentialStatus::InProgress { - return Err(CoconutError::InvalidCredentialStatus { - status: format!("{:?}", credential_status), - }); + { + if spent_credential.serial_number() == credential_data.serial_number_b58() { + state.refuse_proposal_online(proposal_id).await; + return Ok(Json(VerifyEcashCredentialResponse::DoubleSpend)); + } } + + let verification_key = state.verification_key(credential_data.epoch_id).await?; + let params = bandwidth_credential_params(); + + if credential_data.verify(params, &verification_key).is_err() { + state.refuse_proposal_online(proposal_id).await; + return Ok(Json(VerifyEcashCredentialResponse::Refused)); + } + + // TODO: introduce a check to make sure we haven't already voted for this proposal to prevent DDOS + if let Some(id) = proposal_id { + let proposal = state.client.get_proposal(id).await?; + + // Proposal description is the blinded serial number + if !payment.has_serial_number(&proposal.description)? { + state.client.vote_proposal(id, false, None).await?; + return Ok(Json(VerifyEcashCredentialResponse::InvalidFormat( + String::from("incorrect blinded serial number in description"), + ))); + } + if !check_proposal(proposal.msgs) { + state.client.vote_proposal(id, false, None).await?; + return Ok(Json(VerifyEcashCredentialResponse::InvalidFormat( + String::from("action is not to spend_credential"), + ))); + } + + // Vote yes or no on the proposal based on the verification result + let ret = state.client.vote_proposal(id, true, None).await; + accepted_vote_err(ret)?; + } + //From here, credential is considered spent + + //add to bloom filter for fast dup detection + state + .add_spent_credentials(&credential_data.serial_number_b58()) + .await; + //store credential + //don't store free pass, as they do not incur rewards + if !credential_data.typ.is_free_pass() { + state + .store_credential( + &verify_credential_body.credential, + &verify_credential_body.gateway_cosmos_addr, + proposal_id.unwrap(), //safety : It's not a freepass, and we checked before that it was not none + ) + .await?; + } + + Ok(Json(VerifyEcashCredentialResponse::Accepted)) +} + +#[post("/verify-offline-credential", data = "")] +pub async fn verify_offline_credential( + verify_credential_body: Json, + state: &RocketState, +) -> Result> { + let credential_data = &verify_credential_body.credential; + let proposal_id = verify_credential_body.proposal_id; + //tickets needs a proposal + if proposal_id.is_none() && credential_data.typ.is_ticketbook() { + return Ok(Json(VerifyEcashCredentialResponse::InvalidFormat( + "No proposal for a ticketbook".to_string(), + ))); + } + //there should be no proposal on a freepass + if proposal_id.is_some() && credential_data.typ.is_free_pass() { + return Ok(Json(VerifyEcashCredentialResponse::InvalidFormat( + "Proposal present for a freepass".to_string(), + ))); + } + //SW NOTE: Offline scheme, but we still need some check on that, so that client and gateway can't collude and send expired credentials. + //Let's allow the current day (obviously), and the day before (for late sender or around midnight) + let today_date = today_timestamp(); + let yesterday_date = today_date - Duration::DAY.whole_seconds() as u64; + if today_date != credential_data.spend_date && yesterday_date != credential_data.spend_date { + state.refuse_proposal(proposal_id).await; + return Ok(Json(VerifyEcashCredentialResponse::SubmittedTooLate( + yesterday_date, + credential_data.spend_date, + ))); + } + + //actual double spend detection with storage + if let Some(previous_payment) = state + .get_credential_by_sn(credential_data.serial_number_b58()) + .await? + { + match nym_compact_ecash::identify::identify( + credential_data.payment.clone(), + previous_payment.payment, + credential_data.pay_info.clone(), + previous_payment.pay_info, + ) { + IdentifyResult::NotADuplicatePayment => {} //SW NOTE This should never happen, quick message? + IdentifyResult::DuplicatePayInfo(_) => { + log::warn!("Identical payInfo"); + state.refuse_proposal(proposal_id).await; + return Ok(Json(VerifyEcashCredentialResponse::AlreadySent)); + } + IdentifyResult::DoubleSpendingPublicKeys(pub_key) => { + //Actual double spending + log::warn!( + "Double spending attempt for key {}", + pub_key.to_base58_string() + ); + state.refuse_proposal(proposal_id).await; + if credential_data.typ.is_ticketbook() { + state.blacklist(pub_key.to_base58_string()).await; + } + return Ok(Json(VerifyEcashCredentialResponse::DoubleSpend)); + } + } + } + + let epoch_id = credential_data.epoch_id; let verification_key = state.verification_key(epoch_id).await?; let params = bandwidth_credential_params(); - let mut vote_yes = credential_data.verify(params, &verification_key); - vote_yes &= Coin::from(proposed_release_funds) - == Coin::new(voucher_value as u128, state.mix_denom.clone()); + if credential_data.verify(params, &verification_key).is_err() { + state.refuse_proposal(proposal_id).await; + return Ok(Json(VerifyEcashCredentialResponse::Refused)); + } - // Vote yes or no on the proposal based on the verification result - let ret = state - .client - .vote_proposal(proposal_id, vote_yes, None) + //add to bloom filter for fast dup detection + state + .add_spent_credentials(&credential_data.serial_number_b58()) .await; - accepted_vote_err(ret)?; + + //store credential + //don't store free pass, as they do not incur rewards + if !credential_data.typ.is_free_pass() { + state + .store_credential( + &verify_credential_body.credential, + &verify_credential_body.gateway_cosmos_addr, + proposal_id.unwrap(), //safety : It's not a freepass, and we checked before that it was not none + ) + .await?; + } + + state + .accept_and_execute_proposal(proposal_id, credential_data.serial_number_b58()) + .await?; + + Ok(Json(VerifyEcashCredentialResponse::Accepted)) +} #[get("/spent-credentials")] pub async fn spent_credentials( diff --git a/nym-api/src/coconut/client.rs b/nym-api/src/coconut/client.rs index 931b438b77..60b874376d 100644 --- a/nym-api/src/coconut/client.rs +++ b/nym-api/src/coconut/client.rs @@ -4,7 +4,6 @@ use crate::coconut::error::Result; use cw3::{ProposalResponse, VoteResponse}; use cw4::MemberResponse; -use nym_coconut_bandwidth_contract_common::spend_credential::SpendCredentialResponse; use nym_coconut_dkg_common::dealer::{ DealerDetails, DealerDetailsResponse, RegisteredDealerDetails, }; @@ -20,6 +19,7 @@ use nym_coconut_dkg_common::verification_key::{ContractVKShare, VerificationKeyS use nym_contracts_common::IdentityKey; use nym_dkg::Threshold; use nym_ecash_contract_common::blacklist::BlacklistedAccountResponse; +use nym_ecash_contract_common::spend_credential::EcashSpentCredentialResponse; use nym_validator_client::nyxd::cosmwasm_client::types::ExecuteResult; use nym_validator_client::nyxd::{AccountId, Fee, Hash, TxResponse}; @@ -42,7 +42,9 @@ pub trait Client { async fn get_spent_credential( &self, blinded_serial_number: String, - ) -> Result; + ) -> Result; + + async fn propose_for_blacklist(&self, public_key: String) -> Result; async fn get_blacklisted_account( &self, public_key: String, diff --git a/nym-api/src/coconut/error.rs b/nym-api/src/coconut/error.rs index 3c99c1eb63..d6f7a4d71f 100644 --- a/nym-api/src/coconut/error.rs +++ b/nym-api/src/coconut/error.rs @@ -82,8 +82,8 @@ pub enum CoconutError { )] TooLongFreePass { expiry_date: OffsetDateTime }, - #[error("the provided free pass expiry is set in the past!")] - FreePassExpiryInThePast { expiry_date: OffsetDateTime }, + #[error("the spend date provided does not correspnd to today's date. Expected : {expected}, got {got}")] + InvalidSpendDate { expected: u64, got: u64 }, #[error("the received bandwidth voucher did not contain deposit value")] MissingBandwidthValue, diff --git a/nym-api/src/coconut/helpers.rs b/nym-api/src/coconut/helpers.rs index 47ed2fa3b6..ce116aa3e7 100644 --- a/nym-api/src/coconut/helpers.rs +++ b/nym-api/src/coconut/helpers.rs @@ -13,6 +13,8 @@ use nym_compact_ecash::scheme::keygen::SecretKeyAuth; use nym_compact_ecash::setup::{sign_coin_indices, CoinIndexSignature, Parameters}; use nym_compact_ecash::utils::BlindedSignature; use nym_compact_ecash::{PublicKeyUser, VerificationKeyAuth, WithdrawalRequest}; +use nym_ecash_contract_common::events::BLACKLIST_PROPOSAL_ID; +use nym_validator_client::nyxd::cosmwasm_client::logs::Log; use nym_validator_client::nyxd::error::NyxdError::AbciError; use tokio::sync::RwLock; @@ -182,4 +184,14 @@ impl ExpirationDateSignatureCache { signatures.clone().unwrap() // Either we or someone else update the signatures, so they must be there } } + +/// Search for the proposal id in the given log. It'll be in the LAST wasm event, with attribute key "proposal_id" +pub fn find_proposal_id(logs: &[Log]) -> Option<&cosmwasm_std::Attribute> { + logs.iter() + .rev() + .flat_map(|log| log.events.iter()) + .find(|event| event.ty == "wasm")? + .attributes + .iter() + .find(|attr| attr.key == BLACKLIST_PROPOSAL_ID) } diff --git a/nym-api/src/coconut/mod.rs b/nym-api/src/coconut/mod.rs index 8db10d2022..4177327215 100644 --- a/nym-api/src/coconut/mod.rs +++ b/nym-api/src/coconut/mod.rs @@ -47,7 +47,12 @@ where api_routes::get_current_free_pass_nonce, api_routes::post_free_pass, api_routes::post_blind_sign, - api_routes::verify_bandwidth_credential, + api_routes::verify_online_credential, + api_routes::verify_offline_credential, + api_routes::expiration_date_signatures, + api_routes::expiration_date_signatures_timestamp, + api_routes::coin_indices_signatures, + api_routes::spent_credentials, api_routes::epoch_credentials, api_routes::issued_credential, api_routes::issued_credentials, diff --git a/nym-api/src/coconut/state.rs b/nym-api/src/coconut/state.rs index 1daf39c49d..d5cf8244f2 100644 --- a/nym-api/src/coconut/state.rs +++ b/nym-api/src/coconut/state.rs @@ -1,13 +1,15 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::coconut::client::Client as LocalClient; use crate::coconut::comm::APICommunicationChannel; use crate::coconut::deposit::validate_deposit_tx; use crate::coconut::error::Result; use crate::coconut::keys::KeyPair; use crate::coconut::storage::CoconutStorageExt; +use crate::coconut::{client::Client as LocalClient, helpers::find_proposal_id}; use crate::support::storage::NymApiStorage; +use bloomfilter::Bloom; +use cw3::Status; use nym_api_requests::coconut::helpers::issued_credential_plaintext; use nym_api_requests::coconut::BlindSignRequestBody; use nym_compact_ecash::{ @@ -17,6 +19,10 @@ use nym_compact_ecash::{ utils::BlindedSignature, VerificationKeyAuth, }; +use nym_config::defaults::{BLOOM_BITMAP_SIZE, BLOOM_NUM_HASHES, BLOOM_SIP_KEYS}; +use nym_credentials::{coconut::utils::cred_exp_date_timestamp, CredentialSpendingData}; +use nym_ecash_contract_common::spend_credential::check_proposal; + use super::{ error::CoconutError, helpers::{accepted_vote_err, CoinIndexSignatureCache, ExpirationDateSignatureCache}, @@ -107,6 +113,103 @@ impl State { validate_deposit_tx(request, tx).await } + pub(crate) async fn accept_and_execute_proposal( + &self, + proposal_id: Option, + serial_number_bs58: String, + ) -> Result<()> { + if let Some(id) = proposal_id { + let proposal = self.client.get_proposal(id).await?; + + // Proposal description is the blinded serial number + if serial_number_bs58 != proposal.description { + return Err(CoconutError::IncorrectProposal { + reason: String::from("incorrect blinded serial number in description"), + }); + } + if !check_proposal(proposal.msgs) { + return Err(CoconutError::IncorrectProposal { + reason: String::from("action is not to spend_credential"), + }); + } + let client = self.client.clone(); + tokio::spawn(async move { + let ret = client.vote_proposal(id, true, None).await; + //SW NOTE: What to do if this fails + if let Err(e) = accepted_vote_err(ret) { + log::debug!("Failed to vote on proposal {} - {:?}", id, e); + } + + if let Ok(proposal) = client.get_proposal(id).await { + if proposal.status == Status::Passed { + //SW NOTE: What to do if this fails + if let Err(e) = client.execute_proposal(id).await { + log::debug!("Failed to execute proposal {} - {:?}", id, e); + } + } + } + }); + } + Ok(()) + } + + pub(crate) async fn refuse_proposal(&self, proposal_id: Option) { + if let Some(id) = proposal_id { + let client = self.client.clone(); + tokio::spawn(async move { + //whatever is in the proposal, we can refuse it anyway + if let Err(e) = client.vote_proposal(id, false, None).await { + log::debug!("Failed to refuse proposal {:?} - {:?}", id, e) + } + }); + } + } + + pub(crate) async fn refuse_proposal_online(&self, proposal_id: Option) { + if let Some(id) = proposal_id { + //whatever is in the proposal, we can refuse it anyway + if let Err(e) = self.client.vote_proposal(id, false, None).await { + log::debug!("Failed to refuse proposal {:?} - {:?}", id, e) + } + } + } + + pub(crate) async fn blacklist(&self, public_key: String) { + let client = self.client.clone(); + tokio::spawn(async move { + //SW TODO error handling with one log at the end + let response = client.propose_for_blacklist(public_key.clone()).await?; + let proposal_id = find_proposal_id(&response.logs) + .ok_or(CoconutError::ProposalIdError { + reason: "Proposal_ID not found".to_string(), + })? + .value + .parse::() + .map_err(|_| CoconutError::ProposalIdError { + reason: String::from("proposal id could not be parsed to u64"), + })?; + + let proposal = client.get_proposal(proposal_id).await?; + if proposal.status == Status::Open { + if public_key != proposal.description { + return Err(CoconutError::IncorrectProposal { + reason: String::from("incorrect publickey in description"), + }); + } + let ret = client.vote_proposal(proposal_id, true, None).await; + + accepted_vote_err(ret)?; + + if let Ok(proposal) = client.get_proposal(proposal_id).await { + if proposal.status == Status::Passed { + client.execute_proposal(proposal_id).await? + } + } + } + Ok(()) + }); + } + pub(crate) async fn sign_and_store_credential( &self, current_epoch: EpochId, @@ -164,7 +267,7 @@ impl State { Ok(()) } - pub async fn verification_key(&self, epoch_id: EpochId) -> Result { + pub async fn verification_key(&self, epoch_id: EpochId) -> Result { self.comm_channel .aggregated_verification_key(epoch_id) .await diff --git a/nym-api/src/support/nyxd/mod.rs b/nym-api/src/support/nyxd/mod.rs index 337a94d30b..65298fed7c 100644 --- a/nym-api/src/support/nyxd/mod.rs +++ b/nym-api/src/support/nyxd/mod.rs @@ -8,7 +8,6 @@ use anyhow::Result; use async_trait::async_trait; use cw3::{ProposalResponse, VoteResponse}; use cw4::MemberResponse; -use nym_coconut_bandwidth_contract_common::spend_credential::SpendCredentialResponse; use nym_coconut_dkg_common::dealing::{ DealerDealingsStatusResponse, DealingChunkInfo, DealingMetadata, DealingStatusResponse, PartialContractDealing, @@ -24,6 +23,7 @@ use nym_config::defaults::{ChainDetails, NymNetworkDetails}; use nym_coconut_dkg_common::dealer::RegisteredDealerDetails; use nym_ecash_contract_common::blacklist::BlacklistedAccountResponse; +use nym_ecash_contract_common::spend_credential::EcashSpentCredentialResponse; use nym_mixnet_contract_common::families::FamilyHead; use nym_mixnet_contract_common::mixnode::MixNodeDetails; use nym_mixnet_contract_common::reward_params::RewardingParams; @@ -37,7 +37,7 @@ use nym_validator_client::nyxd::contract_traits::{NameServiceQueryClient, PagedD use nym_validator_client::nyxd::error::NyxdError; use nym_validator_client::nyxd::{ contract_traits::{ - CoconutBandwidthQueryClient, DkgQueryClient, DkgSigningClient, GroupQueryClient, + DkgQueryClient, DkgSigningClient, EcashQueryClient, EcashSigningClient, GroupQueryClient, MixnetQueryClient, MixnetSigningClient, MultisigQueryClient, MultisigSigningClient, NymContractsProvider, PagedMixnetQueryClient, PagedMultisigQueryClient, PagedVestingQueryClient, SpDirectoryQueryClient, @@ -401,12 +401,23 @@ impl crate::coconut::client::Client for Client { async fn get_spent_credential( &self, blinded_serial_number: String, - ) -> crate::coconut::error::Result { + ) -> crate::coconut::error::Result { Ok(nyxd_query!( self, get_spent_credential(blinded_serial_number).await? )) } + + async fn propose_for_blacklist( + &self, + public_key: String, + ) -> crate::coconut::error::Result { + Ok(nyxd_signing!( + self, + propose_for_blacklist(public_key, None).await? + )) + } + async fn get_blacklisted_account( &self, public_key: String,