API route for spending online and offline ecash
This commit is contained in:
@@ -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"] }
|
||||
|
||||
@@ -7,4 +7,5 @@ pub mod models;
|
||||
pub use models::{
|
||||
BlindSignRequestBody, BlindedSignatureResponse, CredentialsRequestBody, FreePassRequest,
|
||||
PartialCoinIndicesSignatureResponse, PartialExpirationDateSignatureResponse,
|
||||
VerificationKeyResponse, VerifyCredentialResponse, VerifyEcashCredentialBody,
|
||||
};
|
||||
|
||||
@@ -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<u64>,
|
||||
}
|
||||
|
||||
impl VerifyEcashCredentialBody {
|
||||
pub fn new(
|
||||
credential: CredentialSpendingData,
|
||||
gateway_cosmos_addr: AccountId,
|
||||
proposal_id: Option<u64>,
|
||||
) -> 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 {
|
||||
|
||||
@@ -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 = "<verify_credential_body>")]
|
||||
pub async fn verify_bandwidth_credential(
|
||||
verify_credential_body: Json<VerifyCredentialBody>,
|
||||
#[post("/verify-online-credential", data = "<verify_credential_body>")]
|
||||
pub async fn verify_online_credential(
|
||||
verify_credential_body: Json<VerifyEcashCredentialBody>,
|
||||
state: &RocketState<State>,
|
||||
) -> Result<Json<VerifyCredentialResponse>> {
|
||||
) -> Result<Json<VerifyEcashCredentialResponse>> {
|
||||
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 = "<verify_credential_body>")]
|
||||
pub async fn verify_offline_credential(
|
||||
verify_credential_body: Json<VerifyEcashCredentialBody>,
|
||||
state: &RocketState<State>,
|
||||
) -> Result<Json<VerifyEcashCredentialResponse>> {
|
||||
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(
|
||||
|
||||
@@ -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<SpendCredentialResponse>;
|
||||
) -> Result<EcashSpentCredentialResponse>;
|
||||
|
||||
async fn propose_for_blacklist(&self, public_key: String) -> Result<ExecuteResult>;
|
||||
async fn get_blacklisted_account(
|
||||
&self,
|
||||
public_key: String,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// 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<u64>,
|
||||
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<u64>) {
|
||||
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<u64>) {
|
||||
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::<u64>()
|
||||
.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<VerificationKey> {
|
||||
pub async fn verification_key(&self, epoch_id: EpochId) -> Result<VerificationKeyAuth> {
|
||||
self.comm_channel
|
||||
.aggregated_verification_key(epoch_id)
|
||||
.await
|
||||
|
||||
@@ -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<SpendCredentialResponse> {
|
||||
) -> crate::coconut::error::Result<EcashSpentCredentialResponse> {
|
||||
Ok(nyxd_query!(
|
||||
self,
|
||||
get_spent_credential(blinded_serial_number).await?
|
||||
))
|
||||
}
|
||||
|
||||
async fn propose_for_blacklist(
|
||||
&self,
|
||||
public_key: String,
|
||||
) -> crate::coconut::error::Result<ExecuteResult> {
|
||||
Ok(nyxd_signing!(
|
||||
self,
|
||||
propose_for_blacklist(public_key, None).await?
|
||||
))
|
||||
}
|
||||
|
||||
async fn get_blacklisted_account(
|
||||
&self,
|
||||
public_key: String,
|
||||
|
||||
Reference in New Issue
Block a user