Another Grand Ecash Squasheroo

add offline ecash library

minor changes in coconut benchmarks

add ecash smart contract

change contract traits from coconut to ecash

first wave of andrew's suggestion

first wave of andrew's suggestion

second wave of andrew's suggestion for ecash lib

andrew's suggestion for ecash contract

licensing commit

safety comments for most unwraps

more unwrap handling

change chrono crate for time

latest cargo lock

error revamp

small visibility fix

small fix

remove indexedmap from contract + some tweaks

add cw2 version in ecash contract

remove envryption key from contract

change types from coconut to ecash types

adapt api model for credential issuance

adapt issued credential storage on API

add signatures cache on API

change API routes for new blind signing

modify issued_credential table

add issuance logic client-side

credential and signature storage client side

utils for credential issuance

first wave of fix

some of andrew's suggestions

remove encryption key from deposit

freepass issuance client side

freepass issuance API side

andrew's suggested fixes

other suggested fix

adapt change from PR below

allow offline verification flag

credential spending models

credential spending models for client

credential preperation for the client

credential preperation for the client

credential storage for spending on client

bloom filter for API

spent credential storage on validators

API route for spending online and offline ecash

API routes in the client lib

credential storage on gateway

ecash verifier to replace coconut verifier

accept credentials on gateway

bandwidth expiration for gateways

client ask for more bandwidth if it runs out

credential import

adapt nym validator rewarder and sdk

fix tests api tests and add constants

cargo fmt and lock and small test fix

cargo fmt and lock and small test fix

cargo lock

move stuff where they belong in ecash and static parameters

move some constants, error handling and phase out time crate

error revamp part 2

secret key by ref instead of clone

change l in wallet and v visibility

rework payinfo

rework monster tuples

fix expiration date signature cloning

minor fixes

final bits and bobs fixes

final bits and bobs fixes

rename l accessor to tickets_spent

wave of fixes

second wave of fixes

change hash domain value

removed benchmark flag

remove useless stringification in storage

nuke Bandwidth voucher

change timestamps to offsetdatetime

key name change

post-rebase fixes

update nym-connect 'time' dep due to broken semver

upload ecash contract to the build server

make wasm zknym-lib compile

but it won't work properly just yet

make wasm zknym-lib compile

but it won't work properly just yet

fix typo in ecash contract deps

make sure to use 0.1.0 sphinx packet

optimise pairings in 'check_vk_pairing'

derive serde for ecash types

simplified g1 tuple byte conversion

further optimise the pairing

unified signature type + renamed nym-api coconut module to ecash

using bincode serialiser for more complex binary types

using multimiller loop instead of rayon for verifying coin indices signatures

batching signature verification wherever possible

feature-locked rayon

clippy

refactor ecash contract a bit + introduce deposit storage

reworked find_proposal_id

various minor fixed

add offline_zk_nyms to nym-node everywhere

add missing #query

change test value to fit new serialization

optimised deposits storage

removed duplicate decompression code

using deposit_id instead of transaction hash

removed freepasses

split up ecash handling

unified shared state

fixed deposit_id parsing

log recovered deposit id

removed online verification

add detailed build info to ecash contract

fixed deserialisation of deposit amount received from nyxd queries

changed deposit to only persist attached pubkey

first iteration of split of verification and redemption

basic tool for setting up new network

expanded the tool with the option to bypass DKG

rename + init network without DKG

setting up locally running apis

ecash key migration

more local functionalities

wip fixing sql schemas

gateway immediately submitting redemption proposal

and getting it passed if valid

most of the gateway logic for split redemption with error recovery

fixed gateway not persisting ecash signers

simplify creation of compatible client

create properly serialised ecash key from the beginning

rebuild missing tickets and proposals on startup

stop ticket issuance during DKG transition

fixing build issues

split out ecash storage on nym-api side

master-verification-key route

caching all the signatures and keys

implemented aggregated routes for nym-apis

swagger UI for ecash endpoints

added explicit annotation for index and expiration signatures

revamped client ticketbook storage

save all recovery information in the same underlying storage

wrapper for bloomfilter

being more aggressive with marking tickets as used

ensure client has correct signatures before making deposit

fix deserialisation of AggregatedExpirationDateSignatureResponse + add ticketbook table

split nym-api ecash routes handlers into multiple files

fixed deserialisation of encoded expiration date

add tt_gamma1 to challenge and change naming for paper consistency

rotating double spending bloomfilter

nym-api test fixes + make sure to insert initial BF params

fixed ecash benchmark code

updated contract schema

updated CI to not upload gateway/mixnode binaries

ticket bandwidth revocation

added default deserialisation for zk nym config

post-rebase fixes
This commit is contained in:
Simon Wicky
2024-05-07 09:38:06 +02:00
committed by Jędrzej Stuczyński
parent 7ddd819ff3
commit fc2eedfc66
362 changed files with 27327 additions and 8756 deletions
@@ -42,7 +42,7 @@ jobs:
strategy:
fail-fast: false
matrix:
platform: [ubuntu-20.04]
platform: [ ubuntu-20.04 ]
runs-on: ${{ matrix.platform }}
env:
@@ -15,7 +15,7 @@ jobs:
strategy:
fail-fast: false
matrix:
platform: [ubuntu-20.04]
platform: [ ubuntu-20.04 ]
runs-on: ${{ matrix.platform }}
env:
@@ -58,6 +58,7 @@ jobs:
cp contracts/target/wasm32-unknown-unknown/release/nym_coconut_dkg.wasm $OUTPUT_DIR
cp contracts/target/wasm32-unknown-unknown/release/cw3_flex_multisig.wasm $OUTPUT_DIR
cp contracts/target/wasm32-unknown-unknown/release/cw4_group.wasm $OUTPUT_DIR
cp contracts/target/wasm32-unknown-unknown/release/nym_ecash.wasm $OUTPUT_DIR
- name: Deploy branch to CI www
continue-on-error: true
Generated
+1093 -423
View File
File diff suppressed because it is too large Load Diff
+27 -8
View File
@@ -34,6 +34,7 @@ members = [
"common/commands",
"common/config",
"common/cosmwasm-smart-contracts/coconut-bandwidth-contract",
"common/cosmwasm-smart-contracts/ecash-contract",
"common/cosmwasm-smart-contracts/coconut-dkg",
"common/cosmwasm-smart-contracts/contracts-common",
"common/cosmwasm-smart-contracts/group-contract",
@@ -47,6 +48,8 @@ members = [
"common/credentials-interface",
"common/crypto",
"common/dkg",
"common/ecash-double-spending",
"common/ecash-time",
"common/execute",
"common/exit-policy",
"common/http-api-client",
@@ -59,6 +62,7 @@ members = [
"common/node-tester-utils",
"common/nonexhaustive-delayqueue",
"common/nymcoconut",
"common/nym_offline_compact_ecash",
"common/nym-id",
"common/nym-metrics",
"common/nymsphinx",
@@ -74,6 +78,7 @@ members = [
"common/nymsphinx/types",
"common/nyxd-scraper",
"common/pemstore",
"common/serde-helpers",
"common/socks5-client-core",
"common/socks5/proxy-helpers",
"common/socks5/requests",
@@ -120,6 +125,8 @@ members = [
"wasm/mix-fetch",
"wasm/node-tester",
"wasm/zknym-lib",
"tools/internal/testnet-manager",
"tools/internal/testnet-manager/dkg-bypass-contract",
]
default-members = [
@@ -139,6 +146,7 @@ exclude = [
"explorer",
"contracts",
"nym-wallet",
"nym-vpn/ui/src-tauri",
"cpu-cycles",
"sdk/ffi/cpp",
]
@@ -163,8 +171,13 @@ axum-extra = "0.9.3"
base64 = "0.21.4"
bincode = "1.3.3"
bip39 = { version = "2.0.0", features = ["zeroize"] }
# can we unify those?
bit-vec = "0.7.0"
bitvec = "1.0.0"
blake3 = "1.3.1"
bloomfilter = "1.0.14"
bs58 = "0.5.1"
bytecodec = "0.4.15"
bytes = "1.5.0"
@@ -216,11 +229,11 @@ httpcodec = "0.2.3"
humantime = "2.1.0"
humantime-serde = "1.1.1"
hyper = "1.3.1"
indexed_db_futures = "0.3.0"
inquire = "0.6.2"
ip_network = "0.4.1"
ipnetwork = "0.16"
isocountry = "0.3.2"
itertools = "0.13.0"
k256 = "0.13"
lazy_static = "1.4.0"
ledger-transport = "0.10.0"
@@ -305,7 +318,8 @@ prometheus = { version = "0.13.0" }
# coconut/DKG related
# unfortunately until https://github.com/zkcrypto/bls12_381/issues/10 is resolved, we have to rely on the fork
# as we need to be able to serialize Gt so that we could create the lookup table for baby-step-giant-step algorithm
bls12_381 = { git = "https://github.com/jstuczyn/bls12_381", default-features = false, branch = "feature/gt-serialization-0.8.0" }
# plus to make our live easier we need serde support from https://github.com/zkcrypto/bls12_381/pull/125
bls12_381 = { git = "https://github.com/jstuczyn/bls12_381", default-features = false, branch = "temp/experimental-serdect" }
group = { version = "0.13.0", default-features = false }
ff = { version = "0.13.0", default-features = false }
@@ -328,16 +342,22 @@ cw-controllers = { version = "=1.1.0" }
# cosmrs-related
bip32 = { version = "0.5.1", default-features = false }
# temporarily using a fork again (yay.) because we need staking and slashing support
cosmrs = { git = "https://github.com/jstuczyn/cosmos-rust", branch = "nym-temp/all-validator-features" }
#cosmrs = { git = "https://github.com/jstuczyn/cosmos-rust", branch = "nym-temp/all-validator-features" } # unfortuntely we need a fork by yours truly to get the staking support
tendermint = "0.34" # same version as used by cosmrs
tendermint-rpc = "0.34" # same version as used by cosmrs
# temporarily using a fork again (yay.) because we need staking and slashing support (which are already on main but not released)
# plus response message parsing (which is, as of the time of writing this message, waiting to get merged)
#cosmrs = { path = "../cosmos-rust-fork/cosmos-rust/cosmrs" }
cosmrs = { git = "https://github.com/cosmos/cosmos-rust", rev = "4b1332e6d8258ac845cef71589c8d362a669675a" } # unfortuntely we need a fork by yours truly to get the staking support
tendermint = "0.37.0" # same version as used by cosmrs
tendermint-rpc = "0.37.0" # same version as used by cosmrs
prost = { version = "0.12", default-features = false }
# wasm-related dependencies
gloo-utils = "0.2.0"
gloo-net = "0.5.0"
# use a separate branch due to feature unification failures
# this is blocked until the upstream removes outdates `wasm_bindgen` feature usage
# indexed_db_futures = "0.4.1"
indexed_db_futures = { git = "https://github.com/TiemenSch/rust-indexed-db", branch = "update-uuid" }
js-sys = "0.3.69"
serde-wasm-bindgen = "0.6.5"
tsify = "0.4.5"
@@ -345,7 +365,6 @@ wasm-bindgen = "0.2.92"
wasm-bindgen-futures = "0.4.39"
wasmtimer = "0.2.0"
web-sys = "0.3.69"
itertools = "0.12.0"
# Profile settings for individual crates
+1 -1
View File
@@ -133,7 +133,7 @@ clippy: sdk-wasm-lint
# Build contracts ready for deploy
# -----------------------------------------------------------------------------
CONTRACTS=vesting_contract mixnet_contract
CONTRACTS=vesting_contract mixnet_contract nym_ecash
CONTRACTS_WASM=$(addsuffix .wasm, $(CONTRACTS))
CONTRACTS_OUT_DIR=contracts/target/wasm32-unknown-unknown/release
+5
View File
@@ -26,6 +26,7 @@ pub(crate) mod import_credential;
pub(crate) mod init;
mod list_gateways;
pub(crate) mod run;
mod show_ticketbooks;
mod switch_gateway;
pub(crate) struct CliNativeClient;
@@ -84,6 +85,9 @@ pub(crate) enum Commands {
/// Change the currently active gateway. Note that you must have already registered with the new gateway!
SwitchGateway(switch_gateway::Args),
/// Display information associated with the imported ticketbooks,
ShowTicketbooks(show_ticketbooks::Args),
/// Show build information of this binary
BuildInfo(build_info::BuildInfo),
@@ -116,6 +120,7 @@ pub(crate) async fn execute(args: Cli) -> Result<(), Box<dyn Error + Send + Sync
Commands::ListGateways(args) => list_gateways::execute(args).await?,
Commands::AddGateway(args) => add_gateway::execute(args).await?,
Commands::SwitchGateway(args) => switch_gateway::execute(args).await?,
Commands::ShowTicketbooks(args) => show_ticketbooks::execute(args).await?,
Commands::BuildInfo(m) => build_info::execute(m),
Commands::Completions(s) => s.generate(&mut Cli::command(), bin_name),
Commands::GenerateFigSpec => fig_generate(&mut Cli::command(), bin_name),
@@ -0,0 +1,32 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::commands::CliNativeClient;
use crate::error::ClientError;
use nym_bin_common::output_format::OutputFormat;
use nym_client_core::cli_helpers::client_show_ticketbooks::{
show_ticketbooks, CommonShowTicketbooksArgs,
};
#[derive(clap::Args)]
pub(crate) struct Args {
#[command(flatten)]
common_args: CommonShowTicketbooksArgs,
#[arg(short, long, default_value_t = OutputFormat::default())]
output: OutputFormat,
}
impl AsRef<CommonShowTicketbooksArgs> for Args {
fn as_ref(&self) -> &CommonShowTicketbooksArgs {
&self.common_args
}
}
pub(crate) async fn execute(args: Args) -> Result<(), ClientError> {
let output = args.output;
let res = show_ticketbooks::<CliNativeClient, _>(args).await?;
println!("{}", output.format(&res));
Ok(())
}
+5
View File
@@ -30,6 +30,7 @@ mod import_credential;
pub mod init;
mod list_gateways;
pub(crate) mod run;
mod show_ticketbooks;
mod switch_gateway;
pub(crate) struct CliSocks5Client;
@@ -88,6 +89,9 @@ pub(crate) enum Commands {
/// Change the currently active gateway. Note that you must have already registered with the new gateway!
SwitchGateway(switch_gateway::Args),
/// Display information associated with the imported ticketbooks,
ShowTicketbooks(show_ticketbooks::Args),
/// Show build information of this binary
BuildInfo(build_info::BuildInfo),
@@ -123,6 +127,7 @@ pub(crate) async fn execute(args: Cli) -> Result<(), Box<dyn Error + Send + Sync
Commands::ListGateways(args) => list_gateways::execute(args).await?,
Commands::AddGateway(args) => add_gateway::execute(args).await?,
Commands::SwitchGateway(args) => switch_gateway::execute(args).await?,
Commands::ShowTicketbooks(args) => show_ticketbooks::execute(args).await?,
Commands::BuildInfo(m) => build_info::execute(m),
Commands::Completions(s) => s.generate(&mut Cli::command(), bin_name),
Commands::GenerateFigSpec => fig_generate(&mut Cli::command(), bin_name),
@@ -0,0 +1,32 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::commands::CliSocks5Client;
use crate::error::Socks5ClientError;
use nym_bin_common::output_format::OutputFormat;
use nym_client_core::cli_helpers::client_show_ticketbooks::{
show_ticketbooks, CommonShowTicketbooksArgs,
};
#[derive(clap::Args)]
pub(crate) struct Args {
#[command(flatten)]
common_args: CommonShowTicketbooksArgs,
#[arg(short, long, default_value_t = OutputFormat::default())]
output: OutputFormat,
}
impl AsRef<CommonShowTicketbooksArgs> for Args {
fn as_ref(&self) -> &CommonShowTicketbooksArgs {
&self.common_args
}
}
pub(crate) async fn execute(args: Args) -> Result<(), Socks5ClientError> {
let output = args.output;
let res = show_ticketbooks::<CliSocks5Client, _>(args).await?;
println!("{}", output.format(&res));
Ok(())
}
+2 -1
View File
@@ -14,13 +14,14 @@ thiserror = { workspace = true }
url = { workspace = true }
zeroize = { workspace = true }
nym-coconut = { path = "../nymcoconut" }
nym-ecash-time = { path = "../ecash-time" }
nym-credential-storage = { path = "../credential-storage" }
nym-credentials = { path = "../credentials" }
nym-credentials-interface = { path = "../credentials-interface" }
nym-crypto = { path = "../crypto", features = ["rand", "asymmetric", "symmetric", "aes", "hashing"] }
nym-network-defaults = { path = "../network-defaults" }
nym-validator-client = { path = "../client-libs/validator-client", default-features = false }
nym-ecash-contract-common = { path = "../cosmwasm-smart-contracts/ecash-contract" }
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.nym-validator-client]
path = "../client-libs/validator-client"
+82 -52
View File
@@ -1,87 +1,117 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// Copyright 2023-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::error::BandwidthControllerError;
use nym_credential_storage::models::StorableIssuedCredential;
use crate::utils::{get_coin_index_signatures, get_expiration_date_signatures};
use log::info;
use nym_credential_storage::storage::Storage;
use nym_credentials::coconut::bandwidth::{CredentialType, IssuanceBandwidthCredential};
use nym_credentials::coconut::utils::obtain_aggregate_signature;
use nym_crypto::asymmetric::{encryption, identity};
use nym_validator_client::coconut::all_coconut_api_clients;
use nym_validator_client::nyxd::contract_traits::CoconutBandwidthSigningClient;
use nym_credentials::ecash::bandwidth::IssuanceTicketBook;
use nym_credentials::ecash::utils::obtain_aggregate_wallet;
use nym_credentials::IssuedTicketBook;
use nym_crypto::asymmetric::identity;
use nym_ecash_time::{ecash_default_expiration_date, Date};
use nym_validator_client::coconut::all_ecash_api_clients;
use nym_validator_client::nym_api::EpochId;
use nym_validator_client::nyxd::contract_traits::DkgQueryClient;
use nym_validator_client::nyxd::Coin;
use nym_validator_client::nyxd::contract_traits::EcashSigningClient;
use nym_validator_client::nyxd::cosmwasm_client::ToSingletonContractData;
use nym_validator_client::EcashApiClient;
use rand::rngs::OsRng;
use state::State;
use zeroize::Zeroizing;
pub mod state;
pub async fn deposit<C>(client: &C, amount: Coin) -> Result<State, BandwidthControllerError>
pub async fn make_deposit<C>(
client: &C,
client_id: &[u8],
expiration: Option<Date>,
) -> Result<IssuanceTicketBook, BandwidthControllerError>
where
C: CoconutBandwidthSigningClient + Sync,
C: EcashSigningClient + Sync,
{
let mut rng = OsRng;
let signing_key = identity::PrivateKey::new(&mut rng);
let encryption_key = encryption::PrivateKey::new(&mut rng);
let expiration = expiration.unwrap_or_else(ecash_default_expiration_date);
let tx_hash = client
.deposit(
amount.clone(),
CredentialType::Voucher.to_string(),
signing_key.public_key().to_base58_string(),
encryption_key.public_key().to_base58_string(),
None,
)
.await?
.transaction_hash;
let result = client
.make_ticketbook_deposit(signing_key.public_key().to_base58_string(), None)
.await?;
let voucher =
IssuanceBandwidthCredential::new_voucher(amount, tx_hash, signing_key, encryption_key);
let deposit_id = result.parse_singleton_u32_contract_data()?;
let state = State { voucher };
info!("our ticketbook deposit has been stored under id {deposit_id}");
Ok(state)
Ok(IssuanceTicketBook::new_with_expiration(
deposit_id,
client_id,
signing_key,
expiration,
))
}
pub async fn get_bandwidth_voucher<C, St>(
state: &State,
pub async fn query_and_persist_required_global_signatures<S>(
storage: &S,
epoch_id: EpochId,
expiration_date: Date,
apis: Vec<EcashApiClient>,
) -> Result<(), BandwidthControllerError>
where
S: Storage,
<S as Storage>::StorageError: Send + Sync + 'static,
{
log::info!("Getting expiration date signatures");
// this will also persist the signatures in the storage if they were not there already
get_expiration_date_signatures(storage, epoch_id, expiration_date, apis.clone()).await?;
log::info!("Getting coin indices signatures");
// this will also persist the signatures in the storage if they were not there already
get_coin_index_signatures(storage, epoch_id, apis).await?;
Ok(())
}
pub async fn get_ticket_book<C, St>(
issuance_data: &IssuanceTicketBook,
client: &C,
storage: &St,
) -> Result<(), BandwidthControllerError>
apis: Option<Vec<EcashApiClient>>,
) -> Result<IssuedTicketBook, BandwidthControllerError>
where
C: DkgQueryClient + Send + Sync,
St: Storage,
<St as Storage>::StorageError: Send + Sync + 'static,
{
// temporary
assert!(state.voucher.typ().is_voucher());
let epoch_id = client.get_current_epoch().await?.epoch_id;
let threshold = client
.get_current_epoch_threshold()
.await?
.ok_or(BandwidthControllerError::NoThreshold)?;
let coconut_api_clients = all_coconut_api_clients(client, epoch_id).await?;
let signature =
obtain_aggregate_signature(&state.voucher, &coconut_api_clients, threshold).await?;
let issued = state.voucher.to_issued_credential(signature, epoch_id);
// make sure the data gets zeroized after persisting it
let credential_data = Zeroizing::new(issued.pack_v1());
let storable = StorableIssuedCredential {
serialization_revision: issued.current_serialization_revision(),
credential_data: credential_data.as_ref(),
credential_type: issued.typ().to_string(),
epoch_id: epoch_id
.try_into()
.expect("our epoch is has run over u32::MAX!"),
let apis = match apis {
Some(apis) => apis,
None => all_ecash_api_clients(client, epoch_id).await?,
};
log::info!("Querying wallet signatures");
let wallet = obtain_aggregate_wallet(issuance_data, &apis, threshold).await?;
info!("managed to obtain sufficient number of partial signatures!");
log::info!("Getting expiration date signatures");
// this will also persist the signatures in the storage if they were not there already
get_expiration_date_signatures(
storage,
epoch_id,
issuance_data.expiration_date(),
apis.clone(),
)
.await?;
log::info!("Getting coin indices signatures");
// this will also persist the signatures in the storage if they were not there already
get_coin_index_signatures(storage, epoch_id, apis).await?;
let issued = issuance_data.to_issued_ticketbook(wallet, epoch_id);
info!("persisting the ticketbook into the storage...");
storage
.insert_issued_credential(storable)
.insert_issued_ticketbook(&issued)
.await
.map_err(|err| BandwidthControllerError::CredentialStorageError(Box::new(err)))
.map_err(|err| BandwidthControllerError::CredentialStorageError(Box::new(err)))?;
Ok(issued)
}
@@ -1,14 +0,0 @@
// Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_credentials::coconut::bandwidth::IssuanceBandwidthCredential;
pub struct State {
pub voucher: IssuanceBandwidthCredential,
}
impl State {
pub fn new(voucher: IssuanceBandwidthCredential) -> Self {
State { voucher }
}
}
+16 -5
View File
@@ -1,12 +1,12 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_coconut::CoconutError;
use nym_credential_storage::error::StorageError;
use nym_credentials::error::Error as CredentialsError;
use nym_credentials_interface::CompactEcashError;
use nym_crypto::asymmetric::encryption::KeyRecoveryError;
use nym_crypto::asymmetric::identity::Ed25519RecoveryError;
use nym_validator_client::coconut::CoconutApiError;
use nym_validator_client::coconut::EcashApiError;
use nym_validator_client::error::ValidatorClientError;
use thiserror::Error;
@@ -16,7 +16,7 @@ pub enum BandwidthControllerError {
Nyxd(#[from] nym_validator_client::nyxd::error::NyxdError),
#[error("coconut api query failure: {0}")]
CoconutApiError(#[from] CoconutApiError),
CoconutApiError(#[from] EcashApiError),
#[error("There was a credential storage error - {0}")]
CredentialStorageError(Box<dyn std::error::Error + Send + Sync>),
@@ -28,8 +28,8 @@ pub enum BandwidthControllerError {
#[error(transparent)]
StorageError(#[from] StorageError),
#[error("Coconut error - {0}")]
CoconutError(#[from] CoconutError),
#[error("Ecash error - {0}")]
EcashError(#[from] CompactEcashError),
#[error("Validator client error - {0}")]
ValidatorError(#[from] ValidatorClientError),
@@ -51,4 +51,15 @@ pub enum BandwidthControllerError {
#[error("can't handle recovering storage with revision {stored}. {expected} was expected")]
UnsupportedCredentialStorageRevision { stored: u8, expected: u8 },
#[error("did not receive a valid response for aggregated data ({typ}) from ANY nym-api")]
ExhaustedApiQueries { typ: String },
}
impl BandwidthControllerError {
pub fn credential_storage_error(
source: impl std::error::Error + Send + Sync + 'static,
) -> Self {
BandwidthControllerError::CredentialStorageError(Box::new(source))
}
}
+149 -90
View File
@@ -1,16 +1,24 @@
// Copyright 2021-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
#![warn(clippy::expect_used)]
#![warn(clippy::unwrap_used)]
#![warn(clippy::todo)]
#![warn(clippy::dbg_macro)]
use crate::error::BandwidthControllerError;
use crate::utils::stored_credential_to_issued_bandwidth;
use log::{debug, error, warn};
use crate::utils::{
get_aggregate_verification_key, get_coin_index_signatures, get_expiration_date_signatures,
ApiClientsWrapper,
};
use log::error;
use nym_credential_storage::models::RetrievedTicketbook;
use nym_credential_storage::storage::Storage;
use nym_credentials::coconut::bandwidth::issued::BandwidthCredentialIssuedDataVariant;
use nym_credentials::coconut::bandwidth::CredentialSpendingData;
use nym_credentials::coconut::utils::obtain_aggregate_verification_key;
use nym_credentials::IssuedBandwidthCredential;
use nym_credentials_interface::VerificationKey;
use nym_validator_client::coconut::all_coconut_api_clients;
use nym_credentials::ecash::bandwidth::CredentialSpendingData;
use nym_credentials_interface::{
AnnotatedCoinIndexSignature, AnnotatedExpirationDateSignature, NymPayInfo, VerificationKeyAuth,
};
use nym_ecash_time::Date;
use nym_validator_client::nym_api::EpochId;
use nym_validator_client::nyxd::contract_traits::DkgQueryClient;
@@ -35,13 +43,20 @@ pub struct PreparedCredential {
/// could use correct verification key for validation.
pub epoch_id: EpochId,
/// The database id of the stored credential.
pub credential_id: i64,
/// Auxiliary metadata associated with the withdrawn credential
pub metadata: PreparedCredentialMetadata,
}
pub struct RetrievedCredential {
pub credential: IssuedBandwidthCredential,
pub credential_id: i64,
#[derive(Copy, Clone)]
pub struct PreparedCredentialMetadata {
/// The database id of the stored credential.
pub ticketbook_id: i64,
/// The number of tickets withdrawn in this credential
pub tickets_withdrawn: u32,
/// The amount of tickets used INCLUDING those tickets that JUST got withdrawn
pub used_tickets: u32,
}
impl<C, St: Storage> BandwidthController<C, St> {
@@ -50,111 +65,155 @@ impl<C, St: Storage> BandwidthController<C, St> {
}
/// Tries to retrieve one of the stored, unused credentials that hasn't yet expired.
/// It marks any retrieved intermediate credentials as expired.
pub async fn get_next_usable_credential(
pub async fn get_next_usable_ticketbook(
&self,
gateway_id: &str,
) -> Result<RetrievedCredential, BandwidthControllerError>
tickets: u32,
) -> Result<RetrievedTicketbook, BandwidthControllerError>
where
<St as Storage>::StorageError: Send + Sync + 'static,
{
loop {
let Some(maybe_next) = self
.storage
.get_next_unspent_credential(gateway_id)
.await
.map_err(|err| BandwidthControllerError::CredentialStorageError(Box::new(err)))?
else {
return Err(BandwidthControllerError::NoCredentialsAvailable);
};
let id = maybe_next.id;
let Some(ticketbook) = self
.storage
.get_next_unspent_usable_ticketbook(tickets)
.await
.map_err(BandwidthControllerError::credential_storage_error)?
else {
return Err(BandwidthControllerError::NoCredentialsAvailable);
};
// try to deserialize it
let valid_credential = match stored_credential_to_issued_bandwidth(maybe_next) {
// check if it has already expired
Ok(credential) => match credential.variant_data() {
BandwidthCredentialIssuedDataVariant::Voucher(_) => {
debug!("credential {id} is a bandwidth voucher");
credential
}
BandwidthCredentialIssuedDataVariant::FreePass(freepass_info) => {
debug!("credential {id} is a free pass");
if freepass_info.expired() {
warn!("the free pass (id: {id}) has already expired! The expiration was set to {}", freepass_info.expiry_date());
self.storage.mark_expired(id).await.map_err(|err| {
BandwidthControllerError::CredentialStorageError(Box::new(err))
})?;
continue;
}
credential
}
},
Err(err) => {
error!("failed to deserialize credential with id {id}: {err}. it may need to be manually removed from the storage");
return Err(err);
}
};
return Ok(RetrievedCredential {
credential: valid_credential,
credential_id: id,
});
}
Ok(ticketbook)
}
pub fn storage(&self) -> &St {
&self.storage
pub async fn attempt_revert_ticket_usage(
&self,
info: PreparedCredentialMetadata,
) -> Result<bool, BandwidthControllerError>
where
<St as Storage>::StorageError: Send + Sync + 'static,
{
self.storage
.attempt_revert_ticketbook_withdrawal(
info.ticketbook_id,
info.used_tickets,
info.tickets_withdrawn,
)
.await
.map_err(BandwidthControllerError::credential_storage_error)
}
async fn get_aggregate_verification_key(
&self,
epoch_id: EpochId,
) -> Result<VerificationKey, BandwidthControllerError>
apis: &mut ApiClientsWrapper,
) -> Result<VerificationKeyAuth, BandwidthControllerError>
where
C: DkgQueryClient + Sync + Send,
<St as Storage>::StorageError: Send + Sync + 'static,
{
let coconut_api_clients = all_coconut_api_clients(&self.client, epoch_id).await?;
Ok(obtain_aggregate_verification_key(&coconut_api_clients)?)
let ecash_apis = apis.get_or_init(epoch_id, &self.client).await?;
get_aggregate_verification_key(&self.storage, epoch_id, ecash_apis).await
}
pub async fn prepare_bandwidth_credential(
async fn get_coin_index_signatures(
&self,
gateway_id: &str,
epoch_id: EpochId,
apis: &mut ApiClientsWrapper,
) -> Result<Vec<AnnotatedCoinIndexSignature>, BandwidthControllerError>
where
C: DkgQueryClient + Sync + Send,
<St as Storage>::StorageError: Send + Sync + 'static,
{
let ecash_apis = apis.get_or_init(epoch_id, &self.client).await?;
get_coin_index_signatures(&self.storage, epoch_id, ecash_apis).await
}
async fn get_expiration_date_signatures(
&self,
epoch_id: EpochId,
expiration_date: Date,
apis: &mut ApiClientsWrapper,
) -> Result<Vec<AnnotatedExpirationDateSignature>, BandwidthControllerError>
where
C: DkgQueryClient + Sync + Send,
<St as Storage>::StorageError: Send + Sync + 'static,
{
let ecash_apis = apis.get_or_init(epoch_id, &self.client).await?;
get_expiration_date_signatures(&self.storage, epoch_id, expiration_date, ecash_apis).await
}
async fn prepare_ecash_ticket_inner(
&self,
provider_pk: [u8; 32],
tickets_to_spend: u32,
mut retrieved_ticketbook: RetrievedTicketbook,
) -> Result<CredentialSpendingData, BandwidthControllerError>
where
C: DkgQueryClient + Sync + Send,
<St as Storage>::StorageError: Send + Sync + 'static,
{
let epoch_id = retrieved_ticketbook.ticketbook.epoch_id();
let expiration_date = retrieved_ticketbook.ticketbook.expiration_date();
let mut api_clients = Default::default();
let verification_key = self
.get_aggregate_verification_key(epoch_id, &mut api_clients)
.await?;
let expiration_signatures = self
.get_expiration_date_signatures(epoch_id, expiration_date, &mut api_clients)
.await?;
let coin_indices_signatures = self
.get_coin_index_signatures(epoch_id, &mut api_clients)
.await?;
let pay_info = NymPayInfo::generate(provider_pk);
let spend_request = retrieved_ticketbook.ticketbook.prepare_for_spending(
&verification_key,
pay_info.into(),
&coin_indices_signatures,
&expiration_signatures,
tickets_to_spend as u64,
)?;
Ok(spend_request)
}
pub async fn prepare_ecash_ticket(
&self,
provider_pk: [u8; 32],
tickets_to_spend: u32,
) -> Result<PreparedCredential, BandwidthControllerError>
where
C: DkgQueryClient + Sync + Send,
<St as Storage>::StorageError: Send + Sync + 'static,
{
let retrieved_credential = self.get_next_usable_credential(gateway_id).await?;
let retrieved_ticketbook = self.get_next_usable_ticketbook(tickets_to_spend).await?;
let epoch_id = retrieved_credential.credential.epoch_id();
let credential_id = retrieved_credential.credential_id;
let ticketbook_id = retrieved_ticketbook.ticketbook_id;
let epoch_id = retrieved_ticketbook.ticketbook.epoch_id();
let verification_key = self.get_aggregate_verification_key(epoch_id).await?;
let used_tickets =
retrieved_ticketbook.ticketbook.spent_tickets() as u32 + tickets_to_spend;
let metadata = PreparedCredentialMetadata {
ticketbook_id,
tickets_withdrawn: tickets_to_spend,
used_tickets,
};
let spend_request = retrieved_credential
.credential
.prepare_for_spending(&verification_key)?;
Ok(PreparedCredential {
data: spend_request,
epoch_id,
credential_id,
})
}
pub async fn consume_credential(
&self,
id: i64,
gateway_id: &str,
) -> Result<(), BandwidthControllerError>
where
<St as Storage>::StorageError: Send + Sync + 'static,
{
self.storage
.consume_coconut_credential(id, gateway_id)
match self
.prepare_ecash_ticket_inner(provider_pk, tickets_to_spend, retrieved_ticketbook)
.await
.map_err(|err| BandwidthControllerError::CredentialStorageError(Box::new(err)))
{
Ok(data) => Ok(PreparedCredential {
data,
epoch_id,
metadata,
}),
Err(err) => {
error!("failed to prepare credential spending request. attempting to revert withdrawal...");
self.attempt_revert_ticket_usage(metadata).await?;
Err(err)
}
}
}
}
+174 -15
View File
@@ -2,21 +2,180 @@
// SPDX-License-Identifier: Apache-2.0
use crate::error::BandwidthControllerError;
use nym_credential_storage::models::StoredIssuedCredential;
use nym_credentials::coconut::bandwidth::issued::CURRENT_SERIALIZATION_REVISION;
use nym_credentials::coconut::bandwidth::IssuedBandwidthCredential;
use log::warn;
use nym_credential_storage::storage::Storage;
use nym_credentials_interface::{
AnnotatedCoinIndexSignature, AnnotatedExpirationDateSignature, VerificationKeyAuth,
};
use nym_ecash_time::Date;
use nym_validator_client::coconut::all_ecash_api_clients;
use nym_validator_client::nym_api::EpochId;
use nym_validator_client::nyxd::contract_traits::DkgQueryClient;
use nym_validator_client::EcashApiClient;
use rand::prelude::SliceRandom;
use rand::thread_rng;
use std::fmt::Display;
use std::future::Future;
pub fn stored_credential_to_issued_bandwidth(
cred: StoredIssuedCredential,
) -> Result<IssuedBandwidthCredential, BandwidthControllerError> {
if cred.serialization_revision != CURRENT_SERIALIZATION_REVISION {
return Err(
BandwidthControllerError::UnsupportedCredentialStorageRevision {
stored: cred.serialization_revision,
expected: CURRENT_SERIALIZATION_REVISION,
},
);
// it really doesn't need the RwLock because it's never moved across tasks,
// but we need all the Send/Sync action
#[derive(Default)]
pub(crate) struct ApiClientsWrapper(Option<Vec<EcashApiClient>>);
impl ApiClientsWrapper {
pub(crate) async fn get_or_init<C>(
&mut self,
epoch_id: EpochId,
dkg_client: &C,
) -> Result<Vec<EcashApiClient>, BandwidthControllerError>
where
C: DkgQueryClient + Sync + Send,
{
if let Some(cached) = &self.0 {
return Ok(cached.clone());
}
let clients = all_ecash_api_clients(dkg_client, epoch_id).await?;
// technically we don't have to be cloning all the clients here, but it's way simpler than
// dealing with locking and whatnot given the performance penalty is negligible
self.0 = Some(clients.clone());
Ok(clients)
}
Ok(IssuedBandwidthCredential::unpack_v1(&cred.credential_data)?)
}
pub(crate) async fn query_random_apis_until_success<F, T, U, E>(
mut apis: Vec<EcashApiClient>,
f: F,
typ: impl Into<String>,
) -> Result<T, BandwidthControllerError>
where
F: Fn(EcashApiClient) -> U,
U: Future<Output = Result<T, E>>,
E: Display,
{
// try apis in pseudorandom way to remove any bias towards the first registered dealer
apis.shuffle(&mut thread_rng());
for api in apis {
let disp = api.to_string();
match f(api).await {
Ok(res) => return Ok(res),
Err(err) => {
warn!("failed to obtain valid response from API {disp}: {err}")
}
}
}
Err(BandwidthControllerError::ExhaustedApiQueries { typ: typ.into() })
}
pub(crate) async fn get_aggregate_verification_key<St>(
storage: &St,
epoch_id: EpochId,
ecash_apis: Vec<EcashApiClient>,
) -> Result<VerificationKeyAuth, BandwidthControllerError>
where
St: Storage,
<St as Storage>::StorageError: Send + Sync + 'static,
{
if let Some(stored) = storage
.get_master_verification_key(epoch_id)
.await
.map_err(BandwidthControllerError::credential_storage_error)?
{
return Ok(stored);
};
let master_vk = query_random_apis_until_success(
ecash_apis,
|api| async move { api.api_client.master_verification_key(Some(epoch_id)).await },
format!("aggregated verification key for epoch {epoch_id}"),
)
.await?
.key;
// store the retrieved key
storage
.insert_master_verification_key(epoch_id, &master_vk)
.await
.map_err(BandwidthControllerError::credential_storage_error)?;
Ok(master_vk)
}
pub(crate) async fn get_coin_index_signatures<St>(
storage: &St,
epoch_id: EpochId,
ecash_apis: Vec<EcashApiClient>,
) -> Result<Vec<AnnotatedCoinIndexSignature>, BandwidthControllerError>
where
St: Storage,
<St as Storage>::StorageError: Send + Sync + 'static,
{
if let Some(stored) = storage
.get_coin_index_signatures(epoch_id)
.await
.map_err(BandwidthControllerError::credential_storage_error)?
{
return Ok(stored);
};
let index_sigs = query_random_apis_until_success(
ecash_apis,
|api| async move {
api.api_client
.global_coin_indices_signatures(Some(epoch_id))
.await
},
format!("aggregated coin index signatures for epoch {epoch_id}"),
)
.await?
.signatures;
// store the retrieved key
storage
.insert_coin_index_signatures(epoch_id, &index_sigs)
.await
.map_err(BandwidthControllerError::credential_storage_error)?;
Ok(index_sigs)
}
pub(crate) async fn get_expiration_date_signatures<St>(
storage: &St,
epoch_id: EpochId,
expiration_date: Date,
ecash_apis: Vec<EcashApiClient>,
) -> Result<Vec<AnnotatedExpirationDateSignature>, BandwidthControllerError>
where
St: Storage,
<St as Storage>::StorageError: Send + Sync + 'static,
{
if let Some(stored) = storage
.get_expiration_date_signatures(expiration_date)
.await
.map_err(BandwidthControllerError::credential_storage_error)?
{
return Ok(stored);
};
let expiration_sigs = query_random_apis_until_success(
ecash_apis,
|api| async move {
api.api_client
.global_expiration_date_signatures(Some(expiration_date))
.await
},
format!("aggregated coin index signatures for date {expiration_date}"),
)
.await?
.signatures;
// store the retrieved key
storage
.insert_expiration_date_signatures(epoch_id, expiration_date, &expiration_sigs)
.await
.map_err(BandwidthControllerError::credential_storage_error)?;
Ok(expiration_sigs)
}
+3 -1
View File
@@ -14,6 +14,7 @@ base64 = { workspace = true }
bs58 = { workspace = true }
cfg-if = { workspace = true }
clap = { workspace = true, optional = true }
comfy-table = { version = "7.1.1", optional = true }
futures = { workspace = true }
humantime-serde = { workspace = true }
log = { workspace = true }
@@ -50,6 +51,7 @@ nym-network-defaults = { path = "../network-defaults" }
nym-client-core-config-types = { path = "./config-types", features = ["disk-persistence"] }
nym-client-core-surb-storage = { path = "./surb-storage" }
nym-client-core-gateways-storage = { path = "./gateways-storage" }
nym-ecash-time = { path = "../ecash-time" }
### For serving prometheus metrics
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.hyper]
@@ -112,7 +114,7 @@ tempfile = { workspace = true }
[features]
default = []
cli = ["clap"]
cli = ["clap", "comfy-table"]
fs-surb-storage = ["nym-client-core-surb-storage/fs-surb-storage"]
fs-gateways-storage = ["nym-client-core-gateways-storage/fs-gateways-storage"]
wasm = ["nym-gateway-client/wasm"]
@@ -0,0 +1,127 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::cli_helpers::{CliClient, CliClientConfig};
use crate::error::ClientCoreError;
use nym_credential_storage::models::BasicTicketbookInformation;
use nym_credential_storage::storage::Storage;
use nym_ecash_time::ecash_today;
use nym_network_defaults::TICKET_BANDWIDTH_VALUE;
use serde::{Deserialize, Serialize};
use time::Date;
#[derive(Serialize, Deserialize)]
pub struct AvailableTicketbook {
pub id: i64,
pub expiration: Date,
pub issued_tickets: u32,
pub claimed_tickets: u32,
pub ticket_size: u64,
}
impl AvailableTicketbook {
#[cfg(feature = "cli")]
fn table_row(&self) -> comfy_table::Row {
let ecash_today = ecash_today().date();
let issued = self.issued_tickets;
let si_issued = si_scale::helpers::bibytes2((issued as u64 * self.ticket_size) as f64);
let claimed = self.claimed_tickets;
let si_claimed = si_scale::helpers::bibytes2((claimed as u64 * self.ticket_size) as f64);
let remaining = issued - claimed;
let si_remaining =
si_scale::helpers::bibytes2((remaining as u64 * self.ticket_size) as f64);
let si_size = si_scale::helpers::bibytes2(self.ticket_size as f64);
let expiration = if self.expiration <= ecash_today {
comfy_table::Cell::new(format!("EXPIRED ON {}", self.expiration))
.fg(comfy_table::Color::Red)
.add_attribute(comfy_table::Attribute::Bold)
} else {
comfy_table::Cell::new(self.expiration.to_string())
};
vec![
comfy_table::Cell::new(self.id.to_string()),
expiration,
comfy_table::Cell::new(format!("{issued} ({si_issued})")),
comfy_table::Cell::new(format!("{claimed} ({si_claimed})")),
comfy_table::Cell::new(format!("{remaining} ({si_remaining})")),
comfy_table::Cell::new(si_size),
]
.into()
}
}
impl From<BasicTicketbookInformation> for AvailableTicketbook {
fn from(value: BasicTicketbookInformation) -> Self {
AvailableTicketbook {
id: value.id,
expiration: value.expiration_date,
issued_tickets: value.total_tickets,
claimed_tickets: value.used_tickets,
ticket_size: TICKET_BANDWIDTH_VALUE,
}
}
}
#[derive(Serialize, Deserialize)]
#[serde(transparent)]
pub struct AvailableTicketbooks(Vec<AvailableTicketbook>);
#[cfg(feature = "cli")]
impl std::fmt::Display for AvailableTicketbooks {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut table = comfy_table::Table::new();
table.set_header(vec![
"id",
"expiration",
"issued tickets (bandwidth)",
"claimed tickets (bandwidth)",
"remaining tickets (bandwidth)",
"ticket size",
]);
for ticketbook in &self.0 {
table.add_row(ticketbook.table_row());
}
writeln!(f, "{table}")?;
Ok(())
}
}
#[cfg_attr(feature = "cli", derive(clap::Args))]
#[derive(Debug, Clone)]
pub struct CommonShowTicketbooksArgs {
/// Id of client that is going to display the ticketbook information
#[cfg_attr(feature = "cli", clap(long))]
pub id: String,
}
pub async fn show_ticketbooks<C, A>(args: A) -> Result<AvailableTicketbooks, C::Error>
where
A: AsRef<CommonShowTicketbooksArgs>,
C: CliClient,
{
let common_args = args.as_ref();
let id = &common_args.id;
let config = C::try_load_current_config(id).await?;
let paths = config.common_paths();
let credentials_store =
nym_credential_storage::initialise_persistent_storage(&paths.credentials_database).await;
let ticketbooks = credentials_store
.get_ticketbooks_info()
.await
.map_err(|err| ClientCoreError::CredentialStoreError {
source: Box::new(err),
})?;
Ok(AvailableTicketbooks(
ticketbooks.into_iter().map(Into::into).collect(),
))
}
@@ -6,6 +6,7 @@ pub mod client_import_credential;
pub mod client_init;
pub mod client_list_gateways;
pub mod client_run;
pub mod client_show_ticketbooks;
pub mod client_switch_gateway;
pub mod traits;
mod types;
@@ -3,10 +3,12 @@
use async_trait::async_trait;
use log::{debug, error};
use nym_credential_storage::storage::Storage as CredentialStorage;
use nym_crypto::asymmetric::identity;
use nym_gateway_client::GatewayClient;
pub use nym_gateway_client::{GatewayPacketRouter, PacketRouter};
use nym_sphinx::forwarding::packet::MixPacket;
use nym_validator_client::nyxd::contract_traits::DkgQueryClient;
use std::fmt::Debug;
use std::os::raw::c_int as RawFd;
use thiserror::Error;
@@ -111,8 +113,9 @@ impl<C, St> RemoteGateway<C, St> {
impl<C, St> GatewayTransceiver for RemoteGateway<C, St>
where
C: Send,
St: Send,
C: DkgQueryClient + Send + Sync,
St: CredentialStorage,
<St as CredentialStorage>::StorageError: Send + Sync + 'static,
{
fn gateway_identity(&self) -> identity::PublicKey {
self.gateway_client.gateway_identity()
@@ -126,8 +129,9 @@ where
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl<C, St> GatewaySender for RemoteGateway<C, St>
where
C: Send,
St: Send,
C: DkgQueryClient + Send + Sync,
St: CredentialStorage,
<St as CredentialStorage>::StorageError: Send + Sync + 'static,
{
async fn send_mix_packet(&mut self, packet: MixPacket) -> Result<(), ErasedGatewayError> {
self.gateway_client
+5
View File
@@ -63,6 +63,11 @@ pub enum ClientCoreError {
source: Box<dyn Error + Send + Sync>,
},
#[error("experienced a failure with our credentials storage: {source}")]
CredentialStoreError {
source: Box<dyn Error + Send + Sync>,
},
#[error("the gateway id is invalid - {0}")]
UnableToCreatePublicKeyFromGatewayId(Ed25519RecoveryError),
+61 -56
View File
@@ -23,12 +23,12 @@ use nym_gateway_requests::{
BinaryRequest, ClientControlRequest, ServerResponse, CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION,
CURRENT_PROTOCOL_VERSION,
};
use nym_network_defaults::{REMAINING_BANDWIDTH_THRESHOLD, TOKENS_TO_BURN};
use nym_network_defaults::REMAINING_BANDWIDTH_THRESHOLD;
use nym_sphinx::forwarding::packet::MixPacket;
use nym_task::TaskClient;
use nym_validator_client::nyxd::contract_traits::DkgQueryClient;
use rand::rngs::OsRng;
use std::sync::atomic::{AtomicI64, Ordering};
use std::sync::Arc;
use std::time::Duration;
use tungstenite::protocol::Message;
@@ -83,7 +83,7 @@ impl GatewayConfig {
pub struct GatewayClient<C, St = EphemeralCredentialStorage> {
authenticated: bool,
disabled_credentials_mode: bool,
bandwidth_remaining: i64,
bandwidth_remaining: Arc<AtomicI64>,
gateway_address: String,
gateway_identity: identity::PublicKey,
local_identity: Arc<identity::KeyPair>,
@@ -122,7 +122,7 @@ impl<C, St> GatewayClient<C, St> {
GatewayClient {
authenticated: false,
disabled_credentials_mode: true,
bandwidth_remaining: 0,
bandwidth_remaining: Arc::new(AtomicI64::new(0)),
gateway_address: config.gateway_listener,
gateway_identity: config.gateway_identity,
local_identity,
@@ -182,7 +182,7 @@ impl<C, St> GatewayClient<C, St> {
}
pub fn remaining_bandwidth(&self) -> i64 {
self.bandwidth_remaining
self.bandwidth_remaining.load(Ordering::Acquire)
}
#[cfg(not(target_arch = "wasm32"))]
@@ -259,7 +259,7 @@ impl<C, St> GatewayClient<C, St> {
self.authenticated = false;
for i in 1..self.reconnection_attempts {
info!("attempt {}...", i);
info!("reconnection attempt {}...", i);
if self.try_reconnect().await.is_ok() {
info!("managed to reconnect!");
return Ok(());
@@ -269,7 +269,7 @@ impl<C, St> GatewayClient<C, St> {
}
// final attempt (done separately to be able to return a proper error)
info!("attempt {}", self.reconnection_attempts);
info!("reconnection attempt {}", self.reconnection_attempts);
match self.try_reconnect().await {
Ok(_) => {
info!("managed to reconnect!");
@@ -537,7 +537,8 @@ impl<C, St> GatewayClient<C, St> {
} => {
self.check_gateway_protocol(protocol_version)?;
self.authenticated = status;
self.bandwidth_remaining = bandwidth_remaining;
self.bandwidth_remaining
.store(bandwidth_remaining, Ordering::Release);
self.negotiated_protocol = protocol_version;
log::debug!("authenticated: {status}, bandwidth remaining: {bandwidth_remaining}");
self.task_client.send_status_msg(Box::new(
@@ -576,44 +577,54 @@ impl<C, St> GatewayClient<C, St> {
}
}
async fn claim_coconut_bandwidth(
async fn claim_ecash_bandwidth(
&mut self,
credential: CredentialSpendingData,
) -> Result<(), GatewayClientError> {
let mut rng = OsRng;
let iv = IV::new_random(&mut rng);
let msg = ClientControlRequest::new_enc_coconut_bandwidth_credential_v2(
let msg = ClientControlRequest::new_enc_ecash_credential(
credential,
self.shared_key.as_ref().unwrap(),
iv,
)
.into();
self.bandwidth_remaining = match self.send_websocket_message(msg).await? {
let bandwidth_remaining = match self.send_websocket_message(msg).await? {
ServerResponse::Bandwidth { available_total } => Ok(available_total),
ServerResponse::Error { message } => Err(GatewayClientError::GatewayError(message)),
_ => Err(GatewayClientError::UnexpectedResponse),
}?;
self.bandwidth_remaining
.store(bandwidth_remaining, Ordering::Relaxed);
Ok(())
}
async fn try_claim_testnet_bandwidth(&mut self) -> Result<(), GatewayClientError> {
let msg = ClientControlRequest::ClaimFreeTestnetBandwidth.into();
self.bandwidth_remaining = match self.send_websocket_message(msg).await? {
let bandwidth_remaining = match self.send_websocket_message(msg).await? {
ServerResponse::Bandwidth { available_total } => Ok(available_total),
ServerResponse::Error { message } => Err(GatewayClientError::GatewayError(message)),
_ => Err(GatewayClientError::UnexpectedResponse),
}?;
self.bandwidth_remaining
.store(bandwidth_remaining, Ordering::Release);
Ok(())
}
fn unchecked_bandwidth_controller(&self) -> &BandwidthController<C, St> {
self.bandwidth_controller.as_ref().unwrap()
}
pub async fn claim_bandwidth(&mut self) -> Result<(), GatewayClientError>
where
C: DkgQueryClient + Send + Sync,
St: CredentialStorage,
<St as CredentialStorage>::StorageError: Send + Sync + 'static,
{
// TODO: make it configurable
const TICKETS_TO_SPEND: u32 = 1;
if !self.authenticated {
return Err(GatewayClientError::NotAuthenticated);
}
@@ -641,49 +652,42 @@ impl<C, St> GatewayClient<C, St> {
negotiated_protocol: Some(gateway_protocol),
});
}
let gateway_id = self.gateway_identity().to_base58_string();
let prepared_credential = self
.bandwidth_controller
.as_ref()
.unwrap()
.prepare_bandwidth_credential(&gateway_id)
.unchecked_bandwidth_controller()
.prepare_ecash_ticket(self.gateway_identity.to_bytes(), TICKETS_TO_SPEND)
.await?;
self.claim_coconut_bandwidth(prepared_credential.data)
.await?;
self.bandwidth_controller
.as_ref()
.unwrap()
.consume_credential(prepared_credential.credential_id, &gateway_id)
.await?;
Ok(())
}
fn estimate_required_bandwidth(&self, packets: &[MixPacket]) -> i64 {
packets
.iter()
.map(|packet| packet.packet().len())
.sum::<usize>() as i64
match self.claim_ecash_bandwidth(prepared_credential.data).await {
Ok(_) => Ok(()),
Err(err) => {
error!("failed to claim ecash bandwidth with the gateway... attempting to revert storage withdrawal");
self.unchecked_bandwidth_controller()
.attempt_revert_ticket_usage(prepared_credential.metadata)
.await?;
Err(err)
}
}
}
pub async fn batch_send_mix_packets(
&mut self,
packets: Vec<MixPacket>,
) -> Result<(), GatewayClientError> {
) -> Result<(), GatewayClientError>
where
C: DkgQueryClient + Send + Sync,
St: CredentialStorage,
<St as CredentialStorage>::StorageError: Send + Sync + 'static,
{
debug!("Sending {} mix packets", packets.len());
if !self.authenticated {
return Err(GatewayClientError::NotAuthenticated);
}
if self.estimate_required_bandwidth(&packets) > self.bandwidth_remaining {
return Err(GatewayClientError::NotEnoughBandwidth(
self.estimate_required_bandwidth(&packets),
self.bandwidth_remaining,
));
let bandwidth_remaining = self.bandwidth_remaining.load(Ordering::Acquire);
if bandwidth_remaining < REMAINING_BANDWIDTH_THRESHOLD {
self.claim_bandwidth().await?;
}
if !self.connection.is_established() {
return Err(GatewayClientError::ConnectionNotEstablished);
}
@@ -742,19 +746,20 @@ impl<C, St> GatewayClient<C, St> {
}
// TODO: possibly make responses optional
pub async fn send_mix_packet(
&mut self,
mix_packet: MixPacket,
) -> Result<(), GatewayClientError> {
pub async fn send_mix_packet(&mut self, mix_packet: MixPacket) -> Result<(), GatewayClientError>
where
C: DkgQueryClient + Send + Sync,
St: CredentialStorage,
<St as CredentialStorage>::StorageError: Send + Sync + 'static,
{
if !self.authenticated {
return Err(GatewayClientError::NotAuthenticated);
}
if (mix_packet.packet().len() as i64) > self.bandwidth_remaining {
return Err(GatewayClientError::NotEnoughBandwidth(
mix_packet.packet().len() as i64,
self.bandwidth_remaining,
));
let bandwidth_remaining = self.bandwidth_remaining.load(Ordering::Acquire);
if bandwidth_remaining < REMAINING_BANDWIDTH_THRESHOLD {
self.claim_bandwidth().await?;
}
if !self.connection.is_established() {
return Err(GatewayClientError::ConnectionNotEstablished);
}
@@ -808,6 +813,7 @@ impl<C, St> GatewayClient<C, St> {
.as_ref()
.expect("no shared key present even though we're authenticated!"),
),
self.bandwidth_remaining.clone(),
self.task_client.clone(),
)
}
@@ -848,10 +854,9 @@ impl<C, St> GatewayClient<C, St> {
self.establish_connection().await?;
}
let shared_key = self.perform_initial_authentication().await?;
if self.bandwidth_remaining < REMAINING_BANDWIDTH_THRESHOLD {
info!("Claiming more bandwidth for your tokens. This will use {} token(s) from your wallet. \
Stop the process now if you don't want that to happen.", TOKENS_TO_BURN);
let bandwidth_remaining = self.bandwidth_remaining.load(Ordering::Acquire);
if bandwidth_remaining < REMAINING_BANDWIDTH_THRESHOLD {
info!("Claiming more bandwidth with existing credentials. Stop the process now if you don't want that to happen.");
self.claim_bandwidth().await?;
}
@@ -888,7 +893,7 @@ impl GatewayClient<InitOnly, EphemeralCredentialStorage> {
GatewayClient {
authenticated: false,
disabled_credentials_mode: true,
bandwidth_remaining: 0,
bandwidth_remaining: Arc::new(AtomicI64::new(0)),
gateway_address: gateway_listener.to_string(),
gateway_identity,
local_identity,
@@ -79,6 +79,7 @@ impl PartiallyDelegated {
fn recover_received_plaintexts(
ws_msgs: Vec<Message>,
shared_key: &SharedKeys,
bandwidth_remaining: Arc<AtomicI64>,
) -> Result<Vec<Vec<u8>>, GatewayClientError> {
let mut plaintexts = Vec::with_capacity(ws_msgs.len());
for ws_msg in ws_msgs {
@@ -104,7 +105,11 @@ impl PartiallyDelegated {
{
ServerResponse::Send {
remaining_bandwidth,
} => maybe_log_bandwidth(remaining_bandwidth),
} => {
maybe_log_bandwidth(remaining_bandwidth);
bandwidth_remaining
.store(remaining_bandwidth, std::sync::atomic::Ordering::Release)
}
ServerResponse::Error { message } => {
error!("gateway failure: {message}");
return Err(GatewayClientError::GatewayError(message));
@@ -129,8 +134,10 @@ impl PartiallyDelegated {
ws_msgs: Vec<Message>,
packet_router: &PacketRouter,
shared_key: &SharedKeys,
bandwidth_remaining: Arc<AtomicI64>,
) -> Result<(), GatewayClientError> {
let plaintexts = Self::recover_received_plaintexts(ws_msgs, shared_key)?;
let plaintexts =
Self::recover_received_plaintexts(ws_msgs, shared_key, bandwidth_remaining)?;
packet_router.route_received(plaintexts)
}
@@ -138,6 +145,7 @@ impl PartiallyDelegated {
conn: WsConn,
mut packet_router: PacketRouter,
shared_key: Arc<SharedKeys>,
bandwidth_remaining: Arc<AtomicI64>,
mut shutdown: TaskClient,
) -> Self {
// when called for, it NEEDS TO yield back the stream so that we could merge it and
@@ -169,7 +177,7 @@ impl PartiallyDelegated {
Ok(msgs) => msgs
};
if let Err(err) = Self::route_socket_messages(ws_msgs, &packet_router, shared_key.as_ref()) {
if let Err(err) = Self::route_socket_messages(ws_msgs, &packet_router, shared_key.as_ref(), bandwidth_remaining.clone()) {
log::error!("Route socket messages failed: {err}");
break Err(err)
}
@@ -17,6 +17,7 @@ nym-contracts-common = { path = "../../cosmwasm-smart-contracts/contracts-common
nym-mixnet-contract-common = { path = "../../cosmwasm-smart-contracts/mixnet-contract" }
nym-vesting-contract-common = { path = "../../cosmwasm-smart-contracts/vesting-contract" }
nym-coconut-bandwidth-contract-common = { path = "../../cosmwasm-smart-contracts/coconut-bandwidth-contract" }
nym-ecash-contract-common = { path = "../../cosmwasm-smart-contracts/ecash-contract" }
nym-multisig-contract-common = { path = "../../cosmwasm-smart-contracts/multisig-contract" }
nym-group-contract-common = { path = "../../cosmwasm-smart-contracts/group-contract" }
serde = { workspace = true, features = ["derive"] }
@@ -26,9 +27,10 @@ thiserror = { workspace = true }
log = { workspace = true }
url = { workspace = true, features = ["serde"] }
tokio = { workspace = true, features = ["sync", "time"] }
time = { workspace = true, features = ["formatting"] }
futures = { workspace = true }
nym-coconut = { path = "../../nymcoconut" }
nym-compact-ecash = { path = "../../nym_offline_compact_ecash" }
nym-network-defaults = { path = "../../network-defaults" }
nym-api-requests = { path = "../../../nym-api/nym-api-requests" }
@@ -8,10 +8,14 @@ use crate::{
nym_api, DirectSigningReqwestRpcValidatorClient, QueryReqwestRpcValidatorClient,
ReqwestRpcClient, ValidatorClientError,
};
use nym_api_requests::coconut::models::FreePassNonceResponse;
use nym_api_requests::coconut::{
BlindSignRequestBody, BlindedSignatureResponse, FreePassRequest, VerifyCredentialBody,
VerifyCredentialResponse,
use nym_api_requests::ecash::models::{
AggregatedCoinIndicesSignatureResponse, AggregatedExpirationDateSignatureResponse,
BatchRedeemTicketsBody, EcashBatchTicketRedemptionResponse, EcashTicketVerificationResponse,
SpentCredentialsResponse, VerifyEcashTicketBody,
};
use nym_api_requests::ecash::{
BlindSignRequestBody, BlindedSignatureResponse, PartialCoinIndicesSignatureResponse,
PartialExpirationDateSignatureResponse, VerificationKeyResponse,
};
use nym_api_requests::models::{DescribedGateway, MixNodeBondAnnotated};
use nym_api_requests::models::{
@@ -19,8 +23,10 @@ use nym_api_requests::models::{
RewardEstimationResponse, StakeSaturationResponse,
};
use nym_api_requests::nym_nodes::SkimmedNode;
use nym_coconut_dkg_common::types::EpochId;
use nym_http_api_client::UserAgent;
use nym_network_defaults::NymNetworkDetails;
use time::Date;
use url::Url;
pub use crate::nym_api::NymApiClientExt;
@@ -29,7 +35,7 @@ pub use nym_mixnet_contract_common::{
};
// re-export the type to not break existing imports
pub use crate::coconut::CoconutApiClient;
pub use crate::coconut::EcashApiClient;
#[cfg(feature = "http-client")]
use crate::rpc::http_client;
@@ -375,24 +381,73 @@ impl NymApiClient {
Ok(self.nym_api.blind_sign(request_body).await?)
}
pub async fn verify_bandwidth_credential(
pub async fn verify_ecash_ticket(
&self,
request_body: &VerifyCredentialBody,
) -> Result<VerifyCredentialResponse, ValidatorClientError> {
request_body: &VerifyEcashTicketBody,
) -> Result<EcashTicketVerificationResponse, ValidatorClientError> {
Ok(self.nym_api.verify_ecash_ticket(request_body).await?)
}
pub async fn batch_redeem_ecash_tickets(
&self,
request_body: &BatchRedeemTicketsBody,
) -> Result<EcashBatchTicketRedemptionResponse, ValidatorClientError> {
Ok(self
.nym_api
.verify_bandwidth_credential(request_body)
.batch_redeem_ecash_tickets(request_body)
.await?)
}
pub async fn free_pass_nonce(&self) -> Result<FreePassNonceResponse, ValidatorClientError> {
Ok(self.nym_api.free_pass_nonce().await?)
pub async fn spent_credentials_filter(
&self,
) -> Result<SpentCredentialsResponse, ValidatorClientError> {
Ok(self.nym_api.double_spending_filter_v1().await?)
}
pub async fn issue_free_pass_credential(
pub async fn partial_expiration_date_signatures(
&self,
request: &FreePassRequest,
) -> Result<BlindedSignatureResponse, ValidatorClientError> {
Ok(self.nym_api.free_pass(request).await?)
expiration_date: Option<Date>,
) -> Result<PartialExpirationDateSignatureResponse, ValidatorClientError> {
Ok(self
.nym_api
.partial_expiration_date_signatures(expiration_date)
.await?)
}
pub async fn partial_coin_indices_signatures(
&self,
epoch_id: Option<EpochId>,
) -> Result<PartialCoinIndicesSignatureResponse, ValidatorClientError> {
Ok(self
.nym_api
.partial_coin_indices_signatures(epoch_id)
.await?)
}
pub async fn global_expiration_date_signatures(
&self,
expiration_date: Option<Date>,
) -> Result<AggregatedExpirationDateSignatureResponse, ValidatorClientError> {
Ok(self
.nym_api
.global_expiration_date_signatures(expiration_date)
.await?)
}
pub async fn global_coin_indices_signatures(
&self,
epoch_id: Option<EpochId>,
) -> Result<AggregatedCoinIndicesSignatureResponse, ValidatorClientError> {
Ok(self
.nym_api
.global_coin_indices_signatures(epoch_id)
.await?)
}
pub async fn master_verification_key(
&self,
epoch_id: Option<EpochId>,
) -> Result<VerificationKeyResponse, ValidatorClientError> {
Ok(self.nym_api.master_verification_key(epoch_id).await?)
}
}
@@ -4,26 +4,40 @@
use crate::nyxd::contract_traits::{DkgQueryClient, PagedDkgQueryClient};
use crate::nyxd::error::NyxdError;
use crate::NymApiClient;
use nym_coconut::{Base58, CoconutError, VerificationKey};
use nym_coconut_dkg_common::types::{EpochId, NodeIndex};
use nym_coconut_dkg_common::verification_key::ContractVKShare;
use nym_compact_ecash::error::CompactEcashError;
use nym_compact_ecash::{Base58, VerificationKeyAuth};
use std::fmt::{Display, Formatter};
use thiserror::Error;
use url::Url;
// TODO: it really doesn't feel like this should live in this crate.
#[derive(Clone)]
pub struct CoconutApiClient {
pub struct EcashApiClient {
pub api_client: NymApiClient,
pub verification_key: VerificationKey,
pub verification_key: VerificationKeyAuth,
pub node_id: NodeIndex,
pub cosmos_address: cosmrs::AccountId,
}
impl Display for EcashApiClient {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"[id: {}] {} @ {}",
self.node_id,
self.cosmos_address,
self.api_client.api_url()
)
}
}
// TODO: this should be using the coconut error
// (which is in different crate; perhaps this client should be moved there?)
#[derive(Debug, Error)]
pub enum CoconutApiError {
pub enum EcashApiError {
// TODO: ask @BN whether this is a correct error message
#[error("the provided key share hasn't been verified")]
UnverifiedShare,
@@ -43,7 +57,7 @@ pub enum CoconutApiError {
#[error("the provided verification key is malformed: {source}")]
MalformedVerificationKey {
#[from]
source: CoconutError,
source: CompactEcashError,
},
#[error("the provided account address is malformed: {source}")]
@@ -53,29 +67,29 @@ pub enum CoconutApiError {
},
}
impl TryFrom<ContractVKShare> for CoconutApiClient {
type Error = CoconutApiError;
impl TryFrom<ContractVKShare> for EcashApiClient {
type Error = EcashApiError;
fn try_from(share: ContractVKShare) -> Result<Self, Self::Error> {
if !share.verified {
return Err(CoconutApiError::UnverifiedShare);
return Err(EcashApiError::UnverifiedShare);
}
let url_address = Url::parse(&share.announce_address)?;
Ok(CoconutApiClient {
Ok(EcashApiClient {
api_client: NymApiClient::new(url_address),
verification_key: VerificationKey::try_from_bs58(&share.share)?,
verification_key: VerificationKeyAuth::try_from_bs58(&share.share)?,
node_id: share.node_index,
cosmos_address: share.owner.as_str().parse()?,
})
}
}
pub async fn all_coconut_api_clients<C>(
pub async fn all_ecash_api_clients<C>(
client: &C,
epoch_id: EpochId,
) -> Result<Vec<CoconutApiClient>, CoconutApiError>
) -> Result<Vec<EcashApiClient>, EcashApiError>
where
C: DkgQueryClient + Sync + Send,
{
@@ -7,7 +7,7 @@ use thiserror::Error;
#[derive(Error, Debug)]
pub enum ValidatorClientError {
#[error("nym api request failed - {source}")]
#[error("nym api request failed: {source}")]
NymAPIError {
#[from]
source: nym_api::error::NymAPIError,
@@ -19,7 +19,7 @@ pub enum ValidatorClientError {
#[error("One of the provided URLs was malformed - {0}")]
MalformedUrlProvided(#[from] url::ParseError),
#[error("nyxd request failed - {0}")]
#[error("nyxd request failed: {0}")]
NyxdError(#[from] crate::nyxd::error::NyxdError),
#[error("No validator API url has been provided")]
@@ -15,7 +15,7 @@ pub use crate::error::ValidatorClientError;
pub use crate::rpc::reqwest::ReqwestRpcClient;
pub use crate::signing::direct_wallet::DirectSecp256k1HdWallet;
pub use client::NymApiClient;
pub use client::{Client, CoconutApiClient, Config};
pub use client::{Client, Config, EcashApiClient};
pub use nym_api_requests::*;
pub use nym_http_api_client::UserAgent;
@@ -2,16 +2,33 @@
// SPDX-License-Identifier: Apache-2.0
use crate::nym_api::error::NymAPIError;
use crate::nym_api::routes::{CORE_STATUS_COUNT, SINCE_ARG};
use crate::nym_api::routes::{ecash, CORE_STATUS_COUNT, SINCE_ARG};
use async_trait::async_trait;
use nym_api_requests::ecash::models::{
AggregatedCoinIndicesSignatureResponse, AggregatedExpirationDateSignatureResponse,
BatchRedeemTicketsBody, EcashBatchTicketRedemptionResponse, EcashTicketVerificationResponse,
VerifyEcashTicketBody,
};
use nym_api_requests::nym_nodes::{CachedNodesResponse, SkimmedNode};
use nym_http_api_client::{ApiClient, NO_PARAMS};
use nym_mixnet_contract_common::mixnode::MixNodeDetails;
use nym_mixnet_contract_common::{GatewayBond, IdentityKeyRef, MixId};
use time::format_description::BorrowedFormatItem;
use time::Date;
pub mod error;
pub mod routes;
use nym_api_requests::ecash::VerificationKeyResponse;
pub use nym_api_requests::{
coconut::{
ecash::{
models::{
EpochCredentialsResponse, IssuedCredential, IssuedCredentialBody,
IssuedCredentialResponse, IssuedCredentialsResponse,
IssuedCredentialResponse, IssuedCredentialsResponse, SpentCredentialsResponse,
},
BlindSignRequestBody, BlindedSignatureResponse, CredentialsRequestBody,
VerifyCredentialBody, VerifyCredentialResponse,
PartialCoinIndicesSignatureResponse, PartialExpirationDateSignatureResponse,
VerifyEcashCredentialBody,
},
models::{
ComputeRewardEstParam, DescribedGateway, GatewayBondAnnotated, GatewayCoreStatusResponse,
@@ -22,18 +39,12 @@ pub use nym_api_requests::{
},
};
pub use nym_coconut_dkg_common::types::EpochId;
use nym_http_api_client::{ApiClient, NO_PARAMS};
use nym_mixnet_contract_common::mixnode::MixNodeDetails;
use nym_mixnet_contract_common::{GatewayBond, IdentityKeyRef, MixId};
pub mod error;
pub mod routes;
use nym_api_requests::coconut::models::FreePassNonceResponse;
use nym_api_requests::coconut::FreePassRequest;
use nym_api_requests::nym_nodes::{CachedNodesResponse, SkimmedNode};
pub use nym_http_api_client::Client;
pub fn rfc_3339_date() -> Vec<BorrowedFormatItem<'static>> {
time::format_description::parse("[year]-[month]-[day]").unwrap()
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait NymApiClientExt: ApiClient {
@@ -420,36 +431,6 @@ pub trait NymApiClientExt: ApiClient {
.await
}
async fn free_pass_nonce(&self) -> Result<FreePassNonceResponse, NymAPIError> {
self.get_json(
&[
routes::API_VERSION,
routes::COCONUT_ROUTES,
routes::BANDWIDTH,
routes::COCONUT_FREE_PASS_NONCE,
],
NO_PARAMS,
)
.await
}
async fn free_pass(
&self,
request: &FreePassRequest,
) -> Result<BlindedSignatureResponse, NymAPIError> {
self.post_json(
&[
routes::API_VERSION,
routes::COCONUT_ROUTES,
routes::BANDWIDTH,
routes::COCONUT_FREE_PASS,
],
NO_PARAMS,
request,
)
.await
}
async fn blind_sign(
&self,
request_body: &BlindSignRequestBody,
@@ -457,9 +438,8 @@ pub trait NymApiClientExt: ApiClient {
self.post_json(
&[
routes::API_VERSION,
routes::COCONUT_ROUTES,
routes::BANDWIDTH,
routes::COCONUT_BLIND_SIGN,
routes::ECASH_ROUTES,
routes::ECASH_BLIND_SIGN,
],
NO_PARAMS,
request_body,
@@ -467,16 +447,15 @@ pub trait NymApiClientExt: ApiClient {
.await
}
async fn verify_bandwidth_credential(
async fn verify_ecash_ticket(
&self,
request_body: &VerifyCredentialBody,
) -> Result<VerifyCredentialResponse, NymAPIError> {
request_body: &VerifyEcashTicketBody,
) -> Result<EcashTicketVerificationResponse, NymAPIError> {
self.post_json(
&[
routes::API_VERSION,
routes::COCONUT_ROUTES,
routes::BANDWIDTH,
routes::COCONUT_VERIFY_BANDWIDTH_CREDENTIAL,
routes::ECASH_ROUTES,
routes::VERIFY_ECASH_TICKET,
],
NO_PARAMS,
request_body,
@@ -484,6 +463,139 @@ pub trait NymApiClientExt: ApiClient {
.await
}
async fn batch_redeem_ecash_tickets(
&self,
request_body: &BatchRedeemTicketsBody,
) -> Result<EcashBatchTicketRedemptionResponse, NymAPIError> {
self.post_json(
&[
routes::API_VERSION,
routes::ECASH_ROUTES,
routes::BATCH_REDEEM_ECASH_TICKETS,
],
NO_PARAMS,
request_body,
)
.await
}
async fn double_spending_filter_v1(&self) -> Result<SpentCredentialsResponse, NymAPIError> {
self.get_json(
&[
routes::API_VERSION,
routes::ECASH_ROUTES,
routes::DOUBLE_SPENDING_FILTER_V1,
],
NO_PARAMS,
)
.await
}
async fn partial_expiration_date_signatures(
&self,
expiration_date: Option<Date>,
) -> Result<PartialExpirationDateSignatureResponse, NymAPIError> {
let params = match expiration_date {
None => Vec::new(),
Some(exp) => vec![(
ecash::EXPIRATION_DATE_PARAM,
exp.format(&rfc_3339_date()).unwrap(),
)],
};
self.get_json(
&[
routes::API_VERSION,
routes::ECASH_ROUTES,
routes::PARTIAL_EXPIRATION_DATE_SIGNATURES,
],
&params,
)
.await
}
async fn partial_coin_indices_signatures(
&self,
epoch_id: Option<EpochId>,
) -> Result<PartialCoinIndicesSignatureResponse, NymAPIError> {
let params = match epoch_id {
None => Vec::new(),
Some(epoch_id) => vec![(ecash::EPOCH_ID_PARAM, epoch_id.to_string())],
};
self.get_json(
&[
routes::API_VERSION,
routes::ECASH_ROUTES,
routes::PARTIAL_COIN_INDICES_SIGNATURES,
],
&params,
)
.await
}
async fn global_expiration_date_signatures(
&self,
expiration_date: Option<Date>,
) -> Result<AggregatedExpirationDateSignatureResponse, NymAPIError> {
let params = match expiration_date {
None => Vec::new(),
Some(exp) => vec![(
ecash::EXPIRATION_DATE_PARAM,
exp.format(&rfc_3339_date()).unwrap(),
)],
};
self.get_json(
&[
routes::API_VERSION,
routes::ECASH_ROUTES,
routes::GLOBAL_EXPIRATION_DATE_SIGNATURES,
],
&params,
)
.await
}
async fn global_coin_indices_signatures(
&self,
epoch_id: Option<EpochId>,
) -> Result<AggregatedCoinIndicesSignatureResponse, NymAPIError> {
let params = match epoch_id {
None => Vec::new(),
Some(epoch_id) => vec![(ecash::EPOCH_ID_PARAM, epoch_id.to_string())],
};
self.get_json(
&[
routes::API_VERSION,
routes::ECASH_ROUTES,
routes::GLOBAL_COIN_INDICES_SIGNATURES,
],
&params,
)
.await
}
async fn master_verification_key(
&self,
epoch_id: Option<EpochId>,
) -> Result<VerificationKeyResponse, NymAPIError> {
let params = match epoch_id {
None => Vec::new(),
Some(epoch_id) => vec![(ecash::EPOCH_ID_PARAM, epoch_id.to_string())],
};
self.get_json(
&[
routes::API_VERSION,
routes::ECASH_ROUTES,
routes::ecash::MASTER_VERIFICATION_KEY,
],
&params,
)
.await
}
async fn epoch_credentials(
&self,
dkg_epoch: EpochId,
@@ -491,9 +603,8 @@ pub trait NymApiClientExt: ApiClient {
self.get_json(
&[
routes::API_VERSION,
routes::COCONUT_ROUTES,
routes::BANDWIDTH,
routes::COCONUT_EPOCH_CREDENTIALS,
routes::ECASH_ROUTES,
routes::ECASH_EPOCH_CREDENTIALS,
&dkg_epoch.to_string(),
],
NO_PARAMS,
@@ -508,9 +619,8 @@ pub trait NymApiClientExt: ApiClient {
self.get_json(
&[
routes::API_VERSION,
routes::COCONUT_ROUTES,
routes::BANDWIDTH,
routes::COCONUT_ISSUED_CREDENTIAL,
routes::ECASH_ROUTES,
routes::ECASH_ISSUED_CREDENTIAL,
&credential_id.to_string(),
],
NO_PARAMS,
@@ -525,9 +635,8 @@ pub trait NymApiClientExt: ApiClient {
self.post_json(
&[
routes::API_VERSION,
routes::COCONUT_ROUTES,
routes::BANDWIDTH,
routes::COCONUT_ISSUED_CREDENTIALS,
routes::ECASH_ROUTES,
routes::ECASH_ISSUED_CREDENTIALS,
],
NO_PARAMS,
&CredentialsRequestBody {
@@ -12,16 +12,27 @@ pub const DETAILED: &str = "detailed";
pub const DETAILED_UNFILTERED: &str = "detailed-unfiltered";
pub const ACTIVE: &str = "active";
pub const REWARDED: &str = "rewarded";
pub const COCONUT_ROUTES: &str = "coconut";
pub const BANDWIDTH: &str = "bandwidth";
pub const DOUBLE_SPENDING_FILTER_V1: &str = "double-spending-filter-v1";
pub const COCONUT_FREE_PASS: &str = "free-pass";
pub const COCONUT_FREE_PASS_NONCE: &str = "free-pass-nonce";
pub const COCONUT_BLIND_SIGN: &str = "blind-sign";
pub const COCONUT_VERIFY_BANDWIDTH_CREDENTIAL: &str = "verify-bandwidth-credential";
pub const COCONUT_EPOCH_CREDENTIALS: &str = "epoch-credentials";
pub const COCONUT_ISSUED_CREDENTIAL: &str = "issued-credential";
pub const COCONUT_ISSUED_CREDENTIALS: &str = "issued-credentials";
pub const ECASH_ROUTES: &str = "ecash";
pub use ecash::*;
pub mod ecash {
pub const ECASH_BLIND_SIGN: &str = "blind-sign";
pub const VERIFY_ECASH_TICKET: &str = "verify-ecash-ticket";
pub const BATCH_REDEEM_ECASH_TICKETS: &str = "batch-redeem-ecash-tickets";
pub const PARTIAL_EXPIRATION_DATE_SIGNATURES: &str = "partial-expiration-date-signatures";
pub const GLOBAL_EXPIRATION_DATE_SIGNATURES: &str = "aggregated-expiration-date-signatures";
pub const PARTIAL_COIN_INDICES_SIGNATURES: &str = "partial-coin-indices-signatures";
pub const GLOBAL_COIN_INDICES_SIGNATURES: &str = "aggregated-coin-indices-signatures";
pub const MASTER_VERIFICATION_KEY: &str = "master-verification-key";
pub const ECASH_EPOCH_CREDENTIALS: &str = "epoch-credentials";
pub const ECASH_ISSUED_CREDENTIAL: &str = "issued-credential";
pub const ECASH_ISSUED_CREDENTIALS: &str = "issued-credentials";
pub const EXPIRATION_DATE_PARAM: &str = "expiration_date";
pub const EPOCH_ID_PARAM: &str = "epoch_id";
}
pub const STATUS_ROUTES: &str = "status";
pub const MIXNODE: &str = "mixnode";
@@ -1,100 +0,0 @@
// Copyright 2022-2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::collect_paged;
use crate::nyxd::contract_traits::NymContractsProvider;
use crate::nyxd::error::NyxdError;
use crate::nyxd::CosmWasmClient;
use async_trait::async_trait;
use nym_coconut_bandwidth_contract_common::msg::QueryMsg as CoconutBandwidthQueryMsg;
use nym_coconut_bandwidth_contract_common::spend_credential::{
PagedSpendCredentialResponse, SpendCredential, SpendCredentialResponse,
};
use serde::Deserialize;
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait CoconutBandwidthQueryClient {
async fn query_coconut_bandwidth_contract<T>(
&self,
query: CoconutBandwidthQueryMsg,
) -> Result<T, NyxdError>
where
for<'a> T: Deserialize<'a>;
async fn get_spent_credential(
&self,
blinded_serial_number: String,
) -> Result<SpendCredentialResponse, NyxdError> {
self.query_coconut_bandwidth_contract(CoconutBandwidthQueryMsg::GetSpentCredential {
blinded_serial_number,
})
.await
}
async fn get_all_spent_credential_paged(
&self,
start_after: Option<String>,
limit: Option<u32>,
) -> Result<PagedSpendCredentialResponse, NyxdError> {
self.query_coconut_bandwidth_contract(CoconutBandwidthQueryMsg::GetAllSpentCredentials {
limit,
start_after,
})
.await
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait PagedCoconutBandwidthQueryClient: CoconutBandwidthQueryClient {
async fn get_all_spent_credentials(&self) -> Result<Vec<SpendCredential>, NyxdError> {
collect_paged!(self, get_all_spent_credential_paged, spend_credentials)
}
}
#[async_trait]
impl<T> PagedCoconutBandwidthQueryClient for T where T: CoconutBandwidthQueryClient {}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl<C> CoconutBandwidthQueryClient for C
where
C: CosmWasmClient + NymContractsProvider + Send + Sync,
{
async fn query_coconut_bandwidth_contract<T>(
&self,
query: CoconutBandwidthQueryMsg,
) -> Result<T, NyxdError>
where
for<'a> T: Deserialize<'a>,
{
let coconut_bandwidth_contract_address = self
.coconut_bandwidth_contract_address()
.ok_or_else(|| NyxdError::unavailable_contract_address("coconut bandwidth contract"))?;
self.query_contract_smart(coconut_bandwidth_contract_address, &query)
.await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::nyxd::contract_traits::tests::IgnoreValue;
// it's enough that this compiles and clippy is happy about it
#[allow(dead_code)]
fn all_query_variants_are_covered<C: CoconutBandwidthQueryClient + Send + Sync>(
client: C,
msg: CoconutBandwidthQueryMsg,
) {
match msg {
CoconutBandwidthQueryMsg::GetSpentCredential {
blinded_serial_number,
} => client.get_spent_credential(blinded_serial_number).ignore(),
CoconutBandwidthQueryMsg::GetAllSpentCredentials { limit, start_after } => client
.get_all_spent_credential_paged(start_after, limit)
.ignore(),
};
}
}
@@ -1,153 +0,0 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::nyxd::contract_traits::NymContractsProvider;
use crate::nyxd::cosmwasm_client::types::ExecuteResult;
use crate::nyxd::error::NyxdError;
use crate::nyxd::{Coin, Fee, SigningCosmWasmClient};
use crate::signing::signer::OfflineSigner;
use async_trait::async_trait;
use nym_coconut_bandwidth_contract_common::spend_credential::SpendCredentialData;
use nym_coconut_bandwidth_contract_common::{
deposit::DepositData, msg::ExecuteMsg as CoconutBandwidthExecuteMsg,
};
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait CoconutBandwidthSigningClient {
async fn execute_coconut_bandwidth_contract(
&self,
fee: Option<Fee>,
msg: CoconutBandwidthExecuteMsg,
memo: String,
funds: Vec<Coin>,
) -> Result<ExecuteResult, NyxdError>;
async fn deposit(
&self,
amount: Coin,
info: String,
verification_key: String,
encryption_key: String,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
let req = CoconutBandwidthExecuteMsg::DepositFunds {
data: DepositData::new(info, verification_key, encryption_key),
};
self.execute_coconut_bandwidth_contract(
fee,
req,
"CoconutBandwidth::Deposit".to_string(),
vec![amount],
)
.await
}
async fn spend_credential(
&self,
funds: Coin,
blinded_serial_number: String,
gateway_cosmos_address: String,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
let req = CoconutBandwidthExecuteMsg::SpendCredential {
data: SpendCredentialData::new(
funds.into(),
blinded_serial_number,
gateway_cosmos_address,
),
};
self.execute_coconut_bandwidth_contract(
fee,
req,
"CoconutBandwidth::SpendCredential".to_string(),
vec![],
)
.await
}
async fn release_funds(
&self,
amount: Coin,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
self.execute_coconut_bandwidth_contract(
fee,
CoconutBandwidthExecuteMsg::ReleaseFunds {
funds: amount.into(),
},
"CoconutBandwidth::ReleaseFunds".to_string(),
vec![],
)
.await
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl<C> CoconutBandwidthSigningClient for C
where
C: SigningCosmWasmClient + NymContractsProvider + Sync,
NyxdError: From<<Self as OfflineSigner>::Error>,
{
async fn execute_coconut_bandwidth_contract(
&self,
fee: Option<Fee>,
msg: CoconutBandwidthExecuteMsg,
memo: String,
funds: Vec<Coin>,
) -> Result<ExecuteResult, NyxdError> {
let coconut_bandwidth_contract_address = self
.coconut_bandwidth_contract_address()
.ok_or_else(|| NyxdError::unavailable_contract_address("coconut bandwidth contract"))?;
let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier())));
let signer_address = &self.signer_addresses()?[0];
self.execute(
signer_address,
coconut_bandwidth_contract_address,
&msg,
fee,
memo,
funds,
)
.await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::nyxd::contract_traits::tests::{mock_coin, IgnoreValue};
// it's enough that this compiles and clippy is happy about it
#[allow(dead_code)]
fn all_execute_variants_are_covered<C: CoconutBandwidthSigningClient + Send + Sync>(
client: C,
msg: CoconutBandwidthExecuteMsg,
) {
match msg {
CoconutBandwidthExecuteMsg::DepositFunds { data } => client
.deposit(
mock_coin(),
data.deposit_info().to_string(),
data.identity_key().to_string(),
data.encryption_key().to_string(),
None,
)
.ignore(),
CoconutBandwidthExecuteMsg::SpendCredential { data } => client
.spend_credential(
mock_coin(),
data.blinded_serial_number().to_string(),
data.gateway_cosmos_address().to_string(),
None,
)
.ignore(),
CoconutBandwidthExecuteMsg::ReleaseFunds { funds } => {
client.release_funds(funds.into(), None).ignore()
}
};
}
}
@@ -51,6 +51,11 @@ pub trait DkgQueryClient {
self.query_dkg_contract(request).await
}
async fn get_epoch_threshold(&self, epoch_id: EpochId) -> Result<Option<u64>, NyxdError> {
let request = DkgQueryMsg::GetEpochThreshold { epoch_id };
self.query_dkg_contract(request).await
}
async fn get_registered_dealer_details(
&self,
address: &AccountId,
@@ -256,6 +261,9 @@ mod tests {
DkgQueryMsg::GetCurrentEpochThreshold {} => {
client.get_current_epoch_threshold().ignore()
}
DkgQueryMsg::GetEpochThreshold { epoch_id } => {
client.get_epoch_threshold(epoch_id).ignore()
}
DkgQueryMsg::GetRegisteredDealer {
dealer_address,
epoch_id,
@@ -0,0 +1,123 @@
// Copyright 2022-2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::collect_paged;
use crate::nyxd::contract_traits::NymContractsProvider;
use crate::nyxd::error::NyxdError;
use crate::nyxd::CosmWasmClient;
use async_trait::async_trait;
use cosmwasm_std::Coin;
use nym_ecash_contract_common::msg::QueryMsg as EcashQueryMsg;
use serde::Deserialize;
pub use nym_ecash_contract_common::blacklist::{
BlacklistedAccount, BlacklistedAccountResponse, PagedBlacklistedAccountResponse,
};
pub use nym_ecash_contract_common::deposit::{
Deposit, DepositData, DepositId, DepositResponse, PagedDepositsResponse,
};
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait EcashQueryClient {
async fn query_ecash_contract<T>(&self, query: EcashQueryMsg) -> Result<T, NyxdError>
where
for<'a> T: Deserialize<'a>;
async fn get_blacklisted_account(
&self,
public_key: String,
) -> Result<BlacklistedAccountResponse, NyxdError> {
self.query_ecash_contract(EcashQueryMsg::GetBlacklistedAccount { public_key })
.await
}
async fn get_blacklist_paged(
&self,
start_after: Option<String>,
limit: Option<u32>,
) -> Result<PagedBlacklistedAccountResponse, NyxdError> {
self.query_ecash_contract(EcashQueryMsg::GetBlacklistPaged { start_after, limit })
.await
}
async fn get_required_deposit_amount(&self) -> Result<Coin, NyxdError> {
self.query_ecash_contract(EcashQueryMsg::GetRequiredDepositAmount {})
.await
}
async fn get_deposit(&self, deposit_id: u32) -> Result<DepositResponse, NyxdError> {
self.query_ecash_contract(EcashQueryMsg::GetDeposit { deposit_id })
.await
}
async fn get_deposits_paged(
&self,
start_after: Option<u32>,
limit: Option<u32>,
) -> Result<PagedDepositsResponse, NyxdError> {
self.query_ecash_contract(EcashQueryMsg::GetDepositsPaged { start_after, limit })
.await
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait PagedEcashQueryClient: EcashQueryClient {
async fn get_all_blacklisted_accounts(&self) -> Result<Vec<BlacklistedAccount>, NyxdError> {
collect_paged!(self, get_blacklist_paged, accounts)
}
async fn get_all_deposits(&self) -> Result<Vec<DepositData>, NyxdError> {
collect_paged!(self, get_deposits_paged, deposits)
}
}
#[async_trait]
impl<T> PagedEcashQueryClient for T where T: EcashQueryClient {}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl<C> EcashQueryClient for C
where
C: CosmWasmClient + NymContractsProvider + Send + Sync,
{
async fn query_ecash_contract<T>(&self, query: EcashQueryMsg) -> Result<T, NyxdError>
where
for<'a> T: Deserialize<'a>,
{
let ecash_contract_address = self
.ecash_contract_address()
.ok_or_else(|| NyxdError::unavailable_contract_address("coconut bandwidth contract"))?;
self.query_contract_smart(ecash_contract_address, &query)
.await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::nyxd::contract_traits::tests::IgnoreValue;
use nym_ecash_contract_common::msg::QueryMsg;
// it's enough that this compiles and clippy is happy about it
#[allow(dead_code)]
fn all_query_variants_are_covered<C: EcashQueryClient + Send + Sync>(
client: C,
msg: EcashQueryMsg,
) {
match msg {
EcashQueryMsg::GetBlacklistedAccount { public_key } => {
client.get_blacklisted_account(public_key).ignore()
}
QueryMsg::GetBlacklistPaged { limit, start_after } => {
client.get_blacklist_paged(start_after, limit).ignore()
}
QueryMsg::GetDeposit { deposit_id } => client.get_deposit(deposit_id).ignore(),
QueryMsg::GetDepositsPaged { limit, start_after } => {
client.get_deposits_paged(start_after, limit).ignore()
}
QueryMsg::GetRequiredDepositAmount {} => client.get_required_deposit_amount().ignore(),
};
}
}
@@ -0,0 +1,124 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::nyxd::contract_traits::NymContractsProvider;
use crate::nyxd::cosmwasm_client::types::ExecuteResult;
use crate::nyxd::error::NyxdError;
use crate::nyxd::{Coin, Fee, SigningCosmWasmClient};
use crate::signing::signer::OfflineSigner;
use async_trait::async_trait;
use nym_ecash_contract_common::events::TICKET_BOOK_VALUE;
use nym_ecash_contract_common::msg::ExecuteMsg as EcashExecuteMsg;
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait EcashSigningClient {
async fn execute_ecash_contract(
&self,
fee: Option<Fee>,
msg: EcashExecuteMsg,
memo: String,
funds: Vec<Coin>,
) -> Result<ExecuteResult, NyxdError>;
async fn make_ticketbook_deposit(
&self,
public_key: String,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
let req = EcashExecuteMsg::DepositTicketBookFunds {
identity_key: public_key,
};
let amount = Coin::new(TICKET_BOOK_VALUE, "unym");
self.execute_ecash_contract(fee, req, "Ecash::Deposit".to_string(), vec![amount])
.await
}
async fn request_ticket_redemption(
&self,
commitment_bs58: String,
number_of_tickets: u16,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
let req = EcashExecuteMsg::RequestRedemption {
commitment_bs58,
number_of_tickets,
};
self.execute_ecash_contract(fee, req, Default::default(), vec![])
.await
}
async fn propose_for_blacklist(
&self,
public_key: String,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
let req = EcashExecuteMsg::ProposeToBlacklist { public_key };
self.execute_ecash_contract(fee, req, "Ecash::ProposeToBlacklist".to_string(), vec![])
.await
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl<C> EcashSigningClient for C
where
C: SigningCosmWasmClient + NymContractsProvider + Sync,
NyxdError: From<<Self as OfflineSigner>::Error>,
{
async fn execute_ecash_contract(
&self,
fee: Option<Fee>,
msg: EcashExecuteMsg,
memo: String,
funds: Vec<Coin>,
) -> Result<ExecuteResult, NyxdError> {
let ecash_contract_address = self
.ecash_contract_address()
.ok_or_else(|| NyxdError::unavailable_contract_address("coconut bandwidth contract"))?;
let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier())));
let signer_address = &self.signer_addresses()?[0];
self.execute(
signer_address,
ecash_contract_address,
&msg,
fee,
memo,
funds,
)
.await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::nyxd::contract_traits::tests::IgnoreValue;
use nym_ecash_contract_common::msg::ExecuteMsg;
// it's enough that this compiles and clippy is happy about it
#[allow(dead_code)]
fn all_execute_variants_are_covered<C: EcashSigningClient + Send + Sync>(
client: C,
msg: EcashExecuteMsg,
) {
match msg {
EcashExecuteMsg::DepositTicketBookFunds { identity_key } => client
.make_ticketbook_deposit(identity_key.to_string(), None)
.ignore(),
EcashExecuteMsg::AddToBlacklist { public_key: _ } => unimplemented!(), //no add to blacklist method on client
EcashExecuteMsg::ProposeToBlacklist { public_key } => {
client.propose_for_blacklist(public_key, None).ignore()
}
ExecuteMsg::RequestRedemption {
commitment_bs58,
number_of_tickets,
} => client
.request_ticket_redemption(commitment_bs58, number_of_tickets, None)
.ignore(),
ExecuteMsg::RedeemTickets { .. } => unimplemented!(), // no redeem tickets method for the client
};
}
}
@@ -8,34 +8,32 @@ use std::str::FromStr;
// TODO: all of those could/should be derived via a macro
// query clients
pub mod coconut_bandwidth_query_client;
pub mod dkg_query_client;
pub mod ecash_query_client;
pub mod group_query_client;
pub mod mixnet_query_client;
pub mod multisig_query_client;
pub mod vesting_query_client;
// signing clients
pub mod coconut_bandwidth_signing_client;
pub mod dkg_signing_client;
pub mod ecash_signing_client;
pub mod group_signing_client;
pub mod mixnet_signing_client;
pub mod multisig_signing_client;
pub mod vesting_signing_client;
// re-export query traits
pub use coconut_bandwidth_query_client::{
CoconutBandwidthQueryClient, PagedCoconutBandwidthQueryClient,
};
pub use dkg_query_client::{DkgQueryClient, PagedDkgQueryClient};
pub use ecash_query_client::{EcashQueryClient, PagedEcashQueryClient};
pub use group_query_client::{GroupQueryClient, PagedGroupQueryClient};
pub use mixnet_query_client::{MixnetQueryClient, PagedMixnetQueryClient};
pub use multisig_query_client::{MultisigQueryClient, PagedMultisigQueryClient};
pub use vesting_query_client::{PagedVestingQueryClient, VestingQueryClient};
// re-export signing traits
pub use coconut_bandwidth_signing_client::CoconutBandwidthSigningClient;
pub use dkg_signing_client::DkgSigningClient;
pub use ecash_signing_client::EcashSigningClient;
pub use group_signing_client::GroupSigningClient;
pub use mixnet_signing_client::MixnetSigningClient;
pub use multisig_signing_client::MultisigSigningClient;
@@ -48,7 +46,7 @@ pub trait NymContractsProvider {
fn vesting_contract_address(&self) -> Option<&AccountId>;
// coconut-related
fn coconut_bandwidth_contract_address(&self) -> Option<&AccountId>;
fn ecash_contract_address(&self) -> Option<&AccountId>;
fn dkg_contract_address(&self) -> Option<&AccountId>;
fn group_contract_address(&self) -> Option<&AccountId>;
fn multisig_contract_address(&self) -> Option<&AccountId>;
@@ -59,7 +57,7 @@ pub struct TypedNymContracts {
pub mixnet_contract_address: Option<AccountId>,
pub vesting_contract_address: Option<AccountId>,
pub coconut_bandwidth_contract_address: Option<AccountId>,
pub ecash_contract_address: Option<AccountId>,
pub group_contract_address: Option<AccountId>,
pub multisig_contract_address: Option<AccountId>,
pub coconut_dkg_contract_address: Option<AccountId>,
@@ -78,8 +76,8 @@ impl TryFrom<NymContracts> for TypedNymContracts {
.vesting_contract_address
.map(|addr| addr.parse())
.transpose()?,
coconut_bandwidth_contract_address: value
.coconut_bandwidth_contract_address
ecash_contract_address: value
.ecash_contract_address
.map(|addr| addr.parse())
.transpose()?,
group_contract_address: value
@@ -6,7 +6,7 @@ use crate::nyxd::error::NyxdError;
use crate::nyxd::CosmWasmClient;
use async_trait::async_trait;
use cw3::{
ProposalListResponse, ProposalResponse, VoteListResponse, VoteResponse, VoterDetail,
ProposalListResponse, ProposalResponse, VoteInfo, VoteListResponse, VoteResponse, VoterDetail,
VoterListResponse, VoterResponse,
};
use cw_utils::ThresholdResponse;
@@ -134,6 +134,28 @@ pub trait PagedMultisigQueryClient: MultisigQueryClient {
Ok(voters)
}
async fn get_all_votes(&self, proposal_id: u64) -> Result<Vec<VoteInfo>, NyxdError> {
let mut votes = Vec::new();
let mut start_after = None;
loop {
let mut paged_response = self
.list_votes(proposal_id, start_after.take(), None)
.await?;
let last_voter = paged_response.votes.last().map(|vote| vote.voter.clone());
votes.append(&mut paged_response.votes);
if let Some(start_after_res) = last_voter {
start_after = Some(start_after_res)
} else {
break;
}
}
Ok(votes)
}
}
#[async_trait]
@@ -31,15 +31,15 @@ pub trait MultisigSigningClient: NymContractsProvider {
voucher_value: Coin,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
let coconut_bandwidth_contract_address = self
.coconut_bandwidth_contract_address()
let ecash_contract_address = self
.ecash_contract_address()
.ok_or_else(|| NyxdError::unavailable_contract_address("coconut bandwidth contract"))?;
let release_funds_req = CoconutBandwidthExecuteMsg::ReleaseFunds {
funds: voucher_value.into(),
};
let release_funds_msg = CosmosMsg::Wasm(WasmMsg::Execute {
contract_addr: coconut_bandwidth_contract_address.to_string(),
contract_addr: ecash_contract_address.to_string(),
msg: to_binary(&release_funds_req)?,
funds: vec![],
});
@@ -2,31 +2,33 @@
// SPDX-License-Identifier: Apache-2.0
use crate::nyxd::cosmwasm_client::client_traits::CosmWasmClient;
use crate::nyxd::cosmwasm_client::helpers::{compress_wasm_code, CheckResponse};
use crate::nyxd::cosmwasm_client::helpers::{
compress_wasm_code, parse_msg_responses, CheckResponse,
};
use crate::nyxd::cosmwasm_client::logs::parse_raw_logs;
use crate::nyxd::cosmwasm_client::types::*;
use crate::nyxd::error::NyxdError;
use crate::nyxd::fee::{Fee, DEFAULT_SIMULATED_GAS_MULTIPLIER};
use crate::nyxd::helpers::find_tx_attribute;
use crate::nyxd::{Coin, GasAdjustable, GasPrice, TxResponse};
use crate::signing::signer::OfflineSigner;
use crate::signing::tx_signer::TxSigner;
use crate::signing::SignerData;
use async_trait::async_trait;
use cosmrs::bank::MsgSend;
use cosmrs::cosmwasm::{MsgClearAdmin, MsgUpdateAdmin};
use cosmrs::distribution::MsgWithdrawDelegatorReward;
use cosmrs::feegrant::{
AllowedMsgAllowance, BasicAllowance, MsgGrantAllowance, MsgRevokeAllowance,
};
use cosmrs::proto::cosmos::tx::signing::v1beta1::SignMode;
use cosmrs::staking::{MsgDelegate, MsgUndelegate};
use cosmrs::tendermint::abci::{Event, EventAttribute};
use cosmrs::tx::{self, Msg};
use cosmrs::{cosmwasm, AccountId, Any, Tx};
use log::debug;
use serde::Serialize;
use sha2::Digest;
use sha2::Sha256;
use std::time::SystemTime;
use tendermint_rpc::endpoint::broadcast;
@@ -52,20 +54,6 @@ fn single_unspecified_signer_auth(
}
.auth_info(empty_fee())
}
// Searches in events for an event of the given event type which contains an
// attribute for with the given key.
fn find_attribute<'a>(
events: &'a [Event],
event_type: &str,
attr_key: &str,
) -> Option<&'a EventAttribute> {
events
.iter()
.find(|attr| attr.kind == event_type)?
.attributes
.iter()
.find(|attr| attr.key == attr_key)
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
@@ -132,8 +120,7 @@ where
.await?
.check_response()?;
let logs = parse_raw_logs(tx_res.tx_result.log)?;
let events = tx_res.tx_result.events;
let logs = parse_raw_logs(&tx_res.tx_result.log)?;
let gas_info = GasInfo {
gas_wanted: tx_res.tx_result.gas_wanted.try_into().unwrap_or_default(),
gas_used: tx_res.tx_result.gas_used.try_into().unwrap_or_default(),
@@ -143,9 +130,8 @@ where
// the reason I think unwrap here is fine is that if the transaction succeeded and those
// fields do not exist or code_id is not a number, there's no way we can recover, we're probably connected
// to wrong validator or something
let code_id = find_attribute(&events, "store_code", "code_id")
let code_id = find_tx_attribute(&tx_res, "store_code", "code_id")
.unwrap()
.value
.parse()
.unwrap();
@@ -156,7 +142,7 @@ where
compressed_checksum,
code_id,
logs,
events,
events: tx_res.tx_result.events,
transaction_hash: tx_res.hash,
gas_info,
})
@@ -198,8 +184,7 @@ where
.await?
.check_response()?;
let logs = parse_raw_logs(tx_res.tx_result.log)?;
let events = tx_res.tx_result.events;
let logs = parse_raw_logs(&tx_res.tx_result.log)?;
let gas_info = GasInfo {
gas_wanted: tx_res.tx_result.gas_wanted.try_into().unwrap_or_default(),
gas_used: tx_res.tx_result.gas_used.try_into().unwrap_or_default(),
@@ -208,16 +193,15 @@ where
// the reason I think unwrap here is fine is that if the transaction succeeded and those
// fields do not exist or address is malformed, there's no way we can recover, we're probably connected
// to wrong validator or something
let contract_address = find_attribute(&events, "instantiate", "_contract_address")
let contract_address = find_tx_attribute(&tx_res, "instantiate", "_contract_address")
.unwrap()
.value
.parse()
.unwrap();
Ok(InstantiateResult {
contract_address,
logs,
events,
events: tx_res.tx_result.events,
transaction_hash: tx_res.hash,
gas_info,
})
@@ -231,7 +215,7 @@ where
fee: Fee,
memo: impl Into<String> + Send + 'static,
) -> Result<ChangeAdminResult, NyxdError> {
let change_admin_msg = sealed::cosmwasm::MsgUpdateAdmin {
let change_admin_msg = MsgUpdateAdmin {
sender: sender_address.clone(),
new_admin: new_admin.clone(),
contract: contract_address.clone(),
@@ -263,7 +247,7 @@ where
fee: Fee,
memo: impl Into<String> + Send + 'static,
) -> Result<ChangeAdminResult, NyxdError> {
let change_admin_msg = sealed::cosmwasm::MsgClearAdmin {
let change_admin_msg = MsgClearAdmin {
sender: sender_address.clone(),
contract: contract_address.clone(),
}
@@ -355,10 +339,11 @@ where
gas_wanted: tx_res.tx_result.gas_wanted.try_into().unwrap_or_default(),
gas_used: tx_res.tx_result.gas_used.try_into().unwrap_or_default(),
};
Ok(ExecuteResult {
logs: parse_raw_logs(tx_res.tx_result.log)?,
msg_responses: parse_msg_responses(tx_res.tx_result.data),
events: tx_res.tx_result.events,
data: tx_res.tx_result.data.into(),
transaction_hash: tx_res.hash,
gas_info,
})
@@ -401,8 +386,8 @@ where
};
Ok(ExecuteResult {
logs: parse_raw_logs(tx_res.tx_result.log)?,
msg_responses: parse_msg_responses(tx_res.tx_result.data),
events: tx_res.tx_result.events,
data: tx_res.tx_result.data.into(),
transaction_hash: tx_res.hash,
gas_info,
})
@@ -731,167 +716,3 @@ where
)?)
}
}
// a temporary bypass until https://github.com/cosmos/cosmos-rust/pull/419 is merged
mod sealed {
pub mod cosmwasm {
use cosmrs::{proto, tx::Msg, AccountId, ErrorReport, Result};
/// MsgUpdateAdmin sets a new admin for a smart contract
#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
pub struct MsgUpdateAdmin {
/// Sender is the that actor that signed the messages
pub sender: AccountId,
/// NewAdmin address to be set
pub new_admin: AccountId,
/// Contract is the address of the smart contract
pub contract: AccountId,
}
impl Msg for MsgUpdateAdmin {
type Proto = proto::cosmwasm::wasm::v1::MsgUpdateAdmin;
}
impl TryFrom<proto::cosmwasm::wasm::v1::MsgUpdateAdmin> for MsgUpdateAdmin {
type Error = ErrorReport;
fn try_from(
proto: proto::cosmwasm::wasm::v1::MsgUpdateAdmin,
) -> Result<MsgUpdateAdmin> {
MsgUpdateAdmin::try_from(&proto)
}
}
impl TryFrom<&proto::cosmwasm::wasm::v1::MsgUpdateAdmin> for MsgUpdateAdmin {
type Error = ErrorReport;
fn try_from(
proto: &proto::cosmwasm::wasm::v1::MsgUpdateAdmin,
) -> Result<MsgUpdateAdmin> {
Ok(MsgUpdateAdmin {
sender: proto.sender.parse()?,
new_admin: proto.new_admin.parse()?,
contract: proto.contract.parse()?,
})
}
}
impl From<MsgUpdateAdmin> for proto::cosmwasm::wasm::v1::MsgUpdateAdmin {
fn from(msg: MsgUpdateAdmin) -> proto::cosmwasm::wasm::v1::MsgUpdateAdmin {
proto::cosmwasm::wasm::v1::MsgUpdateAdmin::from(&msg)
}
}
impl From<&MsgUpdateAdmin> for proto::cosmwasm::wasm::v1::MsgUpdateAdmin {
fn from(msg: &MsgUpdateAdmin) -> proto::cosmwasm::wasm::v1::MsgUpdateAdmin {
proto::cosmwasm::wasm::v1::MsgUpdateAdmin {
sender: msg.sender.to_string(),
new_admin: msg.new_admin.to_string(),
contract: msg.contract.to_string(),
}
}
}
/// MsgUpdateAdminResponse returns empty data
#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)]
pub struct MsgUpdateAdminResponse {}
impl Msg for MsgUpdateAdminResponse {
type Proto = proto::cosmwasm::wasm::v1::MsgUpdateAdminResponse;
}
impl TryFrom<proto::cosmwasm::wasm::v1::MsgUpdateAdminResponse> for MsgUpdateAdminResponse {
type Error = ErrorReport;
fn try_from(
_proto: proto::cosmwasm::wasm::v1::MsgUpdateAdminResponse,
) -> Result<MsgUpdateAdminResponse> {
Ok(MsgUpdateAdminResponse {})
}
}
impl From<MsgUpdateAdminResponse> for proto::cosmwasm::wasm::v1::MsgUpdateAdminResponse {
fn from(
_msg: MsgUpdateAdminResponse,
) -> proto::cosmwasm::wasm::v1::MsgUpdateAdminResponse {
proto::cosmwasm::wasm::v1::MsgUpdateAdminResponse {}
}
}
/// MsgClearAdmin removes any admin stored for a smart contract
#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
pub struct MsgClearAdmin {
/// Sender is the that actor that signed the messages
pub sender: AccountId,
/// Contract is the address of the smart contract
pub contract: AccountId,
}
impl Msg for MsgClearAdmin {
type Proto = proto::cosmwasm::wasm::v1::MsgClearAdmin;
}
impl TryFrom<proto::cosmwasm::wasm::v1::MsgClearAdmin> for MsgClearAdmin {
type Error = ErrorReport;
fn try_from(proto: proto::cosmwasm::wasm::v1::MsgClearAdmin) -> Result<MsgClearAdmin> {
MsgClearAdmin::try_from(&proto)
}
}
impl TryFrom<&proto::cosmwasm::wasm::v1::MsgClearAdmin> for MsgClearAdmin {
type Error = ErrorReport;
fn try_from(proto: &proto::cosmwasm::wasm::v1::MsgClearAdmin) -> Result<MsgClearAdmin> {
Ok(MsgClearAdmin {
sender: proto.sender.parse()?,
contract: proto.contract.parse()?,
})
}
}
impl From<MsgClearAdmin> for proto::cosmwasm::wasm::v1::MsgClearAdmin {
fn from(msg: MsgClearAdmin) -> proto::cosmwasm::wasm::v1::MsgClearAdmin {
proto::cosmwasm::wasm::v1::MsgClearAdmin::from(&msg)
}
}
impl From<&MsgClearAdmin> for proto::cosmwasm::wasm::v1::MsgClearAdmin {
fn from(msg: &MsgClearAdmin) -> proto::cosmwasm::wasm::v1::MsgClearAdmin {
proto::cosmwasm::wasm::v1::MsgClearAdmin {
sender: msg.sender.to_string(),
contract: msg.contract.to_string(),
}
}
}
/// MsgClearAdminResponse returns empty data
#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)]
pub struct MsgClearAdminResponse {}
impl Msg for MsgClearAdminResponse {
type Proto = proto::cosmwasm::wasm::v1::MsgClearAdminResponse;
}
impl TryFrom<proto::cosmwasm::wasm::v1::MsgClearAdminResponse> for MsgClearAdminResponse {
type Error = ErrorReport;
fn try_from(
_proto: proto::cosmwasm::wasm::v1::MsgClearAdminResponse,
) -> Result<MsgClearAdminResponse> {
Ok(MsgClearAdminResponse {})
}
}
impl From<MsgClearAdminResponse> for proto::cosmwasm::wasm::v1::MsgClearAdminResponse {
fn from(
_msg: MsgClearAdminResponse,
) -> proto::cosmwasm::wasm::v1::MsgClearAdminResponse {
proto::cosmwasm::wasm::v1::MsgClearAdminResponse {}
}
}
}
}
@@ -2,9 +2,87 @@
// SPDX-License-Identifier: Apache-2.0
use crate::nyxd::error::NyxdError;
use cosmrs::abci::TxMsgData;
use cosmrs::cosmwasm::MsgExecuteContractResponse;
use cosmrs::proto::cosmos::base::query::v1beta1::{PageRequest, PageResponse};
use log::error;
use prost::bytes::Bytes;
use tendermint_rpc::endpoint::broadcast;
use crate::nyxd::cosmwasm_client::types::ExecuteResult;
pub use cosmrs::abci::MsgResponse;
pub fn parse_msg_responses(data: Bytes) -> Vec<MsgResponse> {
// it seems that currently, on wasmd 0.43 + tendermint-rs 0.37 + cosmrs 0.17.0-pre
// the data is left in undecoded base64 form, but I'd imagine this might change so if the decoding fails,
// use the bytes directly instead
let data = if let Ok(decoded) = base64::decode(&data) {
decoded
} else {
error!("failed to base64-decode the 'data' field of the TxResponse - has the chain been upgraded and introduced some breaking changes?");
data.into()
};
match TxMsgData::try_from(data) {
Ok(tx_msg_data) => tx_msg_data.msg_responses,
Err(err) => {
error!("failed to parse tx responses - has the chain been upgraded and introduced some breaking changes? the error was {err}");
Vec::new()
}
}
}
// requires there's a single response message
pub trait ToSingletonContractData: Sized {
fn parse_singleton_u32_contract_data(&self) -> Result<u32, NyxdError> {
let b = self.to_singleton_contract_data()?;
if b.len() != 4 {
return Err(NyxdError::MalformedResponseData {
got: b.len(),
expected: 4,
});
}
Ok(u32::from_be_bytes([b[0], b[1], b[2], b[3]]))
}
fn parse_singleton_u64_contract_data(&self) -> Result<u64, NyxdError> {
let b = self.to_singleton_contract_data()?;
if b.len() != 8 {
return Err(NyxdError::MalformedResponseData {
got: b.len(),
expected: 8,
});
}
Ok(u64::from_be_bytes([
b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7],
]))
}
fn to_singleton_contract_data(&self) -> Result<Vec<u8>, NyxdError>;
}
impl ToSingletonContractData for ExecuteResult {
fn to_singleton_contract_data(&self) -> Result<Vec<u8>, NyxdError> {
if self.msg_responses.len() != 1 {
return Err(NyxdError::UnexpectedNumberOfMsgResponses {
got: self.msg_responses.len(),
});
}
self.msg_responses[0].to_contract_response_data()
}
}
pub trait ToContractResponseData: Sized {
fn to_contract_response_data(&self) -> Result<Vec<u8>, NyxdError>;
}
impl ToContractResponseData for MsgResponse {
fn to_contract_response_data(&self) -> Result<Vec<u8>, NyxdError> {
Ok(self.try_decode_as::<MsgExecuteContractResponse>()?.data)
}
}
pub(crate) trait CheckResponse: Sized {
fn check_response(self) -> Result<Self, NyxdError>;
}
@@ -3,10 +3,11 @@
use crate::nyxd::error::NyxdError;
use itertools::Itertools;
use nym_ecash_contract_common::events::PROPOSAL_ID_ATTRIBUTE_NAME;
use serde::{Deserialize, Serialize};
pub use nym_coconut_bandwidth_contract_common::event_attributes::*;
pub use nym_coconut_dkg_common::event_attributes::*;
pub use nym_ecash_contract_common::event_attributes::*;
// it seems that currently validators just emit stringified events (which are also returned as part of deliverTx response)
// as their logs
@@ -33,6 +34,25 @@ pub fn find_attribute<'a>(
.find(|attr| attr.key == attribute_key)
}
/// 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]) -> Result<u64, NyxdError> {
let maybe_attributes = logs
.iter()
.rev()
.flat_map(|log| log.events.iter())
.find(|event| event.ty == "wasm")
.ok_or(NyxdError::ComswasmEventNotFound)?
.attributes
.iter()
.find(|attr| attr.key == PROPOSAL_ID_ATTRIBUTE_NAME);
let attribute = maybe_attributes.ok_or(NyxdError::ComswasmAttributeNotFound)?;
attribute
.value
.parse::<u64>()
.map_err(|_| NyxdError::DeserializationError("proposal_id".into()))
}
// these two functions were separated so that the internal logic could actually be tested
fn parse_raw_str_logs(raw: &str) -> Result<Vec<Log>, NyxdError> {
// From Cosmos SDK > 0.50 onwards, log field is not populated
@@ -49,7 +69,7 @@ fn parse_raw_str_logs(raw: &str) -> Result<Vec<Log>, NyxdError> {
Ok(logs)
}
pub fn parse_raw_logs(raw: String) -> Result<Vec<Log>, NyxdError> {
pub fn parse_raw_logs<S: AsRef<str>>(raw: S) -> Result<Vec<Log>, NyxdError> {
parse_raw_str_logs(raw.as_ref())
}
@@ -23,6 +23,8 @@ use tendermint_rpc::endpoint::*;
use tendermint_rpc::query::Query;
use tendermint_rpc::{Error as TendermintRpcError, Order, Paging, SimpleRequest};
pub use helpers::{ToContractResponseData, ToSingletonContractData};
#[cfg(feature = "http-client")]
use crate::http_client;
#[cfg(feature = "http-client")]
@@ -30,6 +30,7 @@ use prost::Message;
use serde::Serialize;
pub use cosmrs::abci::GasInfo;
pub use cosmrs::abci::MsgResponse;
pub type ContractCodeId = u64;
@@ -240,7 +241,7 @@ pub struct UploadResult {
pub gas_info: GasInfo,
}
#[derive(Debug)]
#[derive(Debug, Default)]
pub struct InstantiateOptions {
/// The funds that are transferred from the sender to the newly created contract.
/// The funds are transferred as part of the message execution after the contract address is
@@ -262,6 +263,11 @@ impl InstantiateOptions {
admin,
}
}
pub fn with_admin(mut self, admin: AccountId) -> Self {
self.admin = Some(admin);
self
}
}
#[derive(Debug, Serialize)]
@@ -307,7 +313,7 @@ pub struct MigrateResult {
pub struct ExecuteResult {
pub logs: Vec<Log>,
pub data: Vec<u8>,
pub msg_responses: Vec<MsgResponse>,
pub events: Vec<abci::Event>,
@@ -32,6 +32,12 @@ pub enum NyxdError {
#[error("There was an issue on the cosmrs side: {0}")]
CosmrsErrorReport(#[from] cosmrs::ErrorReport),
#[error("cosmwasm event not found")]
ComswasmEventNotFound,
#[error("cosmwasm attribute not found")]
ComswasmAttributeNotFound,
#[error("Failed to derive account address")]
AccountDerivationError,
@@ -142,6 +148,12 @@ pub enum NyxdError {
#[error("Account had an unexpected bech32 prefix. Expected: {expected}, got: {got}")]
UnexpectedBech32Prefix { got: String, expected: String },
#[error("the transaction returned unexpected, {got}, number of MsgResponse. Expected to receive a single one")]
UnexpectedNumberOfMsgResponses { got: usize },
#[error("the response data has invalid size. got {got} bytes, but expected {expected} bytes instead")]
MalformedResponseData { got: usize, expected: usize },
}
// The purpose of parsing the abci query result is that we want to generate the `pretty_log` if
@@ -3,11 +3,16 @@
use crate::nyxd::TxResponse;
// Searches in events for an event of the given event type which contains an
// attribute for with the given key.
pub fn find_tx_attribute(tx: &TxResponse, event_type: &str, attribute_key: &str) -> Option<String> {
let event = tx.tx_result.events.iter().find(|e| e.kind == event_type)?;
let attribute = event
.attributes
.iter()
.find(|attr| attr.key == attribute_key)?;
Some(attribute.value.clone())
let attribute = event.attributes.iter().find(|&attr| {
if let Ok(key_str) = attr.key_str() {
key_str == attribute_key
} else {
false
}
})?;
Some(attribute.value_str().ok().map(|str| str.to_string())).flatten()
}
@@ -245,8 +245,8 @@ impl<C, S> NyxdClient<C, S> {
self.config.contracts.vesting_contract_address = Some(address);
}
pub fn set_coconut_bandwidth_contract_address(&mut self, address: AccountId) {
self.config.contracts.coconut_bandwidth_contract_address = Some(address);
pub fn set_ecash_contract_address(&mut self, address: AccountId) {
self.config.contracts.ecash_contract_address = Some(address);
}
pub fn set_multisig_contract_address(&mut self, address: AccountId) {
@@ -267,11 +267,8 @@ impl<C, S> NymContractsProvider for NyxdClient<C, S> {
self.config.contracts.vesting_contract_address.as_ref()
}
fn coconut_bandwidth_contract_address(&self) -> Option<&AccountId> {
self.config
.contracts
.coconut_bandwidth_contract_address
.as_ref()
fn ecash_contract_address(&self) -> Option<&AccountId> {
self.config.contracts.ecash_contract_address.as_ref()
}
fn dkg_contract_address(&self) -> Option<&AccountId> {
@@ -384,6 +381,14 @@ where
}
}
pub fn mix_coin(&self, amount: u128) -> Coin {
Coin::new(amount, &self.config.chain_details.mix_denom.base)
}
pub fn mix_coins(&self, amount: u128) -> Vec<Coin> {
vec![self.mix_coin(amount)]
}
pub fn cw_address(&self) -> Addr {
// the call to unchecked is fine here as we're converting directly from `AccountId`
// which must have been a valid bech32 address
+1 -1
View File
@@ -43,9 +43,9 @@ nym-contracts-common = { path = "../cosmwasm-smart-contracts/contracts-common" }
nym-bandwidth-controller = { path = "../../common/bandwidth-controller" }
nym-mixnet-contract-common = { path = "../cosmwasm-smart-contracts/mixnet-contract" }
nym-vesting-contract-common = { path = "../cosmwasm-smart-contracts/vesting-contract" }
nym-coconut-bandwidth-contract-common = { path = "../cosmwasm-smart-contracts/coconut-bandwidth-contract" }
nym-coconut-dkg-common = { path = "../cosmwasm-smart-contracts/coconut-dkg" }
nym-multisig-contract-common = { path = "../cosmwasm-smart-contracts/multisig-contract" }
nym-ecash-contract-common = { path = "../cosmwasm-smart-contracts/ecash-contract" }
nym-sphinx = { path = "../../common/nymsphinx" }
nym-client-core = { path = "../../common/client-core" }
nym-config = { path = "../../common/config" }
@@ -1,194 +0,0 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::context::SigningClient;
use anyhow::{anyhow, bail};
use clap::ArgGroup;
use clap::Parser;
use futures::StreamExt;
use log::{error, info};
use nym_coconut_dkg_common::types::EpochId;
use nym_credential_utils::utils::block_until_coconut_is_available;
use nym_credentials::coconut::bandwidth::freepass::MAX_FREE_PASS_VALIDITY;
use nym_credentials::{
obtain_aggregate_verification_key, IssuanceBandwidthCredential, IssuedBandwidthCredential,
};
use nym_credentials_interface::VerificationKey;
use nym_validator_client::coconut::all_coconut_api_clients;
use nym_validator_client::nyxd::contract_traits::{DkgQueryClient, NymContractsProvider};
use nym_validator_client::nyxd::CosmWasmClient;
use nym_validator_client::signing::AccountData;
use nym_validator_client::CoconutApiClient;
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
use std::sync::Arc;
use time::format_description::well_known::Rfc3339;
use time::OffsetDateTime;
use zeroize::Zeroizing;
fn parse_rfc3339_expiration_date(raw: &str) -> Result<OffsetDateTime, time::error::Parse> {
OffsetDateTime::parse(raw, &Rfc3339)
}
#[derive(Debug, Parser)]
#[clap(group(ArgGroup::new("expiration").required(true)))]
pub struct Args {
/// Specifies the expiration date of the free pass(es)
/// Can't be set to more than a week into the future.
#[clap(long, group = "expiration", value_parser = parse_rfc3339_expiration_date)]
pub(crate) expiration_date: Option<OffsetDateTime>,
/// The expiration of the free pass(es) expresses as unix timestamp.
/// Can't be set to more than a week into the future.
#[clap(long, group = "expiration")]
pub(crate) expiration_timestamp: Option<i64>,
/// The number of free passes to issue
#[clap(long, default_value = "1")]
pub(crate) amount: u64,
/// Path to the output directory for generated free passes.
#[clap(long)]
pub(crate) output_dir: PathBuf,
}
async fn get_freepass(
api_clients: Vec<CoconutApiClient>,
aggregate_vk: &VerificationKey,
threshold: u64,
epoch_id: EpochId,
signing_account: &AccountData,
expiration_date: OffsetDateTime,
) -> anyhow::Result<IssuedBandwidthCredential> {
let issuance_pass = IssuanceBandwidthCredential::new_freepass(Some(expiration_date));
let signing_data = issuance_pass.prepare_for_signing();
let credential_shares = Arc::new(tokio::sync::Mutex::new(Vec::new()));
futures::stream::iter(api_clients)
.for_each_concurrent(None, |client| async {
// move the client into the block
let client = client;
let api_url = client.api_client.api_url();
info!("contacting {api_url} for blinded free pass");
match issuance_pass
.obtain_partial_freepass_credential(
&client.api_client,
signing_account,
&client.verification_key,
signing_data.clone(),
)
.await
{
Ok(partial_credential) => {
credential_shares
.lock()
.await
.push((partial_credential, client.node_id).into());
}
Err(err) => {
error!("failed to obtain partial free pass from {api_url}: {err}")
}
}
})
.await;
// SAFETY: the futures have completed, so we MUST have the only arc reference
#[allow(clippy::unwrap_used)]
let credential_shares = Arc::into_inner(credential_shares).unwrap().into_inner();
if credential_shares.len() < threshold as usize {
bail!("we managed to obtain only {} partial credentials while the minimum threshold is {threshold}", credential_shares.len());
}
let signature = issuance_pass.aggregate_signature_shares(aggregate_vk, &credential_shares)?;
Ok(issuance_pass.into_issued_credential(signature, epoch_id))
}
pub async fn execute(args: Args, client: SigningClient) -> anyhow::Result<()> {
let address = client.address();
if !args.output_dir.is_dir() {
bail!("the provided output directory is not a directory!");
}
if args.output_dir.read_dir()?.next().is_some() {
bail!("the provided output directory is not empty!");
}
let Some(bandwidth_contract) = client.coconut_bandwidth_contract_address() else {
bail!("the bandwidth contract address is not set")
};
let Some(bandwidth_admin) = client
.get_contract(bandwidth_contract)
.await
.map(|c| c.contract_info.admin)?
else {
bail!("the bandwidth contract doesn't have any admin set")
};
// sanity checks since nym-apis will reject invalid requests anyway
if address != bandwidth_admin {
bail!("the provided mnemonic does not correspond to the current admin of the bandwidth contract")
}
let expiration_date = match args.expiration_date {
Some(date) => date,
// SAFETY: one of those arguments must have been set
None => OffsetDateTime::from_unix_timestamp(args.expiration_timestamp.unwrap())?,
};
let now = OffsetDateTime::now_utc();
if expiration_date > now + MAX_FREE_PASS_VALIDITY {
bail!("the provided free pass request has too long expiry (expiry is set to on {expiration_date})")
}
if expiration_date < now {
bail!("the provided free pass expiry is set in the past!")
}
// issuance start
block_until_coconut_is_available(&client).await?;
let signing_account = client.signing_account()?;
let epoch_id = client.get_current_epoch().await?.epoch_id;
let threshold = client
.get_current_epoch_threshold()
.await?
.ok_or(anyhow!("no threshold available"))?;
let api_clients = all_coconut_api_clients(&client, epoch_id).await?;
if api_clients.len() < threshold as usize {
bail!(
"we have only {} api clients available while the minimum threshold is {threshold}",
api_clients.len()
)
}
let aggregate_vk = obtain_aggregate_verification_key(&api_clients)?;
for i in 0..args.amount {
let human_index = i + 1;
info!("trying to obtain free pass {human_index}/{}", args.amount);
let free_pass = get_freepass(
api_clients.clone(),
&aggregate_vk,
threshold,
epoch_id,
&signing_account,
expiration_date,
)
.await?;
let credential_data = Zeroizing::new(free_pass.pack_v1());
let output = args.output_dir.join(format!("freepass_{i}.nym"));
info!("saving the freepass to '{}'", output.display());
File::create(output)?.write_all(&credential_data)?;
}
Ok(())
}
@@ -7,7 +7,7 @@ use anyhow::bail;
use clap::Parser;
use nym_credential_storage::initialise_persistent_storage;
use nym_credential_utils::utils;
use nym_validator_client::nyxd::Coin;
use nym_crypto::asymmetric::identity;
use std::path::PathBuf;
#[derive(Debug, Parser)]
@@ -15,21 +15,9 @@ pub struct Args {
/// Config file of the client that is supposed to use the credential.
#[clap(long)]
pub(crate) client_config: PathBuf,
/// The amount of utokens the credential will hold.
#[clap(long, default_value = "0")]
pub(crate) amount: u64,
/// Path to a directory used to store recovery files for unconsumed deposits
#[clap(long)]
pub(crate) recovery_dir: PathBuf,
}
pub async fn execute(args: Args, client: SigningClient) -> anyhow::Result<()> {
if args.amount == 0 {
bail!("did not specify credential amount")
}
let loaded = CommonConfigsWrapper::try_load(args.client_config)?;
if let Ok(id) = loaded.try_get_id() {
@@ -40,16 +28,18 @@ pub async fn execute(args: Args, client: SigningClient) -> anyhow::Result<()> {
bail!("the loaded config does not have a credentials store information")
};
let Ok(private_id_key) = loaded.try_get_private_id_key() else {
bail!("the loaded config does not have a public id key information")
};
println!(
"using credentials store at '{}'",
credentials_store.display()
);
let denom = &client.current_chain_details().mix_denom.base;
let coin = Coin::new(args.amount as u128, denom);
let persistent_storage = initialise_persistent_storage(credentials_store).await;
utils::issue_credential(&client, coin, &persistent_storage, args.recovery_dir).await?;
let private_id_key: identity::PrivateKey = nym_pemstore::load_key(private_id_key)?;
utils::issue_credential(&client, &persistent_storage, &private_id_key.to_bytes()).await?;
Ok(())
}
+9 -11
View File
@@ -3,22 +3,20 @@
use clap::{Args, Subcommand};
pub mod generate_freepass;
pub mod import_credential;
pub mod issue_credentials;
pub mod recover_credentials;
pub mod import_ticket_book;
pub mod issue_ticket_book;
pub mod recover_ticket_book;
#[derive(Debug, Args)]
#[clap(args_conflicts_with_subcommands = true, subcommand_required = true)]
pub struct Coconut {
pub struct Ecash {
#[clap(subcommand)]
pub command: CoconutCommands,
pub command: EcashCommands,
}
#[derive(Debug, Subcommand)]
pub enum CoconutCommands {
GenerateFreepass(generate_freepass::Args),
IssueCredentials(issue_credentials::Args),
RecoverCredentials(recover_credentials::Args),
ImportCredential(import_credential::Args),
pub enum EcashCommands {
IssueTicketBook(issue_ticket_book::Args),
RecoverTicketBook(recover_ticket_book::Args),
ImportTicketBook(import_ticket_book::Args),
}
@@ -6,7 +6,7 @@ use crate::utils::CommonConfigsWrapper;
use anyhow::bail;
use clap::Parser;
use nym_credential_storage::initialise_persistent_storage;
use nym_credential_utils::{recovery_storage, utils};
use nym_credential_utils::utils;
use std::path::PathBuf;
#[derive(Debug, Parser)]
@@ -14,10 +14,6 @@ pub struct Args {
/// Config file of the client that is supposed to use the credential.
#[clap(long)]
pub(crate) client_config: PathBuf,
/// Path to a directory used to store recovery files for unconsumed deposits
#[clap(long)]
pub(crate) recovery_dir: PathBuf,
}
pub async fn execute(args: Args, client: QueryClient) -> anyhow::Result<()> {
@@ -37,12 +33,9 @@ pub async fn execute(args: Args, client: QueryClient) -> anyhow::Result<()> {
);
let persistent_storage = initialise_persistent_storage(credentials_store).await;
let recovery_storage = recovery_storage::RecoveryStorage::new(args.recovery_dir)?;
let recovered =
utils::recover_credentials(&client, &recovery_storage, &persistent_storage).await?;
let recovered = utils::recover_deposits(&client, &persistent_storage).await?;
// TODO: denom?
println!("recovered {recovered} worth of credentials");
println!("recovered {recovered} ticketbooks");
Ok(())
}
+28
View File
@@ -123,6 +123,21 @@ impl CommonConfigsWrapper {
}
}
pub(crate) fn try_get_private_id_key(&self) -> anyhow::Result<PathBuf> {
match self {
CommonConfigsWrapper::NymClients(cfg) => Ok(cfg
.storage_paths
.inner
.keys
.private_identity_key_file
.clone()),
CommonConfigsWrapper::NymApi(_cfg) => {
todo!() //SW this will depend on the new network monitor structure. Ping @Drazen
}
CommonConfigsWrapper::Unknown(cfg) => cfg.try_get_private_id_key(),
}
}
pub(crate) fn try_get_credentials_store(&self) -> anyhow::Result<PathBuf> {
match self {
CommonConfigsWrapper::NymClients(cfg) => {
@@ -225,4 +240,17 @@ impl UnknownConfigWrapper {
bail!("no 'credentials_database_path' field present in the config")
}
}
pub(crate) fn try_get_private_id_key(&self) -> anyhow::Result<PathBuf> {
let id_val = self
.find_value("keys.private_identity_key_file")
.ok_or_else(|| {
anyhow!("no 'keys.private_identity_key_file' field present in the config")
})?;
if let toml::Value::String(pub_id_key) = id_val {
Ok(pub_id_key.parse()?)
} else {
bail!("no 'keys.private_identity_key_file' field present in the config")
}
}
}
@@ -6,17 +6,20 @@ use std::str::FromStr;
use clap::Parser;
use log::{debug, info};
use nym_coconut_bandwidth_contract_common::msg::InstantiateMsg;
use nym_ecash_contract_common::msg::InstantiateMsg;
use nym_validator_client::nyxd::AccountId;
#[derive(Debug, Parser)]
pub struct Args {
#[clap(long)]
pub pool_addr: String,
pub group_addr: Option<AccountId>,
#[clap(long)]
pub multisig_addr: Option<AccountId>,
#[clap(long)]
pub holding_account: AccountId,
#[clap(long)]
pub mix_denom: Option<String>,
}
@@ -26,8 +29,15 @@ pub async fn generate(args: Args) {
debug!("Received arguments: {:?}", args);
let group_addr = args.group_addr.unwrap_or_else(|| {
let address = std::env::var(nym_network_defaults::var_names::GROUP_CONTRACT_ADDRESS)
.expect("Multisig address has to be set");
AccountId::from_str(address.as_str())
.expect("Failed converting multisig address to AccountId")
});
let multisig_addr = args.multisig_addr.unwrap_or_else(|| {
let address = std::env::var(nym_network_defaults::var_names::REWARDING_VALIDATOR_ADDRESS)
let address = std::env::var(nym_network_defaults::var_names::MULTISIG_CONTRACT_ADDRESS)
.expect("Multisig address has to be set");
AccountId::from_str(address.as_str())
.expect("Failed converting multisig address to AccountId")
@@ -38,7 +48,8 @@ pub async fn generate(args: Args) {
});
let instantiate_msg = InstantiateMsg {
pool_addr: args.pool_addr,
holding_account: args.holding_account.to_string(),
group_addr: group_addr.to_string(),
multisig_addr: multisig_addr.to_string(),
mix_denom,
};
@@ -3,8 +3,8 @@
use clap::{Args, Subcommand};
pub mod coconut_bandwidth;
pub mod coconut_dkg;
pub mod ecash_bandwidth;
pub mod mixnet;
pub mod multisig;
pub mod vesting;
@@ -18,7 +18,7 @@ pub struct GenerateMessage {
#[derive(Debug, Subcommand)]
pub enum GenerateMessageCommands {
CoconutBandwidth(coconut_bandwidth::Args),
EcashBandwidth(ecash_bandwidth::Args),
CoconutDKG(coconut_dkg::Args),
Mixnet(mixnet::Args),
Multisig(multisig::Args),
@@ -22,7 +22,7 @@ pub struct Args {
pub max_voting_period: u64,
#[clap(long)]
pub coconut_bandwidth_contract_address: Option<AccountId>,
pub ecash_contract_address: Option<AccountId>,
#[clap(long)]
pub coconut_dkg_contract_address: Option<AccountId>,
@@ -33,14 +33,12 @@ pub async fn generate(args: Args) {
debug!("Received arguments: {:?}", args);
let coconut_bandwidth_contract_address =
args.coconut_bandwidth_contract_address.unwrap_or_else(|| {
let address =
std::env::var(nym_network_defaults::var_names::COCONUT_BANDWIDTH_CONTRACT_ADDRESS)
.expect("Coconut bandwidth contract address has to be set");
AccountId::from_str(address.as_str())
.expect("Failed converting bandwidth contract address to AccountId")
});
let ecash_contract_address = args.ecash_contract_address.unwrap_or_else(|| {
let address = std::env::var(nym_network_defaults::var_names::ECASH_CONTRACT_ADDRESS)
.expect("Coconut bandwidth contract address has to be set");
AccountId::from_str(address.as_str())
.expect("Failed converting bandwidth contract address to AccountId")
});
let coconut_dkg_contract_address = args.coconut_dkg_contract_address.unwrap_or_else(|| {
let address = std::env::var(nym_network_defaults::var_names::COCONUT_DKG_CONTRACT_ADDRESS)
@@ -58,7 +56,7 @@ pub async fn generate(args: Args) {
max_voting_period: Duration::Time(args.max_voting_period),
executor: None,
proposal_deposit: None,
coconut_bandwidth_contract_address: coconut_bandwidth_contract_address.to_string(),
coconut_bandwidth_contract_address: ecash_contract_address.to_string(),
coconut_dkg_contract_address: coconut_dkg_contract_address.to_string(),
};
@@ -87,6 +87,9 @@ pub enum QueryMsg {
#[cfg_attr(feature = "schema", returns(u64))]
GetCurrentEpochThreshold {},
#[cfg_attr(feature = "schema", returns(u64))]
GetEpochThreshold { epoch_id: EpochId },
#[cfg_attr(feature = "schema", returns(StateAdvanceResponse))]
CanAdvanceState {},
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_std::Event;
use std::str::FromStr;
/// Looks up value of particular attribute in the provided event. If it fails to find it,
/// the function panics.
@@ -31,6 +32,23 @@ pub fn may_find_attribute(event: &Event, key: &str) -> Option<String> {
None
}
pub fn try_find_attribute<T, E>(
events: &[Event],
event_name: &str,
key: &str,
) -> Option<Result<T, E>>
where
T: FromStr<Err = E>,
{
for event in events {
if event.ty == event_name {
let value = may_find_attribute(event, key)?;
return Some(value.parse());
}
}
None
}
pub trait OptionallyAddAttribute {
fn add_optional_attribute(
self,
@@ -0,0 +1,21 @@
[package]
name = "nym-ecash-contract-common"
version = "0.1.0"
edition = "2021"
license.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
bs58.workspace = true
cosmwasm-std = { workspace = true }
cosmwasm-schema = { workspace = true }
cw2 = { workspace = true, optional = true }
nym-multisig-contract-common = { path = "../multisig-contract" }
thiserror.workspace = true
cw-utils = { workspace = true }
cw-controllers = { workspace = true }
[features]
schema = ["cw2"]
@@ -0,0 +1,71 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_schema::cw_serde;
#[cw_serde]
pub struct BlacklistedAccount {
pub public_key: String,
pub info: Blacklisting,
}
impl From<(String, Blacklisting)> for BlacklistedAccount {
fn from((public_key, info): (String, Blacklisting)) -> Self {
BlacklistedAccount { public_key, info }
}
}
#[cw_serde]
pub struct Blacklisting {
pub proposal_id: u64,
pub finalized_at_height: Option<u64>,
}
impl Blacklisting {
pub fn new(proposal_id: u64) -> Self {
Blacklisting {
proposal_id,
finalized_at_height: None,
}
}
}
impl BlacklistedAccount {
pub fn public_key(&self) -> &str {
&self.public_key
}
}
#[cw_serde]
pub struct PagedBlacklistedAccountResponse {
pub accounts: Vec<BlacklistedAccount>,
pub per_page: usize,
/// Field indicating paging information for the following queries if the caller wishes to get further entries.
pub start_next_after: Option<String>,
}
impl PagedBlacklistedAccountResponse {
pub fn new(
accounts: Vec<BlacklistedAccount>,
per_page: usize,
start_next_after: Option<String>,
) -> Self {
PagedBlacklistedAccountResponse {
accounts,
per_page,
start_next_after,
}
}
}
#[cw_serde]
pub struct BlacklistedAccountResponse {
pub account: Option<Blacklisting>,
}
impl BlacklistedAccountResponse {
pub fn new(account: Option<Blacklisting>) -> Self {
BlacklistedAccountResponse { account }
}
}
@@ -0,0 +1,76 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::error::EcashContractError;
use cosmwasm_schema::cw_serde;
use cosmwasm_std::{StdError, StdResult};
pub type DepositId = u32;
#[cw_serde]
pub struct Deposit {
pub bs58_encoded_ed25519_pubkey: String,
}
impl Deposit {
pub fn new(bs58_encoded_ed25519_pubkey: String) -> Self {
Deposit {
bs58_encoded_ed25519_pubkey,
}
}
pub fn get_ed25519_pubkey_bytes(raw: &str) -> Result<[u8; 32], EcashContractError> {
let mut ed25519_pubkey_bytes = [0u8; 32];
bs58::decode(raw)
.onto(&mut ed25519_pubkey_bytes)
.map_err(|_| EcashContractError::MalformedEd25519Identity)?;
Ok(ed25519_pubkey_bytes)
}
pub fn encode_pubkey_bytes(raw: &[u8]) -> String {
bs58::encode(raw).into_string()
}
pub fn to_bytes(&self) -> Result<[u8; 32], EcashContractError> {
Self::get_ed25519_pubkey_bytes(&self.bs58_encoded_ed25519_pubkey)
}
pub fn try_from_bytes(bytes: &[u8]) -> StdResult<Self> {
if bytes.len() != 32 {
return Err(StdError::generic_err("malformed deposit data"));
}
Ok(Deposit {
bs58_encoded_ed25519_pubkey: Self::encode_pubkey_bytes(bytes),
})
}
}
#[cw_serde]
pub struct DepositResponse {
pub id: DepositId,
pub deposit: Option<Deposit>,
}
#[cw_serde]
pub struct DepositData {
pub id: DepositId,
pub deposit: Deposit,
}
impl From<(DepositId, Deposit)> for DepositData {
fn from((id, deposit): (DepositId, Deposit)) -> Self {
DepositData { id, deposit }
}
}
#[cw_serde]
pub struct PagedDepositsResponse {
pub deposits: Vec<DepositData>,
/// Field indicating paging information for the following queries if the caller wishes to get further entries.
pub start_next_after: Option<DepositId>,
}
@@ -0,0 +1,68 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_std::{Coin, StdError};
use cw_controllers::AdminError;
use cw_utils::PaymentError;
use thiserror::Error;
#[derive(Error, Debug, PartialEq)]
pub enum EcashContractError {
#[error(transparent)]
Std(#[from] StdError),
#[error("Invalid deposit")]
InvalidDeposit(#[from] PaymentError),
#[error("received wrong amount for deposit. got: {received}. required: {amount}")]
WrongAmount { received: u128, amount: u128 },
#[error("There aren't enough funds in the contract")]
NotEnoughFunds,
#[error(transparent)]
Admin(#[from] AdminError),
#[error("could not find proposal id inside the multisig reply SubMsg")]
MissingProposalId,
// realistically this should NEVER be thrown
#[error("the proposal id returned by the multisig contract could not be parsed into an u64")]
MalformedProposalId,
#[error("Group contract invalid address '{addr}'")]
InvalidGroup { addr: String },
#[error("Unauthorized")]
Unauthorized,
#[error("Failed to parse {value} into a valid SemVer version: {error_message}")]
SemVerFailure {
value: String,
error_message: String,
},
#[error("received an invalid reply id: {id}. it does not correspond to any sent SubMsg")]
InvalidReplyId { id: u64 },
#[error("reached the maximum of 255 different deposit types")]
MaximumDepositTypesReached,
#[error("compressed deposit info {typ} does not corresponds to any known type")]
UnknownCompressedDepositInfoType { typ: u8 },
#[error("deposit info {typ} does not corresponds to any previously seen type")]
UnknownDepositInfoType { typ: String },
#[error("the provided ed25519 identity was malformed")]
MalformedEd25519Identity,
#[error("the required deposit amount has changed since the contract was created! This was not expected! It used to be {at_init} but it's {current} now! Please let the developers know ASAP!")]
DepositAmountChanged { at_init: Coin, current: Coin },
#[error("the e-cash ticket value has changed since the contract was created! This was not expected! It used to be {at_init} but it's {current} now! Please let the developers know ASAP!")]
TicketValueChanged { at_init: Coin, current: Coin },
#[error("the provided tickets redemption commitment is malformed")]
MalformedRedemptionCommitment,
}
@@ -0,0 +1,4 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub const BANDWIDTH_PROPOSAL_ID: &str = "proposal_id";
@@ -0,0 +1,18 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
// event types
pub const DEPOSITED_FUNDS_EVENT_TYPE: &str = "deposited-funds";
// a 'wasm-' prefix is added to all cosmwasm events
pub const COSMWASM_DEPOSITED_FUNDS_EVENT_TYPE: &str = "wasm-deposited-funds";
pub const DEPOSIT_ID: &str = "deposit-id";
pub const TICKET_BOOK_VALUE: u128 = 50_000_000;
pub const TICKET_VALUE: u128 = 50_000;
pub const WASM_EVENT_NAME: &str = "wasm";
pub const PROPOSAL_ID_ATTRIBUTE_NAME: &str = "proposal_id";
pub const BLACKLIST_PROPOSAL_REPLY_ID: u64 = 7759;
pub const REDEMPTION_PROPOSAL_REPLY_ID: u64 = 2137;
@@ -0,0 +1,12 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod blacklist;
pub mod deposit;
pub mod error;
pub mod event_attributes;
pub mod events;
pub mod msg;
pub mod redeem_credential;
pub use error::EcashContractError;
@@ -0,0 +1,76 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_schema::cw_serde;
#[cfg(feature = "schema")]
use crate::blacklist::{BlacklistedAccountResponse, PagedBlacklistedAccountResponse};
#[cfg(feature = "schema")]
use crate::deposit::{DepositResponse, PagedDepositsResponse};
#[cfg(feature = "schema")]
use cosmwasm_schema::QueryResponses;
#[cfg(feature = "schema")]
use cosmwasm_std::Coin;
#[cw_serde]
pub struct InstantiateMsg {
pub holding_account: String,
pub multisig_addr: String,
pub group_addr: String,
pub mix_denom: String,
}
#[cw_serde]
pub enum ExecuteMsg {
/// Used by clients to request ticket books from the signers
DepositTicketBookFunds {
identity_key: String,
},
/// Used by gateways to batch redeem tokens from the spent tickets
RequestRedemption {
commitment_bs58: String,
number_of_tickets: u16,
},
/// The actual message that gets executed, after multisig votes, that transfers the ticket tokens into gateway's (and the holding) account
RedeemTickets {
n: u16,
gw: String,
},
// SpendCredential {
// serial_number: String,
// gateway_cosmos_address: String,
// },
ProposeToBlacklist {
public_key: String,
},
AddToBlacklist {
public_key: String,
},
}
#[cw_serde]
#[cfg_attr(feature = "schema", derive(QueryResponses))]
pub enum QueryMsg {
#[cfg_attr(feature = "schema", returns(BlacklistedAccountResponse))]
GetBlacklistedAccount { public_key: String },
#[cfg_attr(feature = "schema", returns(PagedBlacklistedAccountResponse))]
GetBlacklistPaged {
limit: Option<u32>,
start_after: Option<String>,
},
#[cfg_attr(feature = "schema", returns(Coin))]
GetRequiredDepositAmount {},
#[cfg_attr(feature = "schema", returns(DepositResponse))]
GetDeposit { deposit_id: u32 },
#[cfg_attr(feature = "schema", returns(PagedDepositsResponse))]
GetDepositsPaged {
limit: Option<u32>,
start_after: Option<u32>,
},
}
@@ -0,0 +1,5 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
// TODO: to be moved to multisig
pub const BATCH_REDEMPTION_PROPOSAL_TITLE: &str = "ecash-redemption";
@@ -47,4 +47,10 @@ pub enum ContractError {
#[error("{0}")]
Deposit(#[from] DepositError),
#[error("the provided redemption digest does not have valid base58 encoding or is not 32 bytes long")]
MalformedRedemptionDigest,
#[error("the provided redemption proposal data is malformed and can't be decoded")]
MalformedRedemptionProposalData,
}
+12 -3
View File
@@ -8,22 +8,31 @@ license.workspace = true
[dependencies]
async-trait = { workspace = true }
bincode = { workspace = true, optional = true }
log = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true, features = ["sync"]}
serde = { workspace = true, features = ["derive"], optional = true }
tokio = { workspace = true, features = ["sync"] }
zeroize = { workspace = true, features = ["zeroize_derive"] }
nym-credentials = { path = "../credentials" }
nym-compact-ecash = { path = "../nym_offline_compact_ecash" }
nym-ecash-time = { path = "../ecash-time" }
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx]
workspace = true
features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"]
features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate", "time"]
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.tokio]
workspace = true
features = [ "rt-multi-thread", "net", "signal", "fs" ]
features = ["rt-multi-thread", "net", "signal", "fs"]
[build-dependencies]
sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] }
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
[features]
persistent-storage = ["bincode", "serde"]
@@ -0,0 +1,66 @@
/*
* Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
* SPDX-License-Identifier: Apache-2.0
*/
DROP TABLE coconut_credentials;
CREATE TABLE master_verification_key (
epoch_id INTEGER PRIMARY KEY NOT NULL,
serialised_key BLOB NOT NULL
);
CREATE TABLE coin_indices_signatures
(
epoch_id INTEGER PRIMARY KEY NOT NULL,
serialised_signatures BLOB NOT NULL
);
CREATE TABLE expiration_date_signatures (
expiration_date DATE NOT NULL UNIQUE PRIMARY KEY,
epoch_id INTEGER NOT NULL,
-- combined signatures for all tuples issued for given day
serialised_signatures BLOB NOT NULL
);
CREATE TABLE ecash_ticketbook
(
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
-- introduce a way for us to introduce breaking changes in serialization of data
serialization_revision INTEGER NOT NULL,
-- the actual crypto data of the ticketbook (wallet, keys, etc.)
ticketbook_data BLOB NOT NULL UNIQUE,
-- for each ticketbook we MUST have corresponding expiration date signatures
expiration_date DATE NOT NULL REFERENCES expiration_date_signatures(expiration_date),
-- for each ticketbook we MUST have corresponding coin index signatures
epoch_id INTEGER NOT NULL REFERENCES coin_indices_signatures(epoch_id),
-- the initial number of tickets the wallet has been created for
total_tickets INTEGER NOT NULL,
-- how many tickets have been used so far (the `l` value of the wallet)
used_tickets INTEGER NOT NULL
);
-- data for ticketbooks that have an associated deposit, but failed to get issued
CREATE TABLE pending_issuance
(
deposit_id INTEGER NOT NULL PRIMARY KEY,
-- introduce a way for us to introduce breaking changes in serialization of data
serialization_revision INTEGER NOT NULL,
pending_ticketbook_data BLOB NOT NULL UNIQUE,
-- for each ticketbook we MUST have corresponding expiration date signatures
expiration_date DATE NOT NULL REFERENCES expiration_date_signatures(expiration_date)
);
+212 -100
View File
@@ -1,23 +1,34 @@
// Copyright 2023-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::models::{CredentialUsage, StoredIssuedCredential};
use crate::models::{BasicTicketbookInformation, RetrievedPendingTicketbook, RetrievedTicketbook};
use nym_compact_ecash::scheme::coin_indices_signatures::AnnotatedCoinIndexSignature;
use nym_compact_ecash::scheme::expiration_date_signatures::AnnotatedExpirationDateSignature;
use nym_compact_ecash::VerificationKeyAuth;
use nym_credentials::ecash::bandwidth::serialiser::VersionedSerialise;
use nym_credentials::{IssuanceTicketBook, IssuedTicketBook};
use nym_ecash_time::Date;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
use zeroize::Zeroizing;
#[derive(Clone)]
pub struct CoconutCredentialManager {
inner: Arc<RwLock<CoconutCredentialManagerInner>>,
pub struct MemoryEcachTicketbookManager {
inner: Arc<RwLock<EcashCredentialManagerInner>>,
}
#[derive(Default)]
struct CoconutCredentialManagerInner {
credentials: Vec<StoredIssuedCredential>,
credential_usage: Vec<CredentialUsage>,
struct EcashCredentialManagerInner {
ticketbooks: HashMap<i64, RetrievedTicketbook>,
pending: HashMap<i64, RetrievedPendingTicketbook>,
master_vk: HashMap<u64, VerificationKeyAuth>,
coin_indices_sigs: HashMap<u64, Vec<AnnotatedCoinIndexSignature>>,
expiration_date_sigs: HashMap<Date, Vec<AnnotatedExpirationDateSignature>>,
_next_id: i64,
}
impl CoconutCredentialManagerInner {
impl EcashCredentialManagerInner {
fn next_id(&mut self) -> i64 {
let next = self._next_id;
self._next_id += 1;
@@ -25,108 +36,209 @@ impl CoconutCredentialManagerInner {
}
}
impl CoconutCredentialManager {
// hehe, that's hacky AF, but it works as a **TEMPORARY** workaround
fn hack_clone_ticketbook(book: &IssuedTicketBook) -> IssuedTicketBook {
let ser = book.pack();
let data = Zeroizing::new(ser.data);
IssuedTicketBook::try_unpack(&data, None).unwrap()
}
impl MemoryEcachTicketbookManager {
/// Creates new empty instance of the `CoconutCredentialManager`.
pub fn new() -> Self {
CoconutCredentialManager {
MemoryEcachTicketbookManager {
inner: Default::default(),
}
}
pub async fn insert_issued_credential(
&self,
credential_type: String,
serialization_revision: u8,
credential_data: &[u8],
epoch_id: u32,
) {
let mut inner = self.inner.write().await;
let id = inner.next_id();
inner.credentials.push(StoredIssuedCredential {
id,
serialization_revision,
credential_data: credential_data.to_vec(),
credential_type,
epoch_id,
expired: false,
})
}
async fn bandwidth_voucher_spent(&self, id: i64) -> bool {
self.inner
.read()
.await
.credential_usage
.iter()
.any(|c| c.credential_id == id)
}
async fn freepass_spent(&self, id: i64, gateway_id: &str) -> bool {
self.inner
.read()
.await
.credential_usage
.iter()
.any(|c| c.credential_id == id && c.gateway_id_bs58 == gateway_id)
}
/// Tries to retrieve one of the stored, unused credentials.
pub async fn get_next_unspect_bandwidth_voucher(&self) -> Option<StoredIssuedCredential> {
let guard = self.inner.read().await;
for credential in guard
.credentials
.iter()
.filter(|c| c.credential_type == "BandwidthVoucher")
{
if !self.bandwidth_voucher_spent(credential.id).await {
return Some(credential.clone());
}
}
None
}
pub async fn get_next_unspect_freepass(
&self,
gateway_id: &str,
) -> Option<StoredIssuedCredential> {
let guard = self.inner.read().await;
for credential in guard
.credentials
.iter()
.filter(|c| c.credential_type == "FreeBandwidthPass")
{
if credential.expired {
continue;
}
if !self.freepass_spent(credential.id, gateway_id).await {
return Some(credential.clone());
}
}
None
}
/// Consumes in the database the specified credential.
///
/// # Arguments
///
/// * `id`: Database id.
pub async fn consume_coconut_credential(&self, id: i64, gateway_id: &str) {
pub(crate) async fn cleanup_expired(&self) {
let mut guard = self.inner.write().await;
guard.credential_usage.push(CredentialUsage {
credential_id: id,
gateway_id_bs58: gateway_id.to_string(),
});
let mut to_remove = Vec::new();
for t in guard.ticketbooks.values() {
if t.ticketbook.expired() {
to_remove.push(t.ticketbook_id);
}
}
for id in to_remove {
guard.ticketbooks.remove(&id);
}
}
/// Marks the specified credential as expired
///
/// # Arguments
///
/// * `id`: Id of the credential to mark as expired.
pub async fn mark_expired(&self, id: i64) {
let mut creds = self.inner.write().await;
if let Some(cred) = creds.credentials.get_mut(id as usize) {
cred.expired = true;
pub async fn get_next_unspent_ticketbook_and_update(
&self,
tickets: u32,
) -> Option<RetrievedTicketbook> {
let mut guard = self.inner.write().await;
for t in guard.ticketbooks.values_mut() {
if !t.ticketbook.expired()
&& t.ticketbook.spent_tickets() + tickets as u64
<= t.ticketbook.params_total_tickets()
{
t.ticketbook
.update_spent_tickets(t.ticketbook.spent_tickets() + tickets as u64);
return Some(RetrievedTicketbook {
ticketbook_id: t.ticketbook_id,
ticketbook: hack_clone_ticketbook(&t.ticketbook),
});
}
}
None
}
pub(crate) async fn revert_ticketbook_withdrawal(
&self,
ticketbook_id: i64,
withdrawn: u32,
expected_current_total_spent: u32,
) -> bool {
let mut guard = self.inner.write().await;
let Some(book) = guard.ticketbooks.get_mut(&ticketbook_id) else {
return false;
};
if book.ticketbook.spent_tickets() == expected_current_total_spent as u64 {
book.ticketbook
.update_spent_tickets(book.ticketbook.spent_tickets() - withdrawn as u64);
true
} else {
false
}
}
pub(crate) async fn insert_pending_ticketbook(&self, ticketbook: &IssuanceTicketBook) {
let mut guard = self.inner.write().await;
let ser = ticketbook.pack();
let data = Zeroizing::new(ser.data);
let id = ticketbook.deposit_id() as i64;
guard.pending.insert(
id,
RetrievedPendingTicketbook {
pending_id: ticketbook.deposit_id() as i64,
pending_ticketbook: IssuanceTicketBook::try_unpack(&data, None).unwrap(),
},
);
}
pub(crate) async fn get_pending_ticketbooks(&self) -> Vec<RetrievedPendingTicketbook> {
let guard = self.inner.read().await;
let mut pending = Vec::new();
for p in guard.pending.values() {
// 🫠
let ser = p.pending_ticketbook.pack();
let data = Zeroizing::new(ser.data);
pending.push(RetrievedPendingTicketbook {
pending_id: p.pending_id,
pending_ticketbook: IssuanceTicketBook::try_unpack(&data, None).unwrap(),
})
}
pending
}
pub(crate) async fn remove_pending_ticketbook(&self, pending_id: i64) {
let mut guard = self.inner.write().await;
guard.pending.remove(&pending_id);
}
pub(crate) async fn insert_new_ticketbook(&self, ticketbook: &IssuedTicketBook) {
let mut guard = self.inner.write().await;
let id = guard.next_id();
// hehe, that's hacky AF, but it works as a **TEMPORARY** workaround
let ser = ticketbook.pack();
let data = Zeroizing::new(ser.data);
guard.ticketbooks.insert(
id,
RetrievedTicketbook {
ticketbook_id: id,
ticketbook: IssuedTicketBook::try_unpack(&data, None).unwrap(),
},
);
}
pub(crate) async fn get_ticketbooks_info(&self) -> Vec<BasicTicketbookInformation> {
let guard = self.inner.read().await;
guard
.ticketbooks
.values()
.map(|t| BasicTicketbookInformation {
id: t.ticketbook_id,
expiration_date: t.ticketbook.expiration_date(),
epoch_id: t.ticketbook.epoch_id() as u32,
total_tickets: t.ticketbook.spent_tickets() as u32,
used_tickets: t.ticketbook.params_total_tickets() as u32,
})
.collect()
}
pub(crate) async fn get_master_verification_key(
&self,
epoch_id: u64,
) -> Option<VerificationKeyAuth> {
let guard = self.inner.read().await;
guard.master_vk.get(&epoch_id).cloned()
}
pub(crate) async fn insert_master_verification_key(
&self,
epoch_id: u64,
key: &VerificationKeyAuth,
) {
let mut guard = self.inner.write().await;
guard.master_vk.insert(epoch_id, key.clone());
}
pub(crate) async fn get_coin_index_signatures(
&self,
epoch_id: u64,
) -> Option<Vec<AnnotatedCoinIndexSignature>> {
let guard = self.inner.read().await;
guard.coin_indices_sigs.get(&epoch_id).cloned()
}
pub(crate) async fn insert_coin_index_signatures(
&self,
epoch_id: u64,
sigs: &[AnnotatedCoinIndexSignature],
) {
let mut guard = self.inner.write().await;
guard.coin_indices_sigs.insert(epoch_id, sigs.to_vec());
}
pub(crate) async fn get_expiration_date_signatures(
&self,
expiration_date: Date,
) -> Option<Vec<AnnotatedExpirationDateSignature>> {
let guard = self.inner.read().await;
guard.expiration_date_sigs.get(&expiration_date).cloned()
}
pub(crate) async fn insert_expiration_date_signatures(
&self,
_epoch_id: u64,
expiration_date: Date,
sigs: &[AnnotatedExpirationDateSignature],
) {
let mut guard = self.inner.write().await;
guard
.expiration_date_sigs
.insert(expiration_date, sigs.to_vec());
}
}
@@ -2,5 +2,5 @@
// SPDX-License-Identifier: Apache-2.0
pub mod memory;
#[cfg(not(target_arch = "wasm32"))]
#[cfg(all(not(target_arch = "wasm32"), feature = "persistent-storage"))]
pub mod sqlite;
+225 -62
View File
@@ -1,116 +1,279 @@
// Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::models::StoredIssuedCredential;
use crate::models::{
BasicTicketbookInformation, RawExpirationDateSignatures, StoredIssuedTicketbook,
StoredPendingTicketbook,
};
use nym_ecash_time::Date;
use sqlx::{Executor, Sqlite, Transaction};
#[derive(Clone)]
pub struct CoconutCredentialManager {
pub struct SqliteEcashTicketbookManager {
connection_pool: sqlx::SqlitePool,
}
impl CoconutCredentialManager {
/// Creates new instance of the `CoconutCredentialManager` with the provided sqlite connection pool.
impl SqliteEcashTicketbookManager {
/// Creates new instance of the `EcashTicketbookManager` with the provided sqlite connection pool.
///
/// # Arguments
///
/// * `connection_pool`: database connection pool to use.
pub fn new(connection_pool: sqlx::SqlitePool) -> Self {
CoconutCredentialManager { connection_pool }
SqliteEcashTicketbookManager { connection_pool }
}
pub async fn insert_issued_credential(
pub(crate) async fn cleanup_expired(&self, deadline: Date) -> Result<(), sqlx::Error> {
sqlx::query!(
"DELETE FROM ecash_ticketbook WHERE expiration_date <= ?",
deadline
)
.execute(&self.connection_pool)
.await?;
Ok(())
}
pub(crate) async fn begin_storage_tx(&self) -> Result<Transaction<Sqlite>, sqlx::Error> {
self.connection_pool.begin().await
}
pub(crate) async fn insert_pending_ticketbook(
&self,
credential_type: String,
serialization_revision: u8,
credential_data: &[u8],
serialisation_revision: u8,
deposit_id: u32,
data: &[u8],
expiration_date: Date,
) -> Result<(), sqlx::Error> {
sqlx::query!(
r#"
INSERT INTO pending_issuance
(deposit_id, serialization_revision, pending_ticketbook_data, expiration_date)
VALUES (?, ?, ?, ?)
"#,
deposit_id,
serialisation_revision,
data,
expiration_date,
)
.execute(&self.connection_pool)
.await?;
Ok(())
}
pub(crate) async fn insert_new_ticketbook(
&self,
serialisation_revision: u8,
data: &[u8],
expiration_date: Date,
epoch_id: u32,
total_tickets: u32,
used_tickets: u32,
) -> Result<(), sqlx::Error> {
sqlx::query!(
r#"
INSERT INTO coconut_credentials(serialization_revision, credential_type, credential_data, epoch_id, expired)
VALUES (?, ?, ?, ?, false)
INSERT INTO ecash_ticketbook
(serialization_revision, ticketbook_data, expiration_date, epoch_id, total_tickets, used_tickets)
VALUES (?, ?, ?, ?, ?, ?)
"#,
serialization_revision, credential_type, credential_data, epoch_id
serialisation_revision,
data,
expiration_date,
epoch_id,
total_tickets,
used_tickets,
).execute(&self.connection_pool).await?;
Ok(())
}
pub async fn get_next_unspect_freepass(
pub(crate) async fn get_ticketbooks_info(
&self,
gateway_id: &str,
) -> Result<Option<StoredIssuedCredential>, sqlx::Error> {
// get a credential of freepass type that doesn't appear in `credential_usage` for the provided gateway_id
) -> Result<Vec<BasicTicketbookInformation>, sqlx::Error> {
sqlx::query_as(
r#"
SELECT *
FROM coconut_credentials
WHERE coconut_credentials.credential_type == "FreeBandwidthPass" AND coconut_credentials.expired = false
AND NOT EXISTS (SELECT 1
FROM credential_usage
WHERE credential_usage.credential_id = coconut_credentials.id
AND credential_usage.gateway_id_bs58 == ?)
ORDER BY coconut_credentials.id
LIMIT 1
"#,
SELECT id, expiration_date, epoch_id, total_tickets, used_tickets
FROM ecash_ticketbook
"#,
)
.bind(gateway_id)
.fetch_optional(&self.connection_pool)
.fetch_all(&self.connection_pool)
.await
}
pub async fn get_next_unspect_bandwidth_voucher(
pub(crate) async fn decrease_used_ticketbook_tickets(
&self,
) -> Result<Option<StoredIssuedCredential>, sqlx::Error> {
// get a credential of bandwidth voucher type that doesn't appear in `credential_usage` for any gateway_id
sqlx::query_as(
ticketbook_id: i64,
reverted_spent: u32,
expected_current_total_spent: u32,
) -> Result<bool, sqlx::Error> {
// the 'AND' clause will ensure this will only be executed if nobody else interacted with the row
let affected = sqlx::query!(
r#"
SELECT *
FROM coconut_credentials
WHERE coconut_credentials.credential_type == "BandwidthVoucher"
AND NOT EXISTS (SELECT 1
FROM credential_usage
WHERE credential_usage.credential_id = coconut_credentials.id)
ORDER BY coconut_credentials.id
LIMIT 1
UPDATE ecash_ticketbook
SET used_tickets = used_tickets - ?
WHERE id = ?
AND used_tickets = ?
"#,
reverted_spent,
ticketbook_id,
expected_current_total_spent
)
.fetch_optional(&self.connection_pool)
.await
.execute(&self.connection_pool)
.await?
.rows_affected();
Ok(affected > 0)
}
/// Consumes in the database the specified credential.
///
/// # Arguments
///
/// * `id`: Database id.
/// * `gateway_id`: id of the gateway that received the credential
pub async fn consume_coconut_credential(
pub(crate) async fn get_pending_ticketbooks(
&self,
id: i64,
gateway_id: &str,
) -> Result<Vec<StoredPendingTicketbook>, sqlx::Error> {
sqlx::query_as("SELECT * FROM pending_issuance")
.fetch_all(&self.connection_pool)
.await
}
pub(crate) async fn remove_pending_ticketbook(
&self,
pending_id: i64,
) -> Result<(), sqlx::Error> {
sqlx::query!(
"INSERT INTO credential_usage (credential_id, gateway_id_bs58) VALUES (?, ?)",
id,
gateway_id
"DELETE FROM pending_issuance WHERE deposit_id = ?",
pending_id
)
.execute(&self.connection_pool)
.await?;
Ok(())
}
/// Marks the specified credential as expired
///
/// # Arguments
///
/// * `id`: Id of the credential to mark as expired.
pub async fn mark_expired(&self, id: i64) -> Result<(), sqlx::Error> {
pub(crate) async fn get_master_verification_key(
&self,
epoch_id: i64,
) -> Result<Option<Vec<u8>>, sqlx::Error> {
sqlx::query!(
"UPDATE coconut_credentials SET expired = TRUE WHERE id = ?",
id
"SELECT serialised_key FROM master_verification_key WHERE epoch_id = ?",
epoch_id
)
.fetch_optional(&self.connection_pool)
.await
.map(|maybe_record| maybe_record.map(|r| r.serialised_key))
}
pub(crate) async fn insert_master_verification_key(
&self,
epoch_id: i64,
data: &[u8],
) -> Result<(), sqlx::Error> {
sqlx::query!(
"INSERT INTO master_verification_key(epoch_id, serialised_key) VALUES (?, ?)",
epoch_id,
data
)
.execute(&self.connection_pool)
.await?;
Ok(())
}
pub(crate) async fn get_coin_index_signatures(
&self,
epoch_id: i64,
) -> Result<Option<Vec<u8>>, sqlx::Error> {
sqlx::query!(
"SELECT serialised_signatures FROM coin_indices_signatures WHERE epoch_id = ?",
epoch_id
)
.fetch_optional(&self.connection_pool)
.await
.map(|maybe_record| maybe_record.map(|r| r.serialised_signatures))
}
pub(crate) async fn insert_coin_index_signatures(
&self,
epoch_id: i64,
data: &[u8],
) -> Result<(), sqlx::Error> {
sqlx::query!(
"INSERT INTO coin_indices_signatures(epoch_id, serialised_signatures) VALUES (?, ?)",
epoch_id,
data
)
.execute(&self.connection_pool)
.await?;
Ok(())
}
pub(crate) async fn get_expiration_date_signatures(
&self,
expiration_date: Date,
) -> Result<Option<RawExpirationDateSignatures>, sqlx::Error> {
sqlx::query_as!(
RawExpirationDateSignatures,
r#"
SELECT epoch_id as "epoch_id: u32", serialised_signatures
FROM expiration_date_signatures
WHERE expiration_date = ?
"#,
expiration_date
)
.fetch_optional(&self.connection_pool)
.await
}
pub(crate) async fn insert_expiration_date_signatures(
&self,
epoch_id: i64,
expiration_date: Date,
data: &[u8],
) -> Result<(), sqlx::Error> {
sqlx::query!(
"INSERT INTO expiration_date_signatures(expiration_date, epoch_id, serialised_signatures) VALUES (?, ?, ?)",
expiration_date,
epoch_id,
data
)
.execute(&self.connection_pool)
.await?;
Ok(())
}
}
pub(crate) async fn get_next_unspent_ticketbook<'a, E>(
executor: E,
deadline: Date,
tickets: u32,
) -> Result<Option<StoredIssuedTicketbook>, sqlx::Error>
where
E: Executor<'a, Database = Sqlite>,
{
sqlx::query_as(
r#"
SELECT *
FROM ecash_ticketbook
WHERE used_tickets + ? <= total_tickets
AND expiration_date >= ?
ORDER BY expiration_date ASC
LIMIT 1
"#,
)
.bind(tickets)
.bind(deadline)
.fetch_optional(executor)
.await
}
pub(crate) async fn increase_used_ticketbook_tickets<'a, E>(
executor: E,
ticketbook_id: i64,
extra_spent: u32,
) -> Result<(), sqlx::Error>
where
E: Executor<'a, Database = Sqlite>,
{
sqlx::query!(
"UPDATE ecash_ticketbook SET used_tickets = used_tickets + ? WHERE id = ?",
extra_spent,
ticketbook_id
)
.execute(executor)
.await?;
Ok(())
}
@@ -1,26 +1,30 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::fmt::{self, Debug, Formatter};
use crate::backends::memory::CoconutCredentialManager;
use crate::backends::memory::MemoryEcachTicketbookManager;
use crate::error::StorageError;
use crate::models::{StorableIssuedCredential, StoredIssuedCredential};
use crate::models::{BasicTicketbookInformation, RetrievedPendingTicketbook, RetrievedTicketbook};
use crate::storage::Storage;
use async_trait::async_trait;
use nym_compact_ecash::scheme::coin_indices_signatures::AnnotatedCoinIndexSignature;
use nym_compact_ecash::scheme::expiration_date_signatures::AnnotatedExpirationDateSignature;
use nym_compact_ecash::VerificationKeyAuth;
use nym_credentials::{IssuanceTicketBook, IssuedTicketBook};
use nym_ecash_time::Date;
use std::fmt::{self, Debug, Formatter};
pub type EphemeralCredentialStorage = EphemeralStorage;
// note that clone here is fine as upon cloning the same underlying pool will be used
#[derive(Clone)]
pub struct EphemeralStorage {
coconut_credential_manager: CoconutCredentialManager,
storage_manager: MemoryEcachTicketbookManager,
}
impl Default for EphemeralStorage {
fn default() -> Self {
EphemeralStorage {
coconut_credential_manager: CoconutCredentialManager::new(),
storage_manager: MemoryEcachTicketbookManager::new(),
}
}
}
@@ -35,55 +39,135 @@ impl Debug for EphemeralStorage {
impl Storage for EphemeralStorage {
type StorageError = StorageError;
async fn insert_issued_credential<'a>(
async fn cleanup_expired(&self) -> Result<(), Self::StorageError> {
self.storage_manager.cleanup_expired().await;
Ok(())
}
async fn insert_pending_ticketbook(
&self,
bandwidth_credential: StorableIssuedCredential<'a>,
) -> Result<(), StorageError> {
self.coconut_credential_manager
.insert_issued_credential(
bandwidth_credential.credential_type,
bandwidth_credential.serialization_revision,
bandwidth_credential.credential_data,
bandwidth_credential.epoch_id,
)
ticketbook: &IssuanceTicketBook,
) -> Result<(), Self::StorageError> {
self.storage_manager
.insert_pending_ticketbook(ticketbook)
.await;
Ok(())
}
async fn get_next_unspent_credential(
async fn insert_issued_ticketbook(
&self,
gateway_id: &str,
) -> Result<Option<StoredIssuedCredential>, Self::StorageError> {
// first try to get a free pass if available, otherwise fallback to bandwidth voucher
let maybe_freepass = self
.coconut_credential_manager
.get_next_unspect_freepass(gateway_id)
.await;
if maybe_freepass.is_some() {
return Ok(maybe_freepass);
}
ticketbook: &IssuedTicketBook,
) -> Result<(), StorageError> {
self.storage_manager.insert_new_ticketbook(ticketbook).await;
Ok(())
}
async fn get_ticketbooks_info(
&self,
) -> Result<Vec<BasicTicketbookInformation>, Self::StorageError> {
Ok(self.storage_manager.get_ticketbooks_info().await)
}
async fn get_pending_ticketbooks(
&self,
) -> Result<Vec<RetrievedPendingTicketbook>, Self::StorageError> {
Ok(self.storage_manager.get_pending_ticketbooks().await)
}
async fn remove_pending_ticketbook(&self, pending_id: i64) -> Result<(), Self::StorageError> {
self.storage_manager
.remove_pending_ticketbook(pending_id)
.await;
Ok(())
}
/// Tries to retrieve one of the stored ticketbook,
/// that has not yet expired and has required number of unspent tickets.
/// it immediately updated the on-disk number of used tickets so that another task
/// could obtain their own tickets at the same time
async fn get_next_unspent_usable_ticketbook(
&self,
tickets: u32,
) -> Result<Option<RetrievedTicketbook>, Self::StorageError> {
Ok(self
.coconut_credential_manager
.get_next_unspect_bandwidth_voucher()
.storage_manager
.get_next_unspent_ticketbook_and_update(tickets)
.await)
}
async fn consume_coconut_credential(
async fn attempt_revert_ticketbook_withdrawal(
&self,
id: i64,
gateway_id: &str,
) -> Result<(), StorageError> {
self.coconut_credential_manager
.consume_coconut_credential(id, gateway_id)
.await;
ticketbook_id: i64,
previous_total_spent: u32,
withdrawn: u32,
) -> Result<bool, Self::StorageError> {
Ok(self
.storage_manager
.revert_ticketbook_withdrawal(ticketbook_id, previous_total_spent, withdrawn)
.await)
}
async fn get_master_verification_key(
&self,
epoch_id: u64,
) -> Result<Option<VerificationKeyAuth>, Self::StorageError> {
Ok(self
.storage_manager
.get_master_verification_key(epoch_id)
.await)
}
async fn insert_master_verification_key(
&self,
epoch_id: u64,
key: &VerificationKeyAuth,
) -> Result<(), Self::StorageError> {
self.storage_manager
.insert_master_verification_key(epoch_id, key)
.await;
Ok(())
}
async fn mark_expired(&self, id: i64) -> Result<(), Self::StorageError> {
self.coconut_credential_manager.mark_expired(id).await;
async fn get_coin_index_signatures(
&self,
epoch_id: u64,
) -> Result<Option<Vec<AnnotatedCoinIndexSignature>>, Self::StorageError> {
Ok(self
.storage_manager
.get_coin_index_signatures(epoch_id)
.await)
}
async fn insert_coin_index_signatures(
&self,
epoch_id: u64,
data: &[AnnotatedCoinIndexSignature],
) -> Result<(), Self::StorageError> {
self.storage_manager
.insert_coin_index_signatures(epoch_id, data)
.await;
Ok(())
}
async fn get_expiration_date_signatures(
&self,
expiration_date: Date,
) -> Result<Option<Vec<AnnotatedExpirationDateSignature>>, Self::StorageError> {
Ok(self
.storage_manager
.get_expiration_date_signatures(expiration_date)
.await)
}
async fn insert_expiration_date_signatures(
&self,
epoch_id: u64,
expiration_date: Date,
data: &[AnnotatedExpirationDateSignature],
) -> Result<(), Self::StorageError> {
self.storage_manager
.insert_expiration_date_signatures(epoch_id, expiration_date, data)
.await;
Ok(())
}
}
+14
View File
@@ -9,6 +9,9 @@ pub enum StorageError {
#[error("Database experienced an internal error - {0}")]
InternalDatabaseError(#[from] sqlx::Error),
#[error("experienced internal storage error due to database inconsistency: {reason}")]
DatabaseInconsistency { reason: String },
#[cfg(not(target_arch = "wasm32"))]
#[error("Failed to perform database migration - {0}")]
MigrationError(#[from] sqlx::migrate::MigrateError),
@@ -19,6 +22,17 @@ pub enum StorageError {
#[error("No unused credential in database. You need to buy at least one")]
NoCredential,
#[error("No signatures for epoch {epoch_id} in the database")]
NoSignatures { epoch_id: i64 },
#[error("Database unique constraint violation. Is the credential already imported?")]
ConstraintUnique,
}
impl StorageError {
pub fn database_inconsistency<S: Into<String>>(reason: S) -> StorageError {
StorageError::DatabaseInconsistency {
reason: reason.into(),
}
}
}
+7 -8
View File
@@ -5,21 +5,20 @@
use crate::ephemeral_storage::EphemeralStorage;
#[cfg(not(target_arch = "wasm32"))]
use crate::persistent_storage::PersistentStorage;
#[cfg(not(target_arch = "wasm32"))]
use std::path::Path;
mod backends;
pub mod ephemeral_storage;
pub mod error;
pub mod models;
#[cfg(not(target_arch = "wasm32"))]
#[cfg(all(not(target_arch = "wasm32"), feature = "persistent-storage"))]
pub mod persistent_storage;
pub mod storage;
#[cfg(not(target_arch = "wasm32"))]
pub async fn initialise_persistent_storage<P: AsRef<Path>>(path: P) -> PersistentStorage {
#[cfg(all(not(target_arch = "wasm32"), feature = "persistent-storage"))]
pub async fn initialise_persistent_storage<P: AsRef<std::path::Path>>(
path: P,
) -> crate::persistent_storage::PersistentStorage {
match persistent_storage::PersistentStorage::init(path).await {
Err(err) => panic!("failed to initialise credential storage - {err}"),
Ok(storage) => storage,
+44 -26
View File
@@ -1,44 +1,62 @@
// Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_credentials::{IssuanceTicketBook, IssuedTicketBook};
use nym_ecash_time::Date;
use zeroize::{Zeroize, ZeroizeOnDrop};
// #[derive(Clone)]
// pub struct CoconutCredential {
// #[allow(dead_code)]
// pub id: i64,
// pub voucher_value: String,
// pub voucher_info: String,
// pub serial_number: String,
// pub binding_number: String,
// pub signature: String,
// pub epoch_id: String,
// pub consumed: bool,
// }
pub struct RetrievedTicketbook {
pub ticketbook_id: i64,
pub ticketbook: IssuedTicketBook,
}
pub struct RetrievedPendingTicketbook {
pub pending_id: i64,
pub pending_ticketbook: IssuanceTicketBook,
}
#[cfg_attr(not(target_arch = "wasm32"), derive(sqlx::FromRow))]
pub struct BasicTicketbookInformation {
pub id: i64,
pub expiration_date: Date,
pub epoch_id: u32,
pub total_tickets: u32,
pub used_tickets: u32,
}
#[cfg_attr(not(target_arch = "wasm32"), derive(sqlx::FromRow))]
#[derive(Zeroize, ZeroizeOnDrop, Clone)]
pub struct StoredIssuedCredential {
pub struct StoredIssuedTicketbook {
pub id: i64,
pub serialization_revision: u8,
pub credential_data: Vec<u8>,
pub credential_type: String,
pub ticketbook_data: Vec<u8>,
#[zeroize(skip)]
pub expiration_date: Date,
pub epoch_id: u32,
pub expired: bool,
}
pub struct StorableIssuedCredential<'a> {
pub serialization_revision: u8,
pub credential_data: &'a [u8],
pub credential_type: String,
pub epoch_id: u32,
pub total_tickets: u32,
pub used_tickets: u32,
}
#[cfg_attr(not(target_arch = "wasm32"), derive(sqlx::FromRow))]
pub struct CredentialUsage {
pub credential_id: i64,
pub gateway_id_bs58: String,
#[derive(Zeroize, ZeroizeOnDrop, Clone)]
pub struct StoredPendingTicketbook {
pub deposit_id: i64,
pub serialization_revision: u8,
pub pending_ticketbook_data: Vec<u8>,
#[zeroize(skip)]
pub expiration_date: Date,
}
#[cfg_attr(not(target_arch = "wasm32"), derive(sqlx::FromRow))]
pub struct RawExpirationDateSignatures {
pub epoch_id: u32,
pub serialised_signatures: Vec<u8>,
}
@@ -1,125 +0,0 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::backends::sqlite::CoconutCredentialManager;
use crate::error::StorageError;
use crate::storage::Storage;
use crate::models::{StorableIssuedCredential, StoredIssuedCredential};
use async_trait::async_trait;
use log::{debug, error};
use sqlx::ConnectOptions;
use std::path::Path;
// note that clone here is fine as upon cloning the same underlying pool will be used
#[derive(Clone)]
pub struct PersistentStorage {
coconut_credential_manager: CoconutCredentialManager,
}
impl PersistentStorage {
/// Initialises `PersistentStorage` using the provided path.
///
/// # Arguments
///
/// * `database_path`: path to the database.
pub async fn init<P: AsRef<Path>>(database_path: P) -> Result<Self, StorageError> {
debug!(
"Attempting to connect to database {:?}",
database_path.as_ref().as_os_str()
);
let mut opts = sqlx::sqlite::SqliteConnectOptions::new()
.filename(database_path)
.create_if_missing(true);
opts.disable_statement_logging();
let connection_pool = match sqlx::SqlitePool::connect_with(opts).await {
Ok(db) => db,
Err(err) => {
error!("Failed to connect to SQLx database: {err}");
return Err(err.into());
}
};
if let Err(err) = sqlx::migrate!("./migrations").run(&connection_pool).await {
error!("Failed to perform migration on the SQLx database: {err}");
return Err(err.into());
}
Ok(PersistentStorage {
coconut_credential_manager: CoconutCredentialManager::new(connection_pool.clone()),
})
}
}
#[async_trait]
impl Storage for PersistentStorage {
type StorageError = StorageError;
async fn insert_issued_credential<'a>(
&self,
bandwidth_credential: StorableIssuedCredential<'a>,
) -> Result<(), Self::StorageError> {
self.coconut_credential_manager
.insert_issued_credential(
bandwidth_credential.credential_type,
bandwidth_credential.serialization_revision,
bandwidth_credential.credential_data,
bandwidth_credential.epoch_id,
)
.await
.map_err(|err| {
// There is one error we want to handle specifically.
// Check if database_error is `SqliteError` with code 2067 which
// means UNIQUE constraint violation
if let Some(db_error) = err.as_database_error() {
if db_error.code().map_or(false, |code| code == "2067") {
StorageError::ConstraintUnique
} else {
err.into()
}
} else {
err.into()
}
})
}
async fn get_next_unspent_credential(
&self,
gateway_id: &str,
) -> Result<Option<StoredIssuedCredential>, Self::StorageError> {
// first try to get a free pass if available, otherwise fallback to bandwidth voucher
let maybe_freepass = self
.coconut_credential_manager
.get_next_unspect_freepass(gateway_id)
.await?;
if maybe_freepass.is_some() {
return Ok(maybe_freepass);
}
Ok(self
.coconut_credential_manager
.get_next_unspect_bandwidth_voucher()
.await?)
}
async fn consume_coconut_credential(
&self,
id: i64,
gateway_id: &str,
) -> Result<(), StorageError> {
self.coconut_credential_manager
.consume_coconut_credential(id, gateway_id)
.await?;
Ok(())
}
async fn mark_expired(&self, id: i64) -> Result<(), Self::StorageError> {
self.coconut_credential_manager.mark_expired(id).await?;
Ok(())
}
}
@@ -0,0 +1,54 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::error::StorageError;
use bincode::Options;
use nym_compact_ecash::scheme::coin_indices_signatures::AnnotatedCoinIndexSignature;
use nym_compact_ecash::scheme::expiration_date_signatures::AnnotatedExpirationDateSignature;
use serde::{Deserialize, Serialize};
#[derive(Serialize)]
struct StorageBorrowedSerdeWrapper<'a, T>(&'a T);
#[derive(Serialize, Deserialize)]
struct StorageSerdeWrapper<T>(T);
pub(crate) fn serialise_coin_index_signatures(sigs: &[AnnotatedCoinIndexSignature]) -> Vec<u8> {
storage_serialiser()
.serialize(&StorageBorrowedSerdeWrapper(&sigs))
.unwrap()
}
pub(crate) fn deserialise_coin_index_signatures(
raw: &[u8],
) -> Result<Vec<AnnotatedCoinIndexSignature>, StorageError> {
let de: StorageSerdeWrapper<_> = storage_serialiser().deserialize(raw).map_err(|_| {
StorageError::database_inconsistency("malformed stored coin index signatures")
})?;
Ok(de.0)
}
pub(crate) fn serialise_expiration_date_signatures(
sigs: &[AnnotatedExpirationDateSignature],
) -> Vec<u8> {
storage_serialiser()
.serialize(&StorageBorrowedSerdeWrapper(&sigs))
.unwrap()
}
pub(crate) fn deserialise_expiration_date_signatures(
raw: &[u8],
) -> Result<Vec<AnnotatedExpirationDateSignature>, StorageError> {
let de: StorageSerdeWrapper<_> = storage_serialiser().deserialize(raw).map_err(|_| {
StorageError::database_inconsistency("malformed expiration date signatures")
})?;
Ok(de.0)
}
// storage serialiser used for non-critical data, such as global expiration signatures or master verification keys,
// i.e. data that could always be queried for again if malformed
fn storage_serialiser() -> impl bincode::Options {
bincode::DefaultOptions::new()
.with_big_endian()
.with_varint_encoding()
}
@@ -0,0 +1,307 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::backends::sqlite::{
get_next_unspent_ticketbook, increase_used_ticketbook_tickets, SqliteEcashTicketbookManager,
};
use crate::error::StorageError;
use crate::models::{BasicTicketbookInformation, RetrievedPendingTicketbook, RetrievedTicketbook};
use crate::persistent_storage::helpers::{
deserialise_coin_index_signatures, deserialise_expiration_date_signatures,
serialise_coin_index_signatures, serialise_expiration_date_signatures,
};
use crate::storage::Storage;
use async_trait::async_trait;
use log::{debug, error};
use nym_compact_ecash::scheme::coin_indices_signatures::AnnotatedCoinIndexSignature;
use nym_compact_ecash::scheme::expiration_date_signatures::AnnotatedExpirationDateSignature;
use nym_compact_ecash::VerificationKeyAuth;
use nym_credentials::ecash::bandwidth::serialiser::VersionedSerialise;
use nym_credentials::{IssuanceTicketBook, IssuedTicketBook};
use nym_ecash_time::{ecash_today, Date, EcashTime};
use sqlx::ConnectOptions;
use std::path::Path;
use zeroize::Zeroizing;
mod helpers;
// note that clone here is fine as upon cloning the same underlying pool will be used
#[derive(Clone)]
pub struct PersistentStorage {
storage_manager: SqliteEcashTicketbookManager,
}
impl PersistentStorage {
/// Initialises `PersistentStorage` using the provided path.
///
/// # Arguments
///
/// * `database_path`: path to the database.
pub async fn init<P: AsRef<Path>>(database_path: P) -> Result<Self, StorageError> {
debug!(
"Attempting to connect to database {:?}",
database_path.as_ref().as_os_str()
);
let mut opts = sqlx::sqlite::SqliteConnectOptions::new()
.filename(database_path)
.create_if_missing(true);
opts.disable_statement_logging();
let connection_pool = match sqlx::SqlitePool::connect_with(opts).await {
Ok(db) => db,
Err(err) => {
error!("Failed to connect to SQLx database: {err}");
return Err(err.into());
}
};
if let Err(err) = sqlx::migrate!("./migrations").run(&connection_pool).await {
error!("Failed to perform migration on the SQLx database: {err}");
return Err(err.into());
}
Ok(PersistentStorage {
storage_manager: SqliteEcashTicketbookManager::new(connection_pool.clone()),
})
}
}
#[async_trait]
impl Storage for PersistentStorage {
type StorageError = StorageError;
/// remove all expired ticketbooks and expiration date signatures
async fn cleanup_expired(&self) -> Result<(), Self::StorageError> {
let ecash_yesterday = ecash_today().date().previous_day().unwrap();
self.storage_manager
.cleanup_expired(ecash_yesterday)
.await?;
Ok(())
}
async fn insert_pending_ticketbook(
&self,
ticketbook: &IssuanceTicketBook,
) -> Result<(), Self::StorageError> {
let ser = ticketbook.pack();
let data = Zeroizing::new(ser.data);
let serialisation_revision = ser.revision;
self.storage_manager
.insert_pending_ticketbook(
serialisation_revision,
ticketbook.deposit_id(),
&data,
ticketbook.expiration_date(),
)
.await?;
Ok(())
}
async fn insert_issued_ticketbook(
&self,
ticketbook: &IssuedTicketBook,
) -> Result<(), Self::StorageError> {
let ser = ticketbook.pack();
let data = Zeroizing::new(ser.data);
let serialisation_revision = ser.revision;
self.storage_manager
.insert_new_ticketbook(
serialisation_revision,
&data,
ticketbook.expiration_date(),
ticketbook.epoch_id() as u32,
ticketbook.params_total_tickets() as u32,
ticketbook.spent_tickets() as u32,
)
.await?;
Ok(())
}
async fn get_ticketbooks_info(
&self,
) -> Result<Vec<BasicTicketbookInformation>, Self::StorageError> {
Ok(self.storage_manager.get_ticketbooks_info().await?)
}
async fn get_pending_ticketbooks(
&self,
) -> Result<Vec<RetrievedPendingTicketbook>, Self::StorageError> {
let pending = self
.storage_manager
.get_pending_ticketbooks()
.await?
.into_iter()
.map(|p| {
IssuanceTicketBook::try_unpack(&p.pending_ticketbook_data, p.serialization_revision)
.map_err(|err| {
StorageError::database_inconsistency(format!(
"failed to deserialise stored pending ticketbook: {err}"
))
})
.map(|pending_ticketbook| RetrievedPendingTicketbook {
pending_id: p.deposit_id,
pending_ticketbook,
})
})
.collect::<Result<_, _>>()?;
Ok(pending)
}
async fn remove_pending_ticketbook(&self, pending_id: i64) -> Result<(), Self::StorageError> {
self.storage_manager
.remove_pending_ticketbook(pending_id)
.await?;
Ok(())
}
/// Tries to retrieve one of the stored ticketbook,
/// that has not yet expired and has required number of unspent tickets.
/// it immediately updated the on-disk number of used tickets so that another task
/// could obtain their own tickets at the same time
async fn get_next_unspent_usable_ticketbook(
&self,
tickets: u32,
) -> Result<Option<RetrievedTicketbook>, Self::StorageError> {
let deadline = ecash_today().ecash_date();
let mut tx = self.storage_manager.begin_storage_tx().await?;
// we don't want ticketbooks with expiration in the past
let Some(raw) = get_next_unspent_ticketbook(&mut tx, deadline, tickets).await? else {
// make sure to finish our tx
tx.commit().await?;
return Ok(None);
};
let mut deserialised =
IssuedTicketBook::try_unpack(&raw.ticketbook_data, raw.serialization_revision)
.map_err(|err| {
StorageError::database_inconsistency(format!(
"failed to deserialise stored ticketbook: {err}"
))
})?;
increase_used_ticketbook_tickets(&mut tx, raw.id, tickets).await?;
tx.commit().await?;
// set the number of spent tickets on the crypto object
// TODO: I don't like how that's required and can be easily missed,
// perhaps we shouldn't be storing the `IssuedTicketBook` data in the db,
// but all of its fields instead?
deserialised.update_spent_tickets(raw.used_tickets as u64);
Ok(Some(RetrievedTicketbook {
ticketbook_id: raw.id,
ticketbook: deserialised,
}))
}
async fn attempt_revert_ticketbook_withdrawal(
&self,
ticketbook_id: i64,
withdrawn: u32,
expected_current_total_spent: u32,
) -> Result<bool, Self::StorageError> {
Ok(self
.storage_manager
.decrease_used_ticketbook_tickets(
ticketbook_id,
withdrawn,
expected_current_total_spent,
)
.await?)
}
async fn get_master_verification_key(
&self,
epoch_id: u64,
) -> Result<Option<VerificationKeyAuth>, Self::StorageError> {
let Some(raw) = self
.storage_manager
.get_master_verification_key(epoch_id as i64)
.await?
else {
return Ok(None);
};
let master_vk = VerificationKeyAuth::from_bytes(&raw).map_err(|_| {
StorageError::database_inconsistency("malformed stored master verification key")
})?;
Ok(Some(master_vk))
}
async fn insert_master_verification_key(
&self,
epoch_id: u64,
key: &VerificationKeyAuth,
) -> Result<(), Self::StorageError> {
Ok(self
.storage_manager
.insert_master_verification_key(epoch_id as i64, &key.to_bytes())
.await?)
}
async fn get_coin_index_signatures(
&self,
epoch_id: u64,
) -> Result<Option<Vec<AnnotatedCoinIndexSignature>>, Self::StorageError> {
let Some(raw) = self
.storage_manager
.get_coin_index_signatures(epoch_id as i64)
.await?
else {
return Ok(None);
};
Ok(Some(deserialise_coin_index_signatures(&raw)?))
}
async fn insert_coin_index_signatures(
&self,
epoch_id: u64,
sigs: &[AnnotatedCoinIndexSignature],
) -> Result<(), Self::StorageError> {
self.storage_manager
.insert_coin_index_signatures(epoch_id as i64, &serialise_coin_index_signatures(sigs))
.await?;
Ok(())
}
async fn get_expiration_date_signatures(
&self,
expiration_date: Date,
) -> Result<Option<Vec<AnnotatedExpirationDateSignature>>, Self::StorageError> {
let Some(raw) = self
.storage_manager
.get_expiration_date_signatures(expiration_date)
.await?
else {
return Ok(None);
};
Ok(Some(deserialise_expiration_date_signatures(
&raw.serialised_signatures,
)?))
}
async fn insert_expiration_date_signatures(
&self,
epoch_id: u64,
expiration_date: Date,
sigs: &[AnnotatedExpirationDateSignature],
) -> Result<(), Self::StorageError> {
self.storage_manager
.insert_expiration_date_signatures(
epoch_id as i64,
expiration_date,
&serialise_expiration_date_signatures(sigs),
)
.await?;
Ok(())
}
}
+77 -26
View File
@@ -1,42 +1,93 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::models::{StorableIssuedCredential, StoredIssuedCredential};
use crate::models::{BasicTicketbookInformation, RetrievedPendingTicketbook, RetrievedTicketbook};
use async_trait::async_trait;
use nym_compact_ecash::scheme::coin_indices_signatures::AnnotatedCoinIndexSignature;
use nym_compact_ecash::scheme::expiration_date_signatures::AnnotatedExpirationDateSignature;
use nym_compact_ecash::VerificationKeyAuth;
use nym_credentials::{IssuanceTicketBook, IssuedTicketBook};
use nym_ecash_time::Date;
use std::error::Error;
// for future reference, if you want to make a query for "how much bandwidth do we have left"
// do something along the lines of
// `SELECT total_tickets, used_tickets FROM ecash_ticketbook WHERE expiration_date >= ?`, today_date
// then for each calculate the diff total_tickets - used_tickets and multiply the result by the size of the ticket
#[async_trait]
pub trait Storage: Send + Sync {
type StorageError: Error;
async fn insert_issued_credential<'a>(
/// remove all expired ticketbooks and expiration date signatures
async fn cleanup_expired(&self) -> Result<(), Self::StorageError>;
async fn insert_pending_ticketbook(
&self,
bandwidth_credential: StorableIssuedCredential<'a>,
ticketbook: &IssuanceTicketBook,
) -> Result<(), Self::StorageError>;
/// Tries to retrieve one of the stored, unused credentials,
/// that is also not marked as expired
async fn get_next_unspent_credential(
async fn insert_issued_ticketbook(
&self,
gateway_id: &str,
) -> Result<Option<StoredIssuedCredential>, Self::StorageError>;
/// Marks as consumed in the database the specified credential.
///
/// # Arguments
///
/// * `id`: Id of the credential to be consumed.
/// * `gateway_id`: id of the gateway that received the credential.
async fn consume_coconut_credential(
&self,
id: i64,
gateway_id: &str,
ticketbook: &IssuedTicketBook,
) -> Result<(), Self::StorageError>;
/// Marks the specified credential as expired
///
/// # Arguments
///
/// * `id`: Id of the credential to mark as expired.
async fn mark_expired(&self, id: i64) -> Result<(), Self::StorageError>;
async fn get_ticketbooks_info(
&self,
) -> Result<Vec<BasicTicketbookInformation>, Self::StorageError>;
async fn get_pending_ticketbooks(
&self,
) -> Result<Vec<RetrievedPendingTicketbook>, Self::StorageError>;
async fn remove_pending_ticketbook(&self, pending_id: i64) -> Result<(), Self::StorageError>;
/// Tries to retrieve one of the stored ticketbook,
/// that has not yet expired and has required number of unspent tickets.
/// it immediately updated the on-disk number of used tickets so that another task
/// could obtain their own tickets at the same time
async fn get_next_unspent_usable_ticketbook(
&self,
tickets: u32,
) -> Result<Option<RetrievedTicketbook>, Self::StorageError>;
async fn attempt_revert_ticketbook_withdrawal(
&self,
ticketbook_id: i64,
withdrawn: u32,
expected_current_total_spent: u32,
) -> Result<bool, Self::StorageError>;
async fn get_master_verification_key(
&self,
epoch_id: u64,
) -> Result<Option<VerificationKeyAuth>, Self::StorageError>;
async fn insert_master_verification_key(
&self,
epoch_id: u64,
key: &VerificationKeyAuth,
) -> Result<(), Self::StorageError>;
async fn get_coin_index_signatures(
&self,
epoch_id: u64,
) -> Result<Option<Vec<AnnotatedCoinIndexSignature>>, Self::StorageError>;
async fn insert_coin_index_signatures(
&self,
epoch_id: u64,
data: &[AnnotatedCoinIndexSignature],
) -> Result<(), Self::StorageError>;
async fn get_expiration_date_signatures(
&self,
expiration_date: Date,
) -> Result<Option<Vec<AnnotatedExpirationDateSignature>>, Self::StorageError>;
async fn insert_expiration_date_signatures(
&self,
epoch_id: u64,
expiration_date: Date,
data: &[AnnotatedExpirationDateSignature],
) -> Result<(), Self::StorageError>;
}
+4 -2
View File
@@ -10,11 +10,13 @@ license.workspace = true
log = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true }
time.workspace = true
nym-bandwidth-controller = { path = "../../common/bandwidth-controller" }
nym-coconut = { path = "../nymcoconut" }
nym-credentials = { path = "../../common/credentials" }
nym-credential-storage = { path = "../../common/credential-storage" }
nym-credential-storage = { path = "../../common/credential-storage", features = ["persistent-storage"] }
nym-validator-client = { path = "../../common/client-libs/validator-client" }
nym-config = { path = "../../common/config" }
nym-client-core = { path = "../../common/client-core" }
nym-compact-ecash = { path = "../../common/nym_offline_compact_ecash" }
nym-ecash-time = { path = "../../common/ecash-time" }
+18 -2
View File
@@ -3,6 +3,7 @@
use nym_credential_storage::error::StorageError;
use nym_credentials::error::Error as CredentialError;
use nym_validator_client::coconut::EcashApiError;
use nym_validator_client::nyxd::error::NyxdError;
use std::num::ParseIntError;
use thiserror::Error;
@@ -17,15 +18,30 @@ pub enum Error {
#[error(transparent)]
BandwidthControllerError(#[from] nym_bandwidth_controller::error::BandwidthControllerError),
#[error(transparent)]
EcashApiError(#[from] EcashApiError),
#[error(transparent)]
Nyxd(#[from] NyxdError),
#[error(transparent)]
Credential(#[from] CredentialError),
#[error("Could not use shared storage: {0}")]
SharedStorageError(#[from] StorageError),
#[error("could not use shared storage: {0}")]
SharedStorageError(Box<dyn std::error::Error + Send + Sync>),
#[error("failed to parse credential value: {0}")]
MalformedCredentialValue(#[from] ParseIntError),
}
impl Error {
pub fn storage_error(source: impl std::error::Error + Send + Sync + 'static) -> Self {
Error::SharedStorageError(Box::new(source))
}
}
impl From<StorageError> for Error {
fn from(value: StorageError) -> Self {
Self::storage_error(value)
}
}
-1
View File
@@ -1,5 +1,4 @@
pub mod errors;
pub mod recovery_storage;
pub mod utils;
pub use errors::{Error, Result};
@@ -1,74 +0,0 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::errors::Result;
use log::error;
use nym_credentials::coconut::bandwidth::IssuanceBandwidthCredential;
use std::fs::{create_dir_all, read_dir, File};
use std::io::{Read, Write};
use std::path::PathBuf;
pub const DUMPED_VOUCHER_EXTENSION: &str = "credentialrecovery";
pub struct RecoveryStorage {
recovery_dir: PathBuf,
}
impl RecoveryStorage {
pub fn new(recovery_dir: PathBuf) -> Result<Self> {
create_dir_all(&recovery_dir)?;
Ok(Self { recovery_dir })
}
pub fn unconsumed_vouchers(&self) -> Result<Vec<IssuanceBandwidthCredential>> {
let entries = read_dir(&self.recovery_dir)?;
let mut paths = vec![];
for entry in entries.flatten() {
let path = entry.path();
if let Some(extension) = path.extension() {
if extension == DUMPED_VOUCHER_EXTENSION {
paths.push(path)
}
}
}
let mut vouchers = vec![];
for path in paths {
if let Ok(mut file) = File::open(&path) {
let mut buff = Vec::new();
if file.read_to_end(&mut buff).is_ok() {
match IssuanceBandwidthCredential::try_from_recovered_bytes(&buff) {
Ok(voucher) => vouchers.push(voucher),
Err(err) => {
error!("failed to parse the voucher at {}: {err}", path.display())
}
}
}
}
}
Ok(vouchers)
}
pub fn voucher_filename(voucher: &IssuanceBandwidthCredential) -> String {
let prefix = voucher.typ().to_string();
let suffix = voucher.blinded_serial_number_bs58();
format!("{prefix}-{suffix}.{DUMPED_VOUCHER_EXTENSION}")
}
pub fn insert_voucher(&self, voucher: &IssuanceBandwidthCredential) -> Result<PathBuf> {
let file_name = Self::voucher_filename(voucher);
let file_path = self.recovery_dir.join(file_name);
let mut file = File::create(&file_path)?;
let buff = voucher.to_recovery_bytes();
file.write_all(&buff)?;
Ok(file_path)
}
pub fn remove_voucher(&self, file_name: String) -> Result<()> {
let file_path = self.recovery_dir.join(file_name);
Ok(std::fs::remove_file(file_path)?)
}
}
+81 -78
View File
@@ -1,74 +1,75 @@
use crate::errors::{Error, Result};
use crate::recovery_storage::RecoveryStorage;
use log::*;
use nym_bandwidth_controller::acquire::state::State;
use nym_bandwidth_controller::acquire::{
get_ticket_book, query_and_persist_required_global_signatures,
};
use nym_client_core::config::disk_persistence::CommonClientPaths;
use nym_config::DEFAULT_DATA_DIR;
use nym_credential_storage::persistent_storage::PersistentStorage;
use nym_credentials::coconut::bandwidth::CredentialType;
use nym_credential_storage::storage::Storage;
use nym_ecash_time::ecash_default_expiration_date;
use nym_validator_client::coconut::all_ecash_api_clients;
use nym_validator_client::nyxd::contract_traits::{
dkg_query_client::EpochState, CoconutBandwidthSigningClient, DkgQueryClient,
dkg_query_client::EpochState, DkgQueryClient, EcashSigningClient,
};
use nym_validator_client::nyxd::Coin;
use std::path::PathBuf;
use std::time::{Duration, SystemTime};
use std::time::Duration;
use time::OffsetDateTime;
pub async fn issue_credential<C>(
client: &C,
amount: Coin,
persistent_storage: &PersistentStorage,
recovery_storage_path: PathBuf,
) -> Result<()>
pub async fn issue_credential<C, S>(client: &C, storage: &S, client_id: &[u8]) -> Result<()>
where
C: DkgQueryClient + CoconutBandwidthSigningClient + Send + Sync,
C: DkgQueryClient + EcashSigningClient + Send + Sync,
S: Storage,
<S as Storage>::StorageError: Send + Sync + 'static,
{
let recovery_storage = setup_recovery_storage(recovery_storage_path).await;
block_until_coconut_is_available(client).await?;
block_until_ecash_is_available(client).await?;
info!("Starting to deposit funds, don't kill the process");
if let Ok(recovered_amount) =
recover_credentials(client, &recovery_storage, persistent_storage).await
{
if recovered_amount != 0 {
info!(
"Recovered credentials in the amount of {}",
recovered_amount
);
if let Ok(recovered_ticketbooks) = recover_deposits(client, storage).await {
if recovered_ticketbooks != 0 {
info!("managed to recover {recovered_ticketbooks} ticket books. no need to make fresh deposit");
return Ok(());
}
};
let state = nym_bandwidth_controller::acquire::deposit(client, amount.clone()).await?;
let epoch_id = client.get_current_epoch().await?.epoch_id;
let apis = all_ecash_api_clients(client, epoch_id).await?;
let ticketbook_expiration = ecash_default_expiration_date();
if nym_bandwidth_controller::acquire::get_bandwidth_voucher(&state, client, persistent_storage)
// make sure we have all required coin indices and expiration date signatures before attempting the deposit
query_and_persist_required_global_signatures(
storage,
epoch_id,
ticketbook_expiration,
apis.clone(),
)
.await?;
let issuance_data = nym_bandwidth_controller::acquire::make_deposit(
client,
client_id,
Some(ticketbook_expiration),
)
.await?;
info!("Deposit done");
if get_ticket_book(&issuance_data, client, storage, Some(apis))
.await
.is_err()
{
warn!("Failed to obtain credential. Dumping recovery data.",);
match recovery_storage.insert_voucher(&state.voucher) {
Ok(file_path) => {
warn!("Dumped recovery data to {}. Try using recovery mode to convert it to a credential", file_path.to_str().unwrap());
}
Err(e) => {
error!("Could not dump recovery data to file system due to {:?}, the deposit will be lost!", e)
}
}
error!("failed to obtain credential. saving recovery data...");
return Err(Error::Credential(
nym_credentials::error::Error::BandwidthCredentialError,
));
storage.insert_pending_ticketbook(&issuance_data).await.inspect_err(|err| {
let deposit = issuance_data.deposit_id();
error!("could not save the recovery data for deposit {deposit}: {err}. the data will unfortunately get lost")
}).map_err(Error::storage_error)?
}
info!("Succeeded adding a credential with amount {amount}");
info!("Succeeded adding a ticketbook");
Ok(())
}
pub async fn setup_recovery_storage(recovery_dir: PathBuf) -> RecoveryStorage {
RecoveryStorage::new(recovery_dir).expect("")
}
pub async fn setup_persistent_storage(client_home_directory: PathBuf) -> PersistentStorage {
let data_dir = client_home_directory.join(DEFAULT_DATA_DIR);
let paths = CommonClientPaths::new_base(data_dir);
@@ -77,16 +78,13 @@ pub async fn setup_persistent_storage(client_home_directory: PathBuf) -> Persist
nym_credential_storage::initialise_persistent_storage(db_path).await
}
pub async fn block_until_coconut_is_available<C>(client: &C) -> Result<()>
pub async fn block_until_ecash_is_available<C>(client: &C) -> Result<()>
where
C: DkgQueryClient + Send + Sync,
{
loop {
let epoch = client.get_current_epoch().await?;
let current_timestamp_secs = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.expect("the system clock is set to 01/01/1970 (or earlier)")
.as_secs();
let current_timestamp_secs = OffsetDateTime::now_utc().unix_timestamp() as u64;
if epoch.state.is_final() {
break;
@@ -101,7 +99,7 @@ where
} else {
// this should never be the case since the only case where final timestamp is unknown is when it's waiting for initialisation,
// but let's guard ourselves against future changes
info!("it is unknown when coconut will be come available. Going to check again later");
info!("it is unknown when ecash will be come available. Going to check again later");
tokio::time::sleep(Duration::from_secs(60 * 5)).await;
}
}
@@ -109,42 +107,47 @@ where
Ok(())
}
pub async fn recover_credentials<C>(
client: &C,
recovery_storage: &RecoveryStorage,
shared_storage: &PersistentStorage,
) -> Result<u128>
pub async fn recover_deposits<C, S>(client: &C, storage: &S) -> Result<usize>
where
C: DkgQueryClient + Send + Sync,
S: Storage,
<S as Storage>::StorageError: Send + Sync + 'static,
{
let mut recovered_amount: u128 = 0;
for voucher in recovery_storage.unconsumed_vouchers()? {
let voucher_value = match voucher.typ() {
CredentialType::Voucher => voucher.get_bandwidth_attribute(),
CredentialType::FreePass => {
error!("unimplemented recovery of free pass credentials");
continue;
}
};
recovered_amount += voucher_value.parse::<u128>()?;
info!("checking for any incomplete previous issuance attempts...");
let voucher_name = RecoveryStorage::voucher_filename(&voucher);
let state = State::new(voucher);
let incomplete = storage
.get_pending_ticketbooks()
.await
.map_err(Error::storage_error)?;
info!(
"we recovered {} incomplete ticketbook issuances",
incomplete.len()
);
if let Err(e) =
nym_bandwidth_controller::acquire::get_bandwidth_voucher(&state, client, shared_storage)
.await
{
error!("Could not recover deposit {voucher_name} due to {e}, try again later",)
} else {
info!(
"Converted deposit {voucher_name} to a credential, removing recovery data for it",
);
if let Err(err) = recovery_storage.remove_voucher(voucher_name) {
warn!("Could not remove recovery data: {err}");
let mut recovered_books = 0;
for issuance in incomplete {
let deposit = issuance.pending_ticketbook.deposit_id();
if issuance.pending_ticketbook.expired() {
warn!("ticketbook data associated with deposit {deposit} has expired. if you haven't contacted more than 1/3 of signers. it could still be recoverable (but out of scope of this library)");
continue;
}
if issuance.pending_ticketbook.check_expiration_date() {
warn!("deposit {deposit} was made with a different expiration date, it's validity will be shorter than the max one");
}
match get_ticket_book(&issuance.pending_ticketbook, client, storage, None).await {
Err(err) => error!("could not recover deposit {deposit} due to: {err}"),
Ok(_) => {
info!("managed to recover deposit {deposit}! the ticketbook has been added to the storage");
storage
.remove_pending_ticketbook(issuance.pending_id)
.await
.map_err(Error::storage_error)?;
recovered_books += 1;
}
}
}
Ok(recovered_amount)
Ok(recovered_books)
}
+4 -1
View File
@@ -14,5 +14,8 @@ license.workspace = true
bls12_381 = { workspace = true, default-features = false }
serde = { workspace = true, features = ["derive"] }
thiserror = { workspace = true }
time = { workspace = true, features = ["serde"] }
rand = { workspace = true }
nym-coconut = { path = "../nymcoconut" }
nym-compact-ecash = { path = "../nym_offline_compact_ecash" }
nym-ecash-time = { path = "../ecash-time" }
+185 -103
View File
@@ -1,136 +1,218 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use bls12_381::Scalar;
use rand::Rng;
use serde::{Deserialize, Serialize};
use std::fmt::{Display, Formatter};
use std::str::FromStr;
use thiserror::Error;
use time::{Date, OffsetDateTime};
pub use nym_coconut::{
aggregate_signature_shares, aggregate_signature_shares_and_verify, aggregate_verification_keys,
blind_sign, hash_to_scalar, keygen, prepare_blind_sign, prove_bandwidth_credential,
verify_credential, Attribute, Base58, BlindSignRequest, BlindedSerialNumber, BlindedSignature,
Bytable, CoconutError, KeyPair, Parameters, PrivateAttribute, PublicAttribute, SecretKey,
Signature, SignatureShare, VerificationKey, VerifyCredentialRequest,
pub use nym_compact_ecash::{
aggregate_verification_keys, aggregate_wallets, constants, ecash_parameters,
error::CompactEcashError,
generate_keypair_user, generate_keypair_user_from_seed, issue_verify,
scheme::coin_indices_signatures::aggregate_indices_signatures,
scheme::coin_indices_signatures::{
AnnotatedCoinIndexSignature, CoinIndexSignature, CoinIndexSignatureShare,
PartialCoinIndexSignature,
},
scheme::expiration_date_signatures::aggregate_expiration_signatures,
scheme::expiration_date_signatures::date_scalar,
scheme::expiration_date_signatures::{
AnnotatedExpirationDateSignature, ExpirationDateSignature, ExpirationDateSignatureShare,
PartialExpirationDateSignature,
},
scheme::keygen::KeyPairUser,
scheme::withdrawal::RequestInfo,
scheme::Payment,
scheme::{Wallet, WalletSignatures},
withdrawal_request, Base58, BlindedSignature, Bytable, PartialWallet, PayInfo, PublicKeyUser,
SecretKeyUser, VerificationKeyAuth, WithdrawalRequest,
};
pub const VOUCHER_INFO_TYPE: &str = "BandwidthVoucher";
pub const FREE_PASS_INFO_TYPE: &str = "FreeBandwidthPass";
// pub trait NymCredential {
// fn prove_credential(&self) -> Result<(), ()>;
// }
#[derive(Debug, Error)]
#[error("{0} is not a valid credential type")]
pub struct UnknownCredentialType(String);
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum CredentialType {
Voucher,
FreePass,
}
impl FromStr for CredentialType {
type Err = UnknownCredentialType;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s == VOUCHER_INFO_TYPE {
Ok(CredentialType::Voucher)
} else if s == FREE_PASS_INFO_TYPE {
Ok(CredentialType::FreePass)
} else {
Err(UnknownCredentialType(s.to_string()))
}
}
}
impl CredentialType {
pub fn validate(&self, type_plain: &str) -> bool {
match self {
CredentialType::Voucher => type_plain == VOUCHER_INFO_TYPE,
CredentialType::FreePass => type_plain == FREE_PASS_INFO_TYPE,
}
}
pub fn is_free_pass(&self) -> bool {
matches!(self, CredentialType::FreePass)
}
pub fn is_voucher(&self) -> bool {
matches!(self, CredentialType::Voucher)
}
}
impl Display for CredentialType {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
CredentialType::Voucher => VOUCHER_INFO_TYPE.fmt(f),
CredentialType::FreePass => FREE_PASS_INFO_TYPE.fmt(f),
}
}
}
use nym_ecash_time::EcashTime;
#[derive(Debug, Clone)]
pub struct CredentialSigningData {
pub pedersen_commitments_openings: Vec<Scalar>,
pub withdrawal_request: WithdrawalRequest,
pub blind_sign_request: BlindSignRequest,
pub request_info: RequestInfo,
pub public_attributes_plain: Vec<String>,
pub ecash_pub_key: PublicKeyUser,
pub typ: CredentialType,
pub expiration_date: Date,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
pub struct CredentialSpendingData {
pub embedded_private_attributes: usize,
pub payment: Payment,
pub verify_credential_request: VerifyCredentialRequest,
pub pay_info: PayInfo,
pub public_attributes_plain: Vec<String>,
pub typ: CredentialType,
pub spend_date: Date,
// pub value: u64,
/// The (DKG) epoch id under which the credential has been issued so that the verifier could use correct verification key for validation.
pub epoch_id: u64,
}
impl CredentialSpendingData {
pub fn verify(&self, params: &Parameters, verification_key: &VerificationKey) -> bool {
let hashed_public_attributes = self
.public_attributes_plain
.iter()
.map(hash_to_scalar)
.collect::<Vec<_>>();
// get references to the attributes
let public_attributes = hashed_public_attributes.iter().collect::<Vec<_>>();
verify_credential(
params,
pub fn verify(&self, verification_key: &VerificationKeyAuth) -> Result<(), CompactEcashError> {
self.payment.spend_verify(
verification_key,
&self.verify_credential_request,
&public_attributes,
&self.pay_info,
date_scalar(self.spend_date.ecash_unix_timestamp()),
)
}
pub fn validate_type_attribute(&self) -> bool {
// the first attribute is variant specific bandwidth encoding, the second one should be the type
let Some(type_plain) = self.public_attributes_plain.get(1) else {
return false;
pub fn encoded_serial_number(&self) -> Vec<u8> {
self.payment.encoded_serial_number()
}
pub fn serial_number_b58(&self) -> String {
self.payment.serial_number_bs58()
}
pub fn to_bytes(&self) -> Vec<u8> {
// simple length prefixed serialization
// TODO: change it to a standard format instead
let mut bytes = Vec::new();
let payment_bytes = self.payment.to_bytes();
bytes.extend_from_slice(&(payment_bytes.len() as u32).to_be_bytes());
bytes.extend_from_slice(&payment_bytes);
bytes.extend_from_slice(&self.pay_info.pay_info_bytes); //this is 72 bytes long
bytes.extend_from_slice(&self.spend_date.to_julian_day().to_be_bytes());
bytes.extend_from_slice(&self.epoch_id.to_be_bytes());
bytes
}
pub fn try_from_bytes(raw: &[u8]) -> Result<Self, CompactEcashError> {
// minimum length: 72 (pay_info) + 8 (epoch_id) + 4 (spend date) + 4 (payment length prefix)
if raw.len() < 72 + 8 + 4 + 4 {
return Err(CompactEcashError::DeserializationFailure {
object: "EcashCredential".into(),
});
}
let mut index = 0;
//SAFETY : casting a slice of length 4 into an array of size 4
let payment_len = u32::from_be_bytes(raw[index..index + 4].try_into().unwrap()) as usize;
index += 4;
if raw[index..].len() != payment_len + 84 {
return Err(CompactEcashError::DeserializationFailure {
object: "EcashCredential".into(),
});
}
let payment = Payment::try_from(&raw[index..index + payment_len])?;
index += payment_len;
let pay_info = PayInfo {
//SAFETY : casting a slice of length 72 into an array of size 72
pay_info_bytes: raw[index..index + 72].try_into().unwrap(),
};
index += 72;
self.typ.validate(type_plain)
}
//SAFETY : casting a slice of length 4 into an array of size 4
let spend_date_julian = i32::from_be_bytes(raw[index..index + 4].try_into().unwrap());
let spend_date = Date::from_julian_day(spend_date_julian).map_err(|_| {
CompactEcashError::DeserializationFailure {
object: "CredentialSpendingData".into(),
}
})?;
index += 4;
pub fn get_bandwidth_attribute(&self) -> Option<&String> {
// the first attribute is variant specific bandwidth encoding, the second one should be the type
self.public_attributes_plain.first()
}
if raw[index..].len() != 8 {
return Err(CompactEcashError::DeserializationFailure {
object: "EcashCredential".into(),
});
}
pub fn blinded_serial_number(&self) -> BlindedSerialNumber {
self.verify_credential_request.blinded_serial_number()
//SAFETY : casting a slice of length 8 into an array of size 8
let epoch_id = u64::from_be_bytes(raw[index..].try_into().unwrap());
Ok(CredentialSpendingData {
payment,
pay_info,
spend_date,
epoch_id,
})
}
}
impl Bytable for CredentialSpendingData {
fn to_byte_vec(&self) -> Vec<u8> {
self.to_bytes()
}
fn try_from_byte_slice(slice: &[u8]) -> Result<Self, CompactEcashError> {
Self::try_from_bytes(slice)
}
}
impl Base58 for CredentialSpendingData {}
#[derive(PartialEq, Eq, Debug, Clone, Copy)]
pub struct NymPayInfo {
randomness: [u8; 32],
timestamp: i64,
provider_public_key: [u8; 32],
}
impl NymPayInfo {
/// Generates a new `NymPayInfo` instance with random bytes, a timestamp, and a provider public key.
///
/// # Arguments
///
/// * `provider_pk` - The public key of the payment provider.
///
/// # Returns
///
/// A new `NymPayInfo` instance.
///
pub fn generate(provider_pk: [u8; 32]) -> Self {
let mut randomness = [0u8; 32];
rand::thread_rng().fill(&mut randomness[..32]);
let timestamp = OffsetDateTime::now_utc().unix_timestamp();
NymPayInfo {
randomness,
timestamp,
provider_public_key: provider_pk,
}
}
pub fn timestamp(&self) -> i64 {
self.timestamp
}
pub fn pk(&self) -> [u8; 32] {
self.provider_public_key
}
}
impl From<NymPayInfo> for PayInfo {
fn from(value: NymPayInfo) -> Self {
let mut pay_info_bytes = [0u8; 72];
pay_info_bytes[..32].copy_from_slice(&value.randomness);
pay_info_bytes[32..40].copy_from_slice(&value.timestamp.to_be_bytes());
pay_info_bytes[40..].copy_from_slice(&value.provider_public_key);
PayInfo { pay_info_bytes }
}
}
impl From<PayInfo> for NymPayInfo {
fn from(value: PayInfo) -> Self {
//SAFETY : slice to array of same length
let randomness = value.pay_info_bytes[..32].try_into().unwrap();
let timestamp = i64::from_be_bytes(value.pay_info_bytes[32..40].try_into().unwrap());
let provider_public_key = value.pay_info_bytes[40..].try_into().unwrap();
NymPayInfo {
randomness,
timestamp,
provider_public_key,
}
}
}
+5 -1
View File
@@ -16,11 +16,15 @@ time = { workspace = true, features = ["serde"] }
serde = { workspace = true, features = ["derive"] }
zeroize = { workspace = true }
nym-ecash-time = { path = "../ecash-time", features = ["expiration"] }
# I guess temporarily until we get serde support in coconut up and running
nym-credentials-interface = { path = "../credentials-interface" }
nym-crypto = { path = "../crypto", features = ["rand", "asymmetric", "serde"] }
nym-crypto = { path = "../crypto" }
nym-api-requests = { path = "../../nym-api/nym-api-requests" }
nym-validator-client = { path = "../client-libs/validator-client", default-features = false }
nym-ecash-contract-common = { path = "../cosmwasm-smart-contracts/ecash-contract" }
nym-network-defaults = { path = "../network-defaults" }
[dev-dependencies]
rand = "0.8.5"
@@ -1,142 +0,0 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::coconut::utils::scalar_serde_helper;
use crate::error::Error;
use nym_api_requests::coconut::FreePassRequest;
use nym_credentials_interface::{
hash_to_scalar, Attribute, BlindedSignature, CredentialSigningData, PublicAttribute,
};
use nym_validator_client::signing::AccountData;
use serde::{Deserialize, Serialize};
use time::{Duration, OffsetDateTime, Time};
use zeroize::{Zeroize, ZeroizeOnDrop};
pub const DEFAULT_FREE_PASS_VALIDITY: Duration = Duration::WEEK; // 1 week
pub const MAX_FREE_PASS_VALIDITY: Duration = Duration::weeks(12); // 12 weeks
#[derive(Debug, Zeroize, ZeroizeOnDrop, Serialize, Deserialize)]
pub struct FreePassIssuedData {
/// the plain validity value of this credential expressed as unix timestamp
#[zeroize(skip)]
expiry_date: OffsetDateTime,
}
impl<'a> From<&'a FreePassIssuanceData> for FreePassIssuedData {
fn from(value: &'a FreePassIssuanceData) -> Self {
FreePassIssuedData {
expiry_date: value.expiry_date,
}
}
}
impl FreePassIssuedData {
pub fn expired(&self) -> bool {
self.expiry_date <= OffsetDateTime::now_utc()
}
pub fn expiry_date(&self) -> OffsetDateTime {
self.expiry_date
}
pub fn expiry_date_plain(&self) -> String {
self.expiry_date.unix_timestamp().to_string()
}
}
#[derive(Zeroize, Serialize, Deserialize)]
pub struct FreePassIssuanceData {
/// the plain validity value of this credential expressed as unix timestamp
#[zeroize(skip)]
expiry_date: OffsetDateTime,
// the expiry date, as unix timestamp, hashed into a scalar
#[serde(with = "scalar_serde_helper")]
expiry_date_prehashed: PublicAttribute,
}
impl FreePassIssuanceData {
pub fn new(expiry_date: Option<OffsetDateTime>) -> Self {
// ideally we should have implemented a proper error handling here, sure.
// but given it's meant to only be used by nym, imo it's fine to just panic here in case of invalid arguments
let expiry_date = if let Some(provided) = expiry_date {
if provided - OffsetDateTime::now_utc() > MAX_FREE_PASS_VALIDITY {
panic!("the provided expiry date is bigger than the maximum value of {MAX_FREE_PASS_VALIDITY}");
}
provided
} else {
Self::default_expiry_date()
};
let expiry_date_prehashed = hash_to_scalar(expiry_date.unix_timestamp().to_string());
FreePassIssuanceData {
expiry_date,
expiry_date_prehashed,
}
}
pub fn default_expiry_date() -> OffsetDateTime {
// set it to the furthest midnight in the future such as it's no more than a week away,
// i.e. if it's currently for example 9:43 on 2nd March 2024, it will set it to 0:00 on 9th March 2024
(OffsetDateTime::now_utc() + DEFAULT_FREE_PASS_VALIDITY).replace_time(Time::MIDNIGHT)
}
pub fn expiry_date_attribute(&self) -> &Attribute {
&self.expiry_date_prehashed
}
pub fn expiry_date_plain(&self) -> String {
self.expiry_date.unix_timestamp().to_string()
}
pub async fn obtain_free_pass_nonce(
&self,
client: &nym_validator_client::client::NymApiClient,
) -> Result<[u8; 16], Error> {
let server_response = client.free_pass_nonce().await?;
Ok(server_response.current_nonce)
}
pub fn create_free_pass_request(
&self,
signing_request: &CredentialSigningData,
account_data: &AccountData,
issuer_nonce: [u8; 16],
) -> Result<FreePassRequest, Error> {
let nonce_signature = account_data
.private_key()
.sign(&issuer_nonce)
.map_err(|_| Error::Secp256k1SignFailure)?;
Ok(FreePassRequest {
cosmos_pubkey: account_data.public_key(),
inner_sign_request: signing_request.blind_sign_request.clone(),
used_nonce: issuer_nonce,
nonce_signature,
public_attributes_plain: signing_request.public_attributes_plain.clone(),
})
}
pub async fn obtain_blinded_credential(
&self,
client: &nym_validator_client::client::NymApiClient,
request: &FreePassRequest,
) -> Result<BlindedSignature, Error> {
let server_response = client.issue_free_pass_credential(request).await?;
Ok(server_response.blinded_signature)
}
pub async fn request_blinded_credential(
&self,
signing_request: &CredentialSigningData,
account_data: &AccountData,
client: &nym_validator_client::client::NymApiClient,
) -> Result<BlindedSignature, Error> {
let signing_nonce = self.obtain_free_pass_nonce(client).await?;
let request =
self.create_free_pass_request(signing_request, account_data, signing_nonce)?;
self.obtain_blinded_credential(client, &request).await
}
}
@@ -1,355 +0,0 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::coconut::bandwidth::freepass::FreePassIssuanceData;
use crate::coconut::bandwidth::issued::IssuedBandwidthCredential;
use crate::coconut::bandwidth::voucher::BandwidthVoucherIssuanceData;
use crate::coconut::bandwidth::{
bandwidth_credential_params, CredentialSigningData, CredentialType,
};
use crate::coconut::utils::scalar_serde_helper;
use crate::error::Error;
use nym_credentials_interface::{
aggregate_signature_shares, aggregate_signature_shares_and_verify, hash_to_scalar,
prepare_blind_sign, Attribute, BlindedSerialNumber, BlindedSignature, Parameters,
PrivateAttribute, PublicAttribute, Signature, SignatureShare, VerificationKey,
};
use nym_crypto::asymmetric::{encryption, identity};
use nym_validator_client::nym_api::EpochId;
use nym_validator_client::signing::AccountData;
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
use zeroize::{Zeroize, ZeroizeOnDrop};
pub use nym_validator_client::nyxd::{Coin, Hash};
#[derive(Zeroize, ZeroizeOnDrop, Serialize, Deserialize)]
pub enum BandwidthCredentialIssuanceDataVariant {
Voucher(BandwidthVoucherIssuanceData),
FreePass(FreePassIssuanceData),
}
impl From<FreePassIssuanceData> for BandwidthCredentialIssuanceDataVariant {
fn from(value: FreePassIssuanceData) -> Self {
BandwidthCredentialIssuanceDataVariant::FreePass(value)
}
}
impl From<BandwidthVoucherIssuanceData> for BandwidthCredentialIssuanceDataVariant {
fn from(value: BandwidthVoucherIssuanceData) -> Self {
BandwidthCredentialIssuanceDataVariant::Voucher(value)
}
}
impl BandwidthCredentialIssuanceDataVariant {
pub fn info(&self) -> CredentialType {
match self {
BandwidthCredentialIssuanceDataVariant::Voucher(..) => CredentialType::Voucher,
BandwidthCredentialIssuanceDataVariant::FreePass(..) => CredentialType::FreePass,
}
}
// currently this works under the assumption of there being a single unique public attribute for given variant
pub fn public_value(&self) -> &Attribute {
match self {
BandwidthCredentialIssuanceDataVariant::Voucher(voucher) => voucher.value_attribute(),
BandwidthCredentialIssuanceDataVariant::FreePass(freepass) => {
freepass.expiry_date_attribute()
}
}
}
// currently this works under the assumption of there being a single unique public attribute for given variant
pub fn public_value_plain(&self) -> String {
match self {
BandwidthCredentialIssuanceDataVariant::Voucher(voucher) => voucher.value_plain(),
BandwidthCredentialIssuanceDataVariant::FreePass(freepass) => {
freepass.expiry_date_plain()
}
}
}
pub fn voucher_data(&self) -> Option<&BandwidthVoucherIssuanceData> {
match self {
BandwidthCredentialIssuanceDataVariant::Voucher(voucher) => Some(voucher),
_ => None,
}
}
}
// all types of bandwidth credentials contain serial number and binding number
#[derive(Zeroize, ZeroizeOnDrop, Serialize, Deserialize)]
pub struct IssuanceBandwidthCredential {
// private attributes
/// a random secret value generated by the client used for double-spending detection
#[serde(with = "scalar_serde_helper")]
serial_number: PrivateAttribute,
/// a random secret value generated by the client used to bind multiple credentials together
#[serde(with = "scalar_serde_helper")]
binding_number: PrivateAttribute,
/// data specific to given bandwidth credential, for example a value for bandwidth voucher and expiry date for the free pass
variant_data: BandwidthCredentialIssuanceDataVariant,
/// type of the bandwdith credential hashed onto a scalar
#[serde(with = "scalar_serde_helper")]
type_prehashed: PublicAttribute,
}
impl IssuanceBandwidthCredential {
pub const PUBLIC_ATTRIBUTES: u32 = 2;
pub const PRIVATE_ATTRIBUTES: u32 = 2;
pub const ENCODED_ATTRIBUTES: u32 = Self::PUBLIC_ATTRIBUTES + Self::PRIVATE_ATTRIBUTES;
pub fn default_parameters() -> Parameters {
// safety: the unwrap is fine here as Self::ENCODED_ATTRIBUTES is non-zero
Parameters::new(Self::ENCODED_ATTRIBUTES).unwrap()
}
pub fn new<B: Into<BandwidthCredentialIssuanceDataVariant>>(variant_data: B) -> Self {
let variant_data = variant_data.into();
let type_prehashed = hash_to_scalar(variant_data.info().to_string());
let params = bandwidth_credential_params();
let serial_number = params.random_scalar();
let binding_number = params.random_scalar();
IssuanceBandwidthCredential {
serial_number,
binding_number,
variant_data,
type_prehashed,
}
}
pub fn new_voucher(
value: impl Into<Coin>,
deposit_tx_hash: Hash,
signing_key: identity::PrivateKey,
unused_ed25519: encryption::PrivateKey,
) -> Self {
Self::new(BandwidthVoucherIssuanceData::new(
value,
deposit_tx_hash,
signing_key,
unused_ed25519,
))
}
pub fn new_freepass(expiry_date: Option<OffsetDateTime>) -> Self {
Self::new(FreePassIssuanceData::new(expiry_date))
}
pub fn blind_serial_number(&self) -> BlindedSerialNumber {
(bandwidth_credential_params().gen2() * self.serial_number).into()
}
pub fn blinded_serial_number_bs58(&self) -> String {
use nym_credentials_interface::Base58;
self.blind_serial_number().to_bs58()
}
pub fn typ(&self) -> CredentialType {
self.variant_data.info()
}
pub fn get_private_attributes(&self) -> Vec<&PrivateAttribute> {
vec![&self.serial_number, &self.binding_number]
}
pub fn get_public_attributes(&self) -> Vec<&PublicAttribute> {
vec![self.variant_data.public_value(), &self.type_prehashed]
}
pub fn get_plain_public_attributes(&self) -> Vec<String> {
vec![
self.variant_data.public_value_plain(),
self.typ().to_string(),
]
}
pub fn get_variant_data(&self) -> &BandwidthCredentialIssuanceDataVariant {
&self.variant_data
}
pub fn get_bandwidth_attribute(&self) -> String {
self.variant_data.public_value_plain()
}
pub fn prepare_for_signing(&self) -> CredentialSigningData {
let params = bandwidth_credential_params();
// safety: the creation of the request can only fail if one provided invalid parameters
// and we created then specific to this type of the credential so the unwrap is fine
let (pedersen_commitments_openings, blind_sign_request) = prepare_blind_sign(
params,
&[&self.serial_number, &self.binding_number],
&self.get_public_attributes(),
)
.unwrap();
CredentialSigningData {
pedersen_commitments_openings,
blind_sign_request,
public_attributes_plain: self.get_plain_public_attributes(),
typ: self.typ(),
}
}
pub fn unblind_signature(
&self,
validator_vk: &VerificationKey,
signing_data: &CredentialSigningData,
blinded_signature: BlindedSignature,
) -> Result<Signature, Error> {
let public_attributes = self.get_public_attributes();
let private_attributes = self.get_private_attributes();
let params = bandwidth_credential_params();
let unblinded_signature = blinded_signature.unblind_and_verify(
params,
validator_vk,
&private_attributes,
&public_attributes,
&signing_data.blind_sign_request.get_commitment_hash(),
&signing_data.pedersen_commitments_openings,
)?;
Ok(unblinded_signature)
}
pub async fn obtain_partial_freepass_credential(
&self,
client: &nym_validator_client::client::NymApiClient,
account_data: &AccountData,
validator_vk: &VerificationKey,
signing_data: impl Into<Option<CredentialSigningData>>,
) -> Result<Signature, Error> {
// if we provided signing data, do use them, otherwise generate fresh data
let signing_data = signing_data
.into()
.unwrap_or_else(|| self.prepare_for_signing());
let blinded_signature = match &self.variant_data {
BandwidthCredentialIssuanceDataVariant::FreePass(freepass) => {
freepass
.request_blinded_credential(&signing_data, account_data, client)
.await?
}
_ => return Err(Error::NotAFreePass),
};
self.unblind_signature(validator_vk, &signing_data, blinded_signature)
}
// ideally this would have been generic over credential type, but we really don't need secp256k1 keys for bandwidth vouchers
pub async fn obtain_partial_bandwidth_voucher_credential(
&self,
client: &nym_validator_client::client::NymApiClient,
validator_vk: &VerificationKey,
signing_data: impl Into<Option<CredentialSigningData>>,
) -> Result<Signature, Error> {
// if we provided signing data, do use them, otherwise generate fresh data
let signing_data = signing_data
.into()
.unwrap_or_else(|| self.prepare_for_signing());
let blinded_signature = match &self.variant_data {
BandwidthCredentialIssuanceDataVariant::Voucher(voucher) => {
// TODO: the request can be re-used between different apis
let request = voucher.create_blind_sign_request_body(&signing_data);
voucher.obtain_blinded_credential(client, &request).await?
}
_ => return Err(Error::NotABandwdithVoucher),
};
self.unblind_signature(validator_vk, &signing_data, blinded_signature)
}
pub fn unchecked_aggregate_signature_shares(
&self,
shares: &[SignatureShare],
) -> Result<Signature, Error> {
aggregate_signature_shares(shares).map_err(Error::SignatureAggregationError)
}
pub fn aggregate_signature_shares(
&self,
verification_key: &VerificationKey,
shares: &[SignatureShare],
) -> Result<Signature, Error> {
let public_attributes = self.get_public_attributes();
let private_attributes = self.get_private_attributes();
let params = bandwidth_credential_params();
let mut attributes = Vec::with_capacity(private_attributes.len() + public_attributes.len());
attributes.extend_from_slice(&private_attributes);
attributes.extend_from_slice(&public_attributes);
aggregate_signature_shares_and_verify(params, verification_key, &attributes, shares)
.map_err(Error::SignatureAggregationError)
}
// also drops self after the conversion
pub fn into_issued_credential(
self,
aggregate_signature: Signature,
epoch_id: EpochId,
) -> IssuedBandwidthCredential {
self.to_issued_credential(aggregate_signature, epoch_id)
}
pub fn to_issued_credential(
&self,
aggregate_signature: Signature,
epoch_id: EpochId,
) -> IssuedBandwidthCredential {
IssuedBandwidthCredential::new(
self.serial_number,
self.binding_number,
aggregate_signature,
(&self.variant_data).into(),
self.type_prehashed,
epoch_id,
)
}
// TODO: is that actually needed?
pub fn to_recovery_bytes(&self) -> Vec<u8> {
use bincode::Options;
// safety: our data format is stable and thus the serialization should not fail
make_recovery_bincode_serializer().serialize(self).unwrap()
}
// TODO: is that actually needed?
// idea: make it consistent with the issued credential and its vX serde
pub fn try_from_recovered_bytes(bytes: &[u8]) -> Result<Self, Error> {
use bincode::Options;
make_recovery_bincode_serializer()
.deserialize(bytes)
.map_err(|source| Error::RecoveryCredentialDeserializationFailure { source })
}
}
fn make_recovery_bincode_serializer() -> impl bincode::Options {
use bincode::Options;
bincode::DefaultOptions::new()
.with_big_endian()
.with_varint_encoding()
}
#[cfg(test)]
mod tests {
use super::*;
fn assert_zeroize_on_drop<T: ZeroizeOnDrop>() {}
fn assert_zeroize<T: Zeroize>() {}
#[test]
fn credential_is_zeroized() {
assert_zeroize::<IssuanceBandwidthCredential>();
assert_zeroize_on_drop::<IssuanceBandwidthCredential>();
}
}
@@ -1,217 +0,0 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::coconut::bandwidth::bandwidth_credential_params;
use crate::coconut::bandwidth::freepass::FreePassIssuedData;
use crate::coconut::bandwidth::issuance::{
BandwidthCredentialIssuanceDataVariant, IssuanceBandwidthCredential,
};
use crate::coconut::bandwidth::voucher::BandwidthVoucherIssuedData;
use crate::coconut::bandwidth::{CredentialSpendingData, CredentialType};
use crate::coconut::utils::scalar_serde_helper;
use crate::error::Error;
use nym_credentials_interface::prove_bandwidth_credential;
use nym_credentials_interface::{
Parameters, PrivateAttribute, PublicAttribute, Signature, VerificationKey,
};
use nym_validator_client::nym_api::EpochId;
use serde::{Deserialize, Serialize};
use zeroize::{Zeroize, ZeroizeOnDrop};
pub const CURRENT_SERIALIZATION_REVISION: u8 = 1;
#[derive(Debug, Zeroize, Serialize, Deserialize)]
pub enum BandwidthCredentialIssuedDataVariant {
Voucher(BandwidthVoucherIssuedData),
FreePass(FreePassIssuedData),
}
impl<'a> From<&'a BandwidthCredentialIssuanceDataVariant> for BandwidthCredentialIssuedDataVariant {
fn from(value: &'a BandwidthCredentialIssuanceDataVariant) -> Self {
match value {
BandwidthCredentialIssuanceDataVariant::Voucher(voucher) => {
BandwidthCredentialIssuedDataVariant::Voucher(voucher.into())
}
BandwidthCredentialIssuanceDataVariant::FreePass(freepass) => {
BandwidthCredentialIssuedDataVariant::FreePass(freepass.into())
}
}
}
}
impl From<FreePassIssuedData> for BandwidthCredentialIssuedDataVariant {
fn from(value: FreePassIssuedData) -> Self {
BandwidthCredentialIssuedDataVariant::FreePass(value)
}
}
impl From<BandwidthVoucherIssuedData> for BandwidthCredentialIssuedDataVariant {
fn from(value: BandwidthVoucherIssuedData) -> Self {
BandwidthCredentialIssuedDataVariant::Voucher(value)
}
}
impl BandwidthCredentialIssuedDataVariant {
pub fn info(&self) -> CredentialType {
match self {
BandwidthCredentialIssuedDataVariant::Voucher(..) => CredentialType::Voucher,
BandwidthCredentialIssuedDataVariant::FreePass(..) => CredentialType::FreePass,
}
}
// currently this works under the assumption of there being a single unique public attribute for given variant
pub fn public_value_plain(&self) -> String {
match self {
BandwidthCredentialIssuedDataVariant::Voucher(voucher) => voucher.value_plain(),
BandwidthCredentialIssuedDataVariant::FreePass(freepass) => {
freepass.expiry_date_plain()
}
}
}
}
// the only important thing to zeroize here are the private attributes, the rest can be made fully public for what we're concerned
#[derive(Zeroize, ZeroizeOnDrop, Serialize, Deserialize)]
pub struct IssuedBandwidthCredential {
// private attributes
/// a random secret value generated by the client used for double-spending detection
#[serde(with = "scalar_serde_helper")]
serial_number: PrivateAttribute,
/// a random secret value generated by the client used to bind multiple credentials together
#[serde(with = "scalar_serde_helper")]
binding_number: PrivateAttribute,
/// the underlying aggregated signature on the attributes
#[zeroize(skip)]
signature: Signature,
/// data specific to given bandwidth credential, for example a value for bandwidth voucher and expiry date for the free pass
variant_data: BandwidthCredentialIssuedDataVariant,
/// type of the bandwdith credential hashed onto a scalar
#[serde(with = "scalar_serde_helper")]
type_prehashed: PublicAttribute,
/// Specifies the (DKG) epoch id when this credential has been issued
epoch_id: EpochId,
}
impl IssuedBandwidthCredential {
pub fn new(
serial_number: PrivateAttribute,
binding_number: PrivateAttribute,
signature: Signature,
variant_data: BandwidthCredentialIssuedDataVariant,
type_prehashed: PublicAttribute,
epoch_id: EpochId,
) -> Self {
IssuedBandwidthCredential {
serial_number,
binding_number,
signature,
variant_data,
type_prehashed,
epoch_id,
}
}
pub fn try_unpack(bytes: &[u8], revision: impl Into<Option<u8>>) -> Result<Self, Error> {
let revision = revision.into().unwrap_or(CURRENT_SERIALIZATION_REVISION);
match revision {
1 => Self::unpack_v1(bytes),
_ => Err(Error::UnknownSerializationRevision { revision }),
}
}
pub fn epoch_id(&self) -> EpochId {
self.epoch_id
}
pub fn variant_data(&self) -> &BandwidthCredentialIssuedDataVariant {
&self.variant_data
}
pub fn current_serialization_revision(&self) -> u8 {
CURRENT_SERIALIZATION_REVISION
}
/// Pack (serialize) this credential data into a stream of bytes using v1 serializer.
pub fn pack_v1(&self) -> Vec<u8> {
use bincode::Options;
// safety: our data format is stable and thus the serialization should not fail
make_storable_bincode_serializer().serialize(self).unwrap()
}
/// Unpack (deserialize) the credential data from the given bytes using v1 serializer.
pub fn unpack_v1(bytes: &[u8]) -> Result<Self, Error> {
use bincode::Options;
make_storable_bincode_serializer()
.deserialize(bytes)
.map_err(|source| Error::SerializationFailure {
source,
revision: 1,
})
}
pub fn default_parameters() -> Parameters {
IssuanceBandwidthCredential::default_parameters()
}
pub fn typ(&self) -> CredentialType {
self.variant_data.info()
}
pub fn get_plain_public_attributes(&self) -> Vec<String> {
vec![
self.variant_data.public_value_plain(),
self.typ().to_string(),
]
}
pub fn prepare_for_spending(
&self,
verification_key: &VerificationKey,
) -> Result<CredentialSpendingData, Error> {
let params = bandwidth_credential_params();
let verify_credential_request = prove_bandwidth_credential(
params,
verification_key,
&self.signature,
&self.serial_number,
&self.binding_number,
)?;
Ok(CredentialSpendingData {
embedded_private_attributes: IssuanceBandwidthCredential::PRIVATE_ATTRIBUTES as usize,
verify_credential_request,
public_attributes_plain: self.get_plain_public_attributes(),
typ: self.typ(),
epoch_id: self.epoch_id,
})
}
}
fn make_storable_bincode_serializer() -> impl bincode::Options {
use bincode::Options;
bincode::DefaultOptions::new()
.with_big_endian()
.with_varint_encoding()
}
#[cfg(test)]
mod tests {
use super::*;
fn assert_zeroize_on_drop<T: ZeroizeOnDrop>() {}
fn assert_zeroize<T: Zeroize>() {}
#[test]
fn credential_is_zeroized() {
assert_zeroize::<IssuedBandwidthCredential>();
assert_zeroize_on_drop::<IssuedBandwidthCredential>();
}
}
@@ -1,22 +0,0 @@
// Copyright 2021-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::sync::OnceLock;
pub use issuance::IssuanceBandwidthCredential;
pub use issued::IssuedBandwidthCredential;
pub use nym_credentials_interface::{
CredentialSigningData, CredentialSpendingData, CredentialType, Parameters,
UnknownCredentialType,
};
pub mod freepass;
pub mod issuance;
pub mod issued;
pub mod voucher;
// works under the assumption of having 4 attributes in the underlying credential(s)
pub fn bandwidth_credential_params() -> &'static Parameters {
static BANDWIDTH_CREDENTIAL_PARAMS: OnceLock<Parameters> = OnceLock::new();
BANDWIDTH_CREDENTIAL_PARAMS.get_or_init(IssuanceBandwidthCredential::default_parameters)
}
@@ -1,145 +0,0 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::coconut::bandwidth::CredentialSigningData;
use crate::coconut::utils::scalar_serde_helper;
use crate::error::Error;
use nym_api_requests::coconut::BlindSignRequestBody;
use nym_credentials_interface::{
hash_to_scalar, Attribute, BlindSignRequest, BlindedSignature, CredentialType, PublicAttribute,
};
use nym_crypto::asymmetric::{encryption, identity};
use nym_validator_client::nyxd::{Coin, Hash};
use serde::{Deserialize, Serialize};
use zeroize::{Zeroize, ZeroizeOnDrop};
#[derive(Debug, Zeroize, ZeroizeOnDrop, Serialize, Deserialize)]
pub struct BandwidthVoucherIssuedData {
/// the plain value (e.g., bandwidth) encoded in this voucher
// note: for legacy reasons we're only using the value of the coin and ignoring the denom
#[zeroize(skip)]
value: Coin,
}
impl<'a> From<&'a BandwidthVoucherIssuanceData> for BandwidthVoucherIssuedData {
fn from(value: &'a BandwidthVoucherIssuanceData) -> Self {
BandwidthVoucherIssuedData {
value: value.value.clone(),
}
}
}
impl BandwidthVoucherIssuedData {
pub fn new(value: Coin) -> Self {
BandwidthVoucherIssuedData { value }
}
pub fn value(&self) -> &Coin {
&self.value
}
pub fn value_plain(&self) -> String {
self.value.amount.to_string()
}
}
#[derive(Zeroize, ZeroizeOnDrop, Serialize, Deserialize)]
pub struct BandwidthVoucherIssuanceData {
/// the plain value (e.g., bandwidth) encoded in this voucher
// note: for legacy reasons we're only using the value of the coin and ignoring the denom
#[zeroize(skip)]
value: Coin,
// note: as mentioned above, we're only hashing the value of the coin!
#[serde(with = "scalar_serde_helper")]
value_prehashed: PublicAttribute,
/// the hash of the deposit transaction
#[zeroize(skip)]
deposit_tx_hash: Hash,
/// base58 encoded private key ensuring the depositer requested these attributes
signing_key: identity::PrivateKey,
/// base58 encoded private key ensuring only this client receives the signature share
unused_ed25519: encryption::PrivateKey,
}
impl BandwidthVoucherIssuanceData {
pub fn new(
value: impl Into<Coin>,
deposit_tx_hash: Hash,
signing_key: identity::PrivateKey,
unused_ed25519: encryption::PrivateKey,
) -> Self {
let value = value.into();
let value_prehashed = hash_to_scalar(value.amount.to_string());
BandwidthVoucherIssuanceData {
value,
value_prehashed,
deposit_tx_hash,
signing_key,
unused_ed25519,
}
}
pub fn request_plaintext(request: &BlindSignRequest, tx_hash: Hash) -> Vec<u8> {
let mut message = request.to_bytes();
message.extend_from_slice(tx_hash.as_bytes());
message
}
fn request_signature(&self, signing_request: &CredentialSigningData) -> identity::Signature {
let message =
Self::request_plaintext(&signing_request.blind_sign_request, self.deposit_tx_hash);
self.signing_key.sign(message)
}
pub fn create_blind_sign_request_body(
&self,
signing_request: &CredentialSigningData,
) -> BlindSignRequestBody {
let request_signature = self.request_signature(signing_request);
BlindSignRequestBody::new(
signing_request.blind_sign_request.clone(),
self.deposit_tx_hash,
request_signature,
signing_request.public_attributes_plain.clone(),
)
}
pub async fn obtain_blinded_credential(
&self,
client: &nym_validator_client::client::NymApiClient,
request_body: &BlindSignRequestBody,
) -> Result<BlindedSignature, Error> {
let server_response = client.blind_sign(request_body).await?;
Ok(server_response.blinded_signature)
}
pub fn value_plain(&self) -> String {
self.value.amount.to_string()
}
pub fn value_attribute(&self) -> &Attribute {
&self.value_prehashed
}
pub fn typ() -> CredentialType {
CredentialType::Voucher
}
pub fn tx_hash(&self) -> Hash {
self.deposit_tx_hash
}
pub fn identity_key(&self) -> &identity::PrivateKey {
&self.signing_key
}
pub fn encryption_key(&self) -> &encryption::PrivateKey {
&self.unused_ed25519
}
}
-97
View File
@@ -1,97 +0,0 @@
// Copyright 2021-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::coconut::bandwidth::IssuanceBandwidthCredential;
use crate::error::Error;
use log::{debug, warn};
use nym_credentials_interface::{
aggregate_verification_keys, Signature, SignatureShare, VerificationKey,
};
use nym_validator_client::client::CoconutApiClient;
pub fn obtain_aggregate_verification_key(
api_clients: &[CoconutApiClient],
) -> Result<VerificationKey, Error> {
if api_clients.is_empty() {
return Err(Error::NoValidatorsAvailable);
}
let indices: Vec<_> = api_clients
.iter()
.map(|api_client| api_client.node_id)
.collect();
let shares: Vec<_> = api_clients
.iter()
.map(|api_client| api_client.verification_key.clone())
.collect();
Ok(aggregate_verification_keys(&shares, Some(&indices))?)
}
pub async fn obtain_aggregate_signature(
voucher: &IssuanceBandwidthCredential,
coconut_api_clients: &[CoconutApiClient],
threshold: u64,
) -> Result<Signature, Error> {
if coconut_api_clients.is_empty() {
return Err(Error::NoValidatorsAvailable);
}
let mut shares = Vec::with_capacity(coconut_api_clients.len());
let verification_key = obtain_aggregate_verification_key(coconut_api_clients)?;
let request = voucher.prepare_for_signing();
for coconut_api_client in coconut_api_clients.iter() {
debug!(
"attempting to obtain partial credential from {}",
coconut_api_client.api_client.api_url()
);
match voucher
.obtain_partial_bandwidth_voucher_credential(
&coconut_api_client.api_client,
&coconut_api_client.verification_key,
Some(request.clone()),
)
.await
{
Ok(signature) => {
let share = SignatureShare::new(signature, coconut_api_client.node_id);
shares.push(share)
}
Err(err) => {
warn!(
"failed to obtain partial credential from {}: {err}",
coconut_api_client.api_client.api_url()
);
}
};
}
if shares.len() < threshold as usize {
return Err(Error::NotEnoughShares);
}
voucher.aggregate_signature_shares(&verification_key, &shares)
}
pub(crate) mod scalar_serde_helper {
use bls12_381::Scalar;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use zeroize::Zeroizing;
pub fn serialize<S: Serializer>(scalar: &Scalar, serializer: S) -> Result<S::Ok, S::Error> {
scalar.to_bytes().serialize(serializer)
}
pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Scalar, D::Error> {
let b = <[u8; 32]>::deserialize(deserializer)?;
// make sure the bytes get zeroed
let bytes = Zeroizing::new(b);
let maybe_scalar: Option<Scalar> = Scalar::from_bytes(&bytes).into();
maybe_scalar.ok_or(serde::de::Error::custom(
"did not construct a valid bls12-381 scalar out of the provided bytes",
))
}
}
@@ -0,0 +1,239 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::ecash::bandwidth::issued::IssuedTicketBook;
use crate::ecash::bandwidth::CredentialSigningData;
use crate::ecash::utils::cred_exp_date;
use crate::error::Error;
use nym_api_requests::ecash::BlindSignRequestBody;
use nym_credentials_interface::{
aggregate_wallets, generate_keypair_user_from_seed, issue_verify, withdrawal_request,
BlindedSignature, KeyPairUser, PartialWallet, VerificationKeyAuth, WalletSignatures,
WithdrawalRequest,
};
use nym_crypto::asymmetric::identity;
use nym_ecash_contract_common::deposit::DepositId;
use nym_ecash_time::{ecash_default_expiration_date, ecash_today, EcashTime};
use nym_validator_client::nym_api::EpochId;
use serde::{Deserialize, Serialize};
use time::Date;
use crate::ecash::bandwidth::serialiser::VersionedSerialise;
pub use nym_validator_client::nyxd::{Coin, Hash};
#[derive(Serialize, Deserialize)]
pub struct IssuanceTicketBook {
/// the id of the associated deposit
deposit_id: DepositId,
/// base58 encoded private key ensuring the depositer requested these attributes
signing_key: identity::PrivateKey,
/// ecash keypair related to the credential
ecash_keypair: KeyPairUser,
/// expiration_date of that credential
expiration_date: Date,
}
impl IssuanceTicketBook {
pub fn new<M: AsRef<[u8]>>(
deposit_id: DepositId,
identifier: M,
signing_key: identity::PrivateKey,
) -> Self {
//this expiration date will get fed to the ecash library, force midnight to be set
Self::new_with_expiration(
deposit_id,
identifier,
signing_key,
ecash_default_expiration_date(),
)
}
pub fn new_with_expiration<M: AsRef<[u8]>>(
deposit_id: DepositId,
identifier: M,
signing_key: identity::PrivateKey,
expiration_date: Date,
) -> Self {
let ecash_keypair = generate_keypair_user_from_seed(identifier);
IssuanceTicketBook {
deposit_id,
signing_key,
ecash_keypair,
expiration_date,
}
}
pub fn ecash_pubkey_bs58(&self) -> String {
use nym_credentials_interface::Base58;
self.ecash_keypair.public_key().to_bs58()
}
pub fn expiration_date(&self) -> Date {
self.expiration_date
}
pub fn request_plaintext(request: &WithdrawalRequest, deposit_id: DepositId) -> Vec<u8> {
let mut message = request.to_bytes();
message.extend_from_slice(&deposit_id.to_be_bytes());
message
}
fn request_signature(&self, signing_request: &CredentialSigningData) -> identity::Signature {
let message = Self::request_plaintext(&signing_request.withdrawal_request, self.deposit_id);
self.signing_key.sign(message)
}
pub fn create_blind_sign_request_body(
&self,
signing_request: &CredentialSigningData,
) -> BlindSignRequestBody {
let request_signature = self.request_signature(signing_request);
BlindSignRequestBody::new(
signing_request.withdrawal_request.clone(),
self.deposit_id,
request_signature,
signing_request.ecash_pub_key.clone(),
signing_request.expiration_date,
)
}
pub async fn obtain_blinded_credential(
&self,
client: &nym_validator_client::client::NymApiClient,
request_body: &BlindSignRequestBody,
) -> Result<BlindedSignature, Error> {
let server_response = client.blind_sign(request_body).await?;
Ok(server_response.blinded_signature)
}
pub fn deposit_id(&self) -> DepositId {
self.deposit_id
}
pub fn identity_key(&self) -> &identity::PrivateKey {
&self.signing_key
}
pub fn check_expiration_date(&self) -> bool {
self.expiration_date != cred_exp_date().ecash_date()
}
pub fn expired(&self) -> bool {
self.expiration_date < ecash_today().date()
}
pub fn prepare_for_signing(&self) -> CredentialSigningData {
// safety: the creation of the request can only fail if one provided invalid parameters
// and we created then specific to this type of the credential so the unwrap is fine
let (withdrawal_request, request_info) = withdrawal_request(
self.ecash_keypair.secret_key(),
self.expiration_date.ecash_unix_timestamp(),
)
.unwrap();
CredentialSigningData {
withdrawal_request,
request_info,
ecash_pub_key: self.ecash_keypair.public_key(),
expiration_date: self.expiration_date,
}
}
pub fn unblind_signature(
&self,
validator_vk: &VerificationKeyAuth,
signing_data: &CredentialSigningData,
blinded_signature: BlindedSignature,
signer_index: u64,
) -> Result<PartialWallet, Error> {
let unblinded_signature = issue_verify(
validator_vk,
self.ecash_keypair.secret_key(),
&blinded_signature,
&signing_data.request_info,
signer_index,
)?;
Ok(unblinded_signature)
}
// ideally this would have been generic over credential type, but we really don't need secp256k1 keys for bandwidth vouchers
pub async fn obtain_partial_bandwidth_voucher_credential(
&self,
client: &nym_validator_client::client::NymApiClient,
signer_index: u64,
validator_vk: &VerificationKeyAuth,
signing_data: CredentialSigningData,
) -> Result<PartialWallet, Error> {
// We need signing data, because they will be used at the aggregation step
let request = self.create_blind_sign_request_body(&signing_data);
let blinded_signature = self.obtain_blinded_credential(client, &request).await?;
self.unblind_signature(validator_vk, &signing_data, blinded_signature, signer_index)
}
// pub fn unchecked_aggregate_signature_shares(
// &self,
// shares: &[SignatureShare],
// ) -> Result<Signature, Error> {
// aggregate_signature_shares(shares).map_err(Error::SignatureAggregationError)
// }
pub fn aggregate_signature_shares(
&self,
verification_key: &VerificationKeyAuth,
shares: &[PartialWallet],
signing_data: CredentialSigningData,
) -> Result<WalletSignatures, Error> {
aggregate_wallets(
verification_key,
self.ecash_keypair.secret_key(),
shares,
&signing_data.request_info,
)
.map_err(Error::SignatureAggregationError)
.map(|w| w.into_wallet_signatures())
}
// also drops self after the conversion
pub fn into_issued_ticketbook(
self,
wallet: WalletSignatures,
epoch_id: EpochId,
) -> IssuedTicketBook {
self.to_issued_ticketbook(wallet, epoch_id)
}
pub fn to_issued_ticketbook(
&self,
wallet: WalletSignatures,
epoch_id: EpochId,
) -> IssuedTicketBook {
IssuedTicketBook::new(
wallet,
epoch_id,
self.ecash_keypair.secret_key().clone(),
self.expiration_date,
)
}
}
impl VersionedSerialise for IssuanceTicketBook {
const CURRENT_SERIALISATION_REVISION: u8 = 1;
fn try_unpack(b: &[u8], revision: impl Into<Option<u8>>) -> Result<Self, Error> {
let revision = revision
.into()
.unwrap_or(<Self as VersionedSerialise>::CURRENT_SERIALISATION_REVISION);
match revision {
1 => Self::try_unpack_current(b),
_ => Err(Error::UnknownSerializationRevision { revision }),
}
}
}
@@ -0,0 +1,174 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::ecash::bandwidth::serialiser::VersionedSerialise;
use crate::ecash::bandwidth::CredentialSpendingData;
use crate::ecash::utils::ecash_today;
use crate::error::Error;
use nym_credentials_interface::{
CoinIndexSignature, ExpirationDateSignature, PayInfo, SecretKeyUser, VerificationKeyAuth,
Wallet, WalletSignatures,
};
use nym_ecash_time::EcashTime;
use nym_validator_client::nym_api::EpochId;
use serde::{Deserialize, Serialize};
use std::borrow::Borrow;
use time::Date;
use zeroize::{Zeroize, ZeroizeOnDrop};
pub const CURRENT_SERIALIZATION_REVISION: u8 = 1;
// the only important thing to zeroize here are the private attributes, the rest can be made fully public for what we're concerned
#[derive(Zeroize, ZeroizeOnDrop, Serialize, Deserialize)]
pub struct IssuedTicketBook {
/// the underlying wallet signatures
signatures_wallet: WalletSignatures,
/// the counter indicating how many tickets have been spent so far
spent_tickets: u64,
/// Specifies the (DKG) epoch id when this credential has been issued
epoch_id: EpochId,
/// secret ecash key used to generate this wallet
ecash_secret_key: SecretKeyUser,
/// expiration_date for easier discarding
#[zeroize(skip)]
expiration_date: Date,
}
impl IssuedTicketBook {
pub fn new(
wallet: WalletSignatures,
epoch_id: EpochId,
ecash_secret_key: SecretKeyUser,
expiration_date: Date,
) -> Self {
IssuedTicketBook {
signatures_wallet: wallet,
spent_tickets: 0,
epoch_id,
ecash_secret_key,
expiration_date,
}
}
pub fn from_parts(
signatures_wallet: WalletSignatures,
epoch_id: EpochId,
ecash_secret_key: SecretKeyUser,
expiration_date: Date,
spent_tickets: u64,
) -> Self {
IssuedTicketBook {
signatures_wallet,
spent_tickets,
epoch_id,
ecash_secret_key,
expiration_date,
}
}
pub fn update_spent_tickets(&mut self, spent_tickets: u64) {
self.spent_tickets = spent_tickets
}
pub fn epoch_id(&self) -> EpochId {
self.epoch_id
}
pub fn current_serialization_revision(&self) -> u8 {
CURRENT_SERIALIZATION_REVISION
}
pub fn expiration_date(&self) -> Date {
self.expiration_date
}
pub fn expired(&self) -> bool {
self.expiration_date < ecash_today().date()
}
pub fn params_total_tickets(&self) -> u64 {
nym_credentials_interface::ecash_parameters().get_total_coins()
}
pub fn spent_tickets(&self) -> u64 {
self.spent_tickets
}
pub fn wallet(&self) -> &WalletSignatures {
&self.signatures_wallet
}
pub fn prepare_for_spending<BI, BE>(
&mut self,
verification_key: &VerificationKeyAuth,
pay_info: PayInfo,
coin_indices_signatures: &[BI],
expiration_date_signatures: &[BE],
tickets_to_spend: u64,
) -> Result<CredentialSpendingData, Error>
where
BI: Borrow<CoinIndexSignature>,
BE: Borrow<ExpirationDateSignature>,
{
let params = nym_credentials_interface::ecash_parameters();
let spend_date = ecash_today();
// make sure we still have enough tickets to spend
Wallet::ensure_allowance(params, self.spent_tickets, tickets_to_spend)?;
let payment = self.signatures_wallet.spend(
params,
verification_key,
&self.ecash_secret_key,
&pay_info,
self.spent_tickets,
tickets_to_spend,
expiration_date_signatures,
coin_indices_signatures,
spend_date.ecash_unix_timestamp(),
)?;
self.spent_tickets += tickets_to_spend;
Ok(CredentialSpendingData {
payment,
pay_info,
spend_date: spend_date.ecash_date(),
epoch_id: self.epoch_id,
})
}
}
impl VersionedSerialise for IssuedTicketBook {
const CURRENT_SERIALISATION_REVISION: u8 = 1;
fn try_unpack(b: &[u8], revision: impl Into<Option<u8>>) -> Result<Self, Error> {
let revision = revision
.into()
.unwrap_or(<Self as VersionedSerialise>::CURRENT_SERIALISATION_REVISION);
match revision {
1 => Self::try_unpack_current(b),
_ => Err(Error::UnknownSerializationRevision { revision }),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn assert_zeroize_on_drop<T: ZeroizeOnDrop>() {}
fn assert_zeroize<T: Zeroize>() {}
#[test]
fn credential_is_zeroized() {
assert_zeroize::<IssuedTicketBook>();
assert_zeroize_on_drop::<IssuedTicketBook>();
}
}
@@ -0,0 +1,10 @@
// Copyright 2021-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub use issuance::IssuanceTicketBook;
pub use issued::IssuedTicketBook;
pub use nym_credentials_interface::{CredentialSigningData, CredentialSpendingData};
pub mod issuance;
pub mod issued;
pub mod serialiser;
@@ -0,0 +1,65 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::ecash::bandwidth::issued::CURRENT_SERIALIZATION_REVISION;
use crate::Error;
use bincode::Options;
use serde::de::DeserializeOwned;
use serde::Serialize;
use std::marker::PhantomData;
pub struct VersionSerialised<T: ?Sized> {
pub data: Vec<u8>,
pub revision: u8,
// still wondering if there's any point in having the phantom in here
_phantom: PhantomData<T>,
}
pub trait VersionedSerialise {
const CURRENT_SERIALISATION_REVISION: u8;
fn current_serialization_revision(&self) -> u8 {
CURRENT_SERIALIZATION_REVISION
}
// implicitly always uses current revision
fn pack(&self) -> VersionSerialised<Self>
where
Self: Serialize,
{
let data = make_current_storable_bincode_serializer()
.serialize(self)
.expect("serialisation failure");
VersionSerialised {
data,
revision: Self::CURRENT_SERIALISATION_REVISION,
_phantom: Default::default(),
}
}
fn try_unpack_current(b: &[u8]) -> Result<Self, Error>
where
Self: DeserializeOwned,
{
make_current_storable_bincode_serializer()
.deserialize(b)
.map_err(|source| Error::SerializationFailure {
source,
revision: Self::CURRENT_SERIALISATION_REVISION,
})
}
// this is up to whoever implements the trait to provide function implementation,
// as they might have to have different implementations per revision
fn try_unpack(b: &[u8], revision: impl Into<Option<u8>>) -> Result<Self, Error>
where
Self: DeserializeOwned;
}
fn make_current_storable_bincode_serializer() -> impl bincode::Options {
bincode::DefaultOptions::new()
.with_big_endian()
.with_varint_encoding()
}
+210
View File
@@ -0,0 +1,210 @@
// Copyright 2021-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::ecash::bandwidth::IssuanceTicketBook;
use crate::error::Error;
use log::{debug, warn};
use nym_credentials_interface::{
aggregate_expiration_signatures, aggregate_indices_signatures, Base58, CoinIndexSignature,
CoinIndexSignatureShare, ExpirationDateSignature, ExpirationDateSignatureShare,
VerificationKeyAuth, WalletSignatures,
};
use nym_validator_client::client::EcashApiClient;
// so we wouldn't break all the existing imports
pub use nym_ecash_time::{cred_exp_date, ecash_date_offset, ecash_today, EcashTime};
pub fn aggregate_verification_keys(
api_clients: &[EcashApiClient],
) -> Result<VerificationKeyAuth, Error> {
if api_clients.is_empty() {
return Err(Error::NoValidatorsAvailable);
}
let indices: Vec<_> = api_clients
.iter()
.map(|api_client| api_client.node_id)
.collect();
let shares: Vec<_> = api_clients
.iter()
.map(|api_client| api_client.verification_key.clone())
.collect();
Ok(nym_credentials_interface::aggregate_verification_keys(
&shares,
Some(&indices),
)?)
}
pub fn obtain_aggregated_verification_key(
_api_clients: &[EcashApiClient],
) -> Result<VerificationKeyAuth, Error> {
// TODO:
// let total = api_clients.len();
// let mut rng = thread_rng();
// let indices = sample(&mut rng, total, total);
// for index in indices {
// // randomly try apis until we succeed
// // if let Ok(res) = api_clients[index].api_client.get_aggregated_verification_key().await {
// // //
// // }
// }
todo!()
}
pub async fn obtain_expiration_date_signatures(
ecash_api_clients: &[EcashApiClient],
verification_key: &VerificationKeyAuth,
threshold: u64,
) -> Result<Vec<ExpirationDateSignature>, Error> {
if ecash_api_clients.is_empty() {
return Err(Error::NoValidatorsAvailable);
}
let mut signatures_shares: Vec<_> = Vec::with_capacity(ecash_api_clients.len());
let expiration_date = cred_exp_date().unix_timestamp() as u64;
for ecash_api_client in ecash_api_clients.iter() {
match ecash_api_client
.api_client
.partial_expiration_date_signatures(None)
.await
{
Ok(signature) => {
let index = ecash_api_client.node_id;
let key_share = ecash_api_client.verification_key.clone();
signatures_shares.push(ExpirationDateSignatureShare {
index,
key: key_share,
signatures: signature.signatures,
});
}
Err(err) => {
warn!(
"failed to obtain expiration date signature from {}: {err}",
ecash_api_client.api_client.api_url()
);
}
}
}
if signatures_shares.len() < threshold as usize {
return Err(Error::NotEnoughShares);
}
//this already takes care of partial signatures validation
aggregate_expiration_signatures(verification_key, expiration_date, &signatures_shares)
.map_err(Error::CompactEcashError)
}
pub async fn obtain_coin_indices_signatures(
ecash_api_clients: &[EcashApiClient],
verification_key: &VerificationKeyAuth,
threshold: u64,
) -> Result<Vec<CoinIndexSignature>, Error> {
if ecash_api_clients.is_empty() {
return Err(Error::NoValidatorsAvailable);
}
let mut signatures_shares: Vec<_> = Vec::with_capacity(ecash_api_clients.len());
for ecash_api_client in ecash_api_clients.iter() {
match ecash_api_client
.api_client
.partial_coin_indices_signatures(None)
.await
{
Ok(signature) => {
let index = ecash_api_client.node_id;
let key_share = ecash_api_client.verification_key.clone();
signatures_shares.push(CoinIndexSignatureShare {
index,
key: key_share,
signatures: signature.signatures,
});
}
Err(err) => {
warn!(
"failed to obtain expiration date signature from {}: {err}",
ecash_api_client.api_client.api_url()
);
}
}
}
if signatures_shares.len() < threshold as usize {
return Err(Error::NotEnoughShares);
}
//this takes care of validating partial signatures
aggregate_indices_signatures(
nym_credentials_interface::ecash_parameters(),
verification_key,
&signatures_shares,
)
.map_err(Error::CompactEcashError)
}
pub async fn obtain_aggregate_wallet(
voucher: &IssuanceTicketBook,
ecash_api_clients: &[EcashApiClient],
threshold: u64,
) -> Result<WalletSignatures, Error> {
if ecash_api_clients.len() < threshold as usize {
return Err(Error::NoValidatorsAvailable);
}
let verification_key = aggregate_verification_keys(ecash_api_clients)?;
let request = voucher.prepare_for_signing();
let mut wallets = Vec::with_capacity(ecash_api_clients.len());
// TODO: optimise and query just threshold
for ecash_api_client in ecash_api_clients.iter() {
debug!(
"attempting to obtain partial credential from {}",
ecash_api_client.api_client.api_url()
);
match voucher
.obtain_partial_bandwidth_voucher_credential(
&ecash_api_client.api_client,
ecash_api_client.node_id,
&ecash_api_client.verification_key,
request.clone(),
)
.await
{
Ok(wallet) => wallets.push(wallet),
Err(err) => {
warn!("failed to obtain partial credential from API {ecash_api_client}: {err}",);
}
};
}
if wallets.len() < threshold as usize {
return Err(Error::NotEnoughShares);
}
voucher.aggregate_signature_shares(&verification_key, &wallets, request)
}
pub fn signatures_to_string<B>(sigs: &[B]) -> String
where
B: Base58,
{
sigs.iter()
.map(|sig| sig.to_bs58())
.collect::<Vec<_>>()
.join(",")
}
pub fn signatures_from_string<B>(bs58_sigs: String) -> Result<Vec<B>, Error>
where
B: Base58,
{
bs58_sigs
.split(',')
.map(B::try_from_bs58)
.collect::<Result<Vec<_>, _>>()
.map_err(Error::CompactEcashError)
}

Some files were not shown because too many files have changed in this diff Show More