Compare commits

..

3 Commits

Author SHA1 Message Date
Bogdan-Ștefan Neacşu eaa0f055af Move const to wireguard types 2024-08-06 14:56:33 +00:00
Bogdan-Ștefan Neacşu ad507c6a12 Use a more proper timeout value 2024-08-06 14:56:24 +00:00
Jon Häggblad e5d68a5e7f Don't set NYM_VPN_API to default (#4740) 2024-07-31 11:45:36 +02:00
1221 changed files with 11743 additions and 70883 deletions
-7
View File
@@ -1,7 +0,0 @@
.git
.github
.gitignore
**/node_modules
**/target
dist
documentation
@@ -8,6 +8,11 @@ on:
required: true
default: false
type: boolean
enable_wireguard:
description: "Add --features wireguard"
required: true
default: false
type: boolean
enable_deb:
description: "True to enable cargo-deb installation and .deb package building"
required: false
@@ -37,7 +42,7 @@ jobs:
strategy:
fail-fast: false
matrix:
platform: [ ubuntu-20.04 ]
platform: [ubuntu-20.04]
runs-on: ${{ matrix.platform }}
env:
@@ -65,6 +70,9 @@ jobs:
- name: Set CARGO_FEATURES
run: |
echo 'CARGO_FEATURES=--features wireguard' >> $GITHUB_ENV
if: >
github.event_name == 'schedule' ||
(github.event_name == 'workflow_dispatch' && inputs.enable_wireguard == true)
- name: Install Rust stable
uses: actions-rs/toolchain@v1
@@ -15,7 +15,7 @@ jobs:
strategy:
fail-fast: false
matrix:
platform: [ ubuntu-20.04 ]
platform: [ubuntu-20.04]
runs-on: ${{ matrix.platform }}
env:
@@ -58,7 +58,6 @@ 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
@@ -1,10 +1,9 @@
name: ci-sdk-docs-typescript
on:
workflow_dispatch:
pull_request:
paths:
- "documentation/"
- "sdk/typescript/**"
- "wasm/**"
jobs:
@@ -29,7 +28,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: "1.20"
go-version: '1.20'
- name: Install wasm-pack
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
@@ -37,7 +36,7 @@ jobs:
- name: Install wasm-opt
uses: ./.github/actions/install-wasm-opt
with:
version: "116"
version: '116'
- name: Build branch WASM packages
run: make sdk-wasm-build
+1 -4
View File
@@ -48,7 +48,4 @@ foxyfox.env
.next
ppa-private-key.b64
ppa-private-key.asc
nym-network-monitor/topology.json
nym-network-monitor/__pycache__
nym-network-monitor/*.key
ppa-private-key.asc
-14
View File
@@ -4,20 +4,6 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
## [Unreleased]
## [2024.9-topdeck] (2024-07-26)
- chore: fix 1.80 lint issues ([#4731])
- Handle clients with different versions in IPR ([#4723])
- Add 1GB/day/user bandwidth cap ([#4717])
- Feature/merge back ([#4710])
- removed mixnode/gateway config migration code and disabled cli without explicit flag ([#4706])
[#4731]: https://github.com/nymtech/nym/pull/4731
[#4723]: https://github.com/nymtech/nym/pull/4723
[#4717]: https://github.com/nymtech/nym/pull/4717
[#4710]: https://github.com/nymtech/nym/pull/4710
[#4706]: https://github.com/nymtech/nym/pull/4706
## [2024.8-wispa] (2024-07-10)
- add event parsing to support cosmos_sdk > 0.50 ([#4697])
Generated
+403 -1198
View File
File diff suppressed because it is too large Load Diff
+11 -33
View File
@@ -14,6 +14,7 @@ panic = "abort"
opt-level = 3
[workspace]
resolver = "2"
members = [
"clients/native",
@@ -33,7 +34,6 @@ 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,12 +47,8 @@ members = [
"common/credentials-interface",
"common/crypto",
"common/dkg",
"common/ecash-double-spending",
"common/ecash-time",
"common/execute",
"common/exit-policy",
"common/gateway-requests",
"common/gateway-storage",
"common/http-api-client",
"common/http-api-common",
"common/inclusion-probability",
@@ -63,7 +59,6 @@ members = [
"common/node-tester-utils",
"common/nonexhaustive-delayqueue",
"common/nymcoconut",
"common/nym_offline_compact_ecash",
"common/nym-id",
"common/nym-metrics",
"common/nymsphinx",
@@ -79,7 +74,6 @@ members = [
"common/nymsphinx/types",
"common/nyxd-scraper",
"common/pemstore",
"common/serde-helpers",
"common/socks5-client-core",
"common/socks5/proxy-helpers",
"common/socks5/requests",
@@ -93,11 +87,11 @@ members = [
"common/wasm/utils",
"common/wireguard",
"common/wireguard-types",
"documentation/autodoc",
"explorer-api",
"explorer-api/explorer-api-requests",
"explorer-api/explorer-client",
"gateway",
"gateway/gateway-requests",
"integrations/bity",
"mixnode",
"sdk/lib/socks5-listener",
@@ -106,7 +100,6 @@ members = [
"service-providers/common",
"service-providers/ip-packet-router",
"service-providers/network-requester",
"nym-network-monitor",
"nym-api",
"nym-browser-extension/storage",
"nym-api/nym-api-requests",
@@ -127,8 +120,6 @@ members = [
"wasm/mix-fetch",
"wasm/node-tester",
"wasm/zknym-lib",
"tools/internal/testnet-manager",
"tools/internal/testnet-manager/dkg-bypass-contract",
]
default-members = [
@@ -141,14 +132,13 @@ default-members = [
"tools/nymvisor",
"explorer-api",
"nym-validator-rewarder",
"nym-node",
"nym-node"
]
exclude = [
"explorer",
"contracts",
"nym-wallet",
"nym-vpn/ui/src-tauri",
"cpu-cycles",
"sdk/ffi/cpp",
]
@@ -160,7 +150,6 @@ homepage = "https://nymtech.net"
documentation = "https://nymtech.net"
edition = "2021"
license = "Apache-2.0"
rust-version = "1.80"
[workspace.dependencies]
addr = "0.15.6"
@@ -174,13 +163,8 @@ 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"
@@ -232,11 +216,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"
@@ -321,8 +305,7 @@ 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
# 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" }
bls12_381 = { git = "https://github.com/jstuczyn/bls12_381", default-features = false, branch = "feature/gt-serialization-0.8.0" }
group = { version = "0.13.0", default-features = false }
ff = { version = "0.13.0", default-features = false }
@@ -345,22 +328,16 @@ 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 (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
# 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
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"
@@ -368,6 +345,7 @@ 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 nym_ecash
CONTRACTS=vesting_contract mixnet_contract
CONTRACTS_WASM=$(addsuffix .wasm, $(CONTRACTS))
CONTRACTS_OUT_DIR=contracts/target/wasm32-unknown-unknown/release
+7 -23
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-client"
version = "1.1.39"
version = "1.1.38"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
description = "Implementation of the Nym Client"
edition = "2021"
@@ -26,46 +26,30 @@ clap = { workspace = true, features = ["cargo", "derive"] }
dirs = { workspace = true }
log = { workspace = true } # self explanatory
rand = { workspace = true }
serde = { workspace = true, features = [
"derive",
] } # for config serialization/deserialization
serde = { workspace = true, features = ["derive"] } # for config serialization/deserialization
serde_json = { workspace = true }
thiserror = { workspace = true }
tap = { workspace = true }
time = { workspace = true }
tokio = { workspace = true, features = [
"rt-multi-thread",
"net",
"signal",
] } # async runtime
tokio = { workspace = true, features = ["rt-multi-thread", "net", "signal"] } # async runtime
tokio-tungstenite = { workspace = true }
zeroize = { workspace = true }
## internal
nym-bandwidth-controller = { path = "../../common/bandwidth-controller" }
nym-bin-common = { path = "../../common/bin-common", features = [
"output_format",
"clap",
] }
nym-client-core = { path = "../../common/client-core", features = [
"fs-credentials-storage",
"fs-surb-storage",
"fs-gateways-storage",
"cli",
] }
nym-bin-common = { path = "../../common/bin-common", features = ["output_format", "clap"] }
nym-client-core = { path = "../../common/client-core", features = ["fs-surb-storage", "fs-gateways-storage", "cli"] }
nym-config = { path = "../../common/config" }
nym-credential-storage = { path = "../../common/credential-storage" }
nym-credentials = { path = "../../common/credentials" }
nym-crypto = { path = "../../common/crypto" }
nym-gateway-requests = { path = "../../common/gateway-requests" }
nym-gateway-requests = { path = "../../gateway/gateway-requests" }
nym-network-defaults = { path = "../../common/network-defaults" }
nym-sphinx = { path = "../../common/nymsphinx" }
nym-pemstore = { path = "../../common/pemstore" }
nym-task = { path = "../../common/task" }
nym-topology = { path = "../../common/topology" }
nym-validator-client = { path = "../../common/client-libs/validator-client", features = [
"http-client",
] }
nym-validator-client = { path = "../../common/client-libs/validator-client", features = ["http-client"] }
nym-client-websocket-requests = { path = "websocket-requests" }
nym-id = { path = "../../common/nym-id" }
-5
View File
@@ -26,7 +26,6 @@ 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;
@@ -85,9 +84,6 @@ 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),
@@ -120,7 +116,6 @@ 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),
@@ -1,32 +0,0 @@
// 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(())
}
+1 -1
View File
@@ -422,7 +422,7 @@ impl Handler {
) {
// We don't want a crash in the connection handler to trigger a shutdown of the whole
// process.
task_client.disarm();
task_client.mark_as_success();
let ws_stream = match accept_async(socket).await {
Ok(ws_stream) => ws_stream,
+6 -18
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-socks5-client"
version = "1.1.39"
version = "1.1.38"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address"
edition = "2021"
@@ -11,9 +11,7 @@ license.workspace = true
bs58 = { workspace = true }
clap = { workspace = true, features = ["cargo", "derive"] }
log = { workspace = true }
serde = { workspace = true, features = [
"derive",
] } # for config serialization/deserialization
serde = { workspace = true, features = ["derive"] } # for config serialization/deserialization
serde_json = { workspace = true }
tap = { workspace = true }
thiserror = { workspace = true }
@@ -24,21 +22,13 @@ url = { workspace = true }
zeroize = { workspace = true }
# internal
nym-bin-common = { path = "../../common/bin-common", features = [
"output_format",
"clap",
] }
nym-client-core = { path = "../../common/client-core", features = [
"fs-credentials-storage",
"fs-surb-storage",
"fs-gateways-storage",
"cli",
] }
nym-bin-common = { path = "../../common/bin-common", features = ["output_format"] }
nym-client-core = { path = "../../common/client-core", features = ["fs-surb-storage", "fs-gateways-storage", "cli"] }
nym-config = { path = "../../common/config" }
nym-credential-storage = { path = "../../common/credential-storage" }
nym-credentials = { path = "../../common/credentials" }
nym-crypto = { path = "../../common/crypto" }
nym-gateway-requests = { path = "../../common/gateway-requests" }
nym-gateway-requests = { path = "../../gateway/gateway-requests" }
nym-id = { path = "../../common/nym-id" }
nym-network-defaults = { path = "../../common/network-defaults" }
nym-ordered-buffer = { path = "../../common/socks5/ordered-buffer" }
@@ -46,9 +36,7 @@ nym-pemstore = { path = "../../common/pemstore" }
nym-socks5-client-core = { path = "../../common/socks5-client-core" }
nym-sphinx = { path = "../../common/nymsphinx" }
nym-topology = { path = "../../common/topology" }
nym-validator-client = { path = "../../common/client-libs/validator-client", features = [
"http-client",
] }
nym-validator-client = { path = "../../common/client-libs/validator-client", features = ["http-client"] }
[features]
default = []
-5
View File
@@ -30,7 +30,6 @@ mod import_credential;
pub mod init;
mod list_gateways;
pub(crate) mod run;
mod show_ticketbooks;
mod switch_gateway;
pub(crate) struct CliSocks5Client;
@@ -89,9 +88,6 @@ 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),
@@ -127,7 +123,6 @@ 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),
@@ -1,32 +0,0 @@
// 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(())
}
+1 -2
View File
@@ -14,14 +14,13 @@ thiserror = { workspace = true }
url = { workspace = true }
zeroize = { workspace = true }
nym-ecash-time = { path = "../ecash-time" }
nym-coconut = { path = "../nymcoconut" }
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"
+50 -89
View File
@@ -1,126 +1,87 @@
// Copyright 2023-2024 - Nym Technologies SA <contact@nymtech.net>
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::error::BandwidthControllerError;
use crate::utils::{get_coin_index_signatures, get_expiration_date_signatures};
use log::info;
use nym_credential_storage::models::StorableIssuedCredential;
use nym_credential_storage::storage::Storage;
use nym_credentials::ecash::bandwidth::IssuanceTicketBook;
use nym_credentials::ecash::utils::obtain_aggregate_wallet;
use nym_credentials::IssuedTicketBook;
use nym_credentials_interface::TicketType;
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::EcashSigningClient;
use nym_validator_client::nyxd::contract_traits::{DkgQueryClient, EcashQueryClient};
use nym_validator_client::nyxd::cosmwasm_client::ToSingletonContractData;
use nym_validator_client::EcashApiClient;
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_validator_client::nyxd::contract_traits::DkgQueryClient;
use nym_validator_client::nyxd::Coin;
use rand::rngs::OsRng;
use state::State;
use zeroize::Zeroizing;
pub async fn make_deposit<C>(
client: &C,
client_id: &[u8],
expiration: Option<Date>,
ticketbook_type: TicketType,
) -> Result<IssuanceTicketBook, BandwidthControllerError>
pub mod state;
pub async fn deposit<C>(client: &C, amount: Coin) -> Result<State, BandwidthControllerError>
where
C: EcashSigningClient + EcashQueryClient + Sync,
C: CoconutBandwidthSigningClient + Sync,
{
let mut rng = OsRng;
let signing_key = identity::PrivateKey::new(&mut rng);
let expiration = expiration.unwrap_or_else(ecash_default_expiration_date);
let encryption_key = encryption::PrivateKey::new(&mut rng);
let deposit_amount = client.get_required_deposit_amount().await?;
info!("we'll need to deposit {deposit_amount} to obtain the ticketbook");
let result = client
.make_ticketbook_deposit(
let tx_hash = client
.deposit(
amount.clone(),
CredentialType::Voucher.to_string(),
signing_key.public_key().to_base58_string(),
deposit_amount.into(),
encryption_key.public_key().to_base58_string(),
None,
)
.await?;
.await?
.transaction_hash;
let deposit_id = result.parse_singleton_u32_contract_data()?;
let voucher =
IssuanceBandwidthCredential::new_voucher(amount, tx_hash, signing_key, encryption_key);
info!("our ticketbook deposit has been stored under id {deposit_id}");
let state = State { voucher };
Ok(IssuanceTicketBook::new_with_expiration(
deposit_id,
client_id,
signing_key,
ticketbook_type,
expiration,
))
Ok(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,
pub async fn get_bandwidth_voucher<C, St>(
state: &State,
client: &C,
storage: &St,
apis: Option<Vec<EcashApiClient>>,
) -> Result<IssuedTicketBook, BandwidthControllerError>
) -> Result<(), 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 apis = match apis {
Some(apis) => apis,
None => all_ecash_api_clients(client, epoch_id).await?,
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!"),
};
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_ticketbook(&issued)
.insert_issued_credential(storable)
.await
.map_err(|err| BandwidthControllerError::CredentialStorageError(Box::new(err)))?;
Ok(issued)
.map_err(|err| BandwidthControllerError::CredentialStorageError(Box::new(err)))
}
@@ -0,0 +1,14 @@
// 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 }
}
}
+5 -16
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::EcashApiError;
use nym_validator_client::coconut::CoconutApiError;
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] EcashApiError),
CoconutApiError(#[from] CoconutApiError),
#[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("Ecash error - {0}")]
EcashError(#[from] CompactEcashError),
#[error("Coconut error - {0}")]
CoconutError(#[from] CoconutError),
#[error("Validator client error - {0}")]
ValidatorError(#[from] ValidatorClientError),
@@ -51,15 +51,4 @@ 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))
}
}
+90 -149
View File
@@ -1,24 +1,16 @@
// 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::{
get_aggregate_verification_key, get_coin_index_signatures, get_expiration_date_signatures,
ApiClientsWrapper,
};
use log::error;
use nym_credential_storage::models::RetrievedTicketbook;
use crate::utils::stored_credential_to_issued_bandwidth;
use log::{debug, error, warn};
use nym_credential_storage::storage::Storage;
use nym_credentials::ecash::bandwidth::CredentialSpendingData;
use nym_credentials_interface::{
AnnotatedCoinIndexSignature, AnnotatedExpirationDateSignature, NymPayInfo, VerificationKeyAuth,
};
use nym_ecash_time::Date;
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_validator_client::nym_api::EpochId;
use nym_validator_client::nyxd::contract_traits::DkgQueryClient;
@@ -43,20 +35,13 @@ pub struct PreparedCredential {
/// could use correct verification key for validation.
pub epoch_id: EpochId,
/// Auxiliary metadata associated with the withdrawn credential
pub metadata: PreparedCredentialMetadata,
/// The database id of the stored credential.
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,
pub struct RetrievedCredential {
pub credential: IssuedBandwidthCredential,
pub credential_id: i64,
}
impl<C, St: Storage> BandwidthController<C, St> {
@@ -65,155 +50,111 @@ impl<C, St: Storage> BandwidthController<C, St> {
}
/// Tries to retrieve one of the stored, unused credentials that hasn't yet expired.
pub async fn get_next_usable_ticketbook(
/// It marks any retrieved intermediate credentials as expired.
pub async fn get_next_usable_credential(
&self,
tickets: u32,
) -> Result<RetrievedTicketbook, BandwidthControllerError>
gateway_id: &str,
) -> Result<RetrievedCredential, BandwidthControllerError>
where
<St as Storage>::StorageError: Send + Sync + 'static,
{
let Some(ticketbook) = self
.storage
.get_next_unspent_usable_ticketbook(tickets)
.await
.map_err(BandwidthControllerError::credential_storage_error)?
else {
return Err(BandwidthControllerError::NoCredentialsAvailable);
};
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;
Ok(ticketbook)
// 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,
});
}
}
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)
pub fn storage(&self) -> &St {
&self.storage
}
async fn get_aggregate_verification_key(
&self,
epoch_id: EpochId,
apis: &mut ApiClientsWrapper,
) -> Result<VerificationKeyAuth, BandwidthControllerError>
) -> Result<VerificationKey, 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_aggregate_verification_key(&self.storage, epoch_id, ecash_apis).await
let coconut_api_clients = all_coconut_api_clients(&self.client, epoch_id).await?;
Ok(obtain_aggregate_verification_key(&coconut_api_clients)?)
}
async fn get_coin_index_signatures(
pub async fn prepare_bandwidth_credential(
&self,
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,
gateway_id: &str,
) -> Result<PreparedCredential, BandwidthControllerError>
where
C: DkgQueryClient + Sync + Send,
<St as Storage>::StorageError: Send + Sync + 'static,
{
let retrieved_ticketbook = self.get_next_usable_ticketbook(tickets_to_spend).await?;
let retrieved_credential = self.get_next_usable_credential(gateway_id).await?;
let ticketbook_id = retrieved_ticketbook.ticketbook_id;
let epoch_id = retrieved_ticketbook.ticketbook.epoch_id();
let epoch_id = retrieved_credential.credential.epoch_id();
let credential_id = retrieved_credential.credential_id;
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 verification_key = self.get_aggregate_verification_key(epoch_id).await?;
match self
.prepare_ecash_ticket_inner(provider_pk, tickets_to_spend, retrieved_ticketbook)
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)
.await
{
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)
}
}
.map_err(|err| BandwidthControllerError::CredentialStorageError(Box::new(err)))
}
}
+15 -174
View File
@@ -2,180 +2,21 @@
// SPDX-License-Identifier: Apache-2.0
use crate::error::BandwidthControllerError;
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;
use nym_credential_storage::models::StoredIssuedCredential;
use nym_credentials::coconut::bandwidth::issued::CURRENT_SERIALIZATION_REVISION;
use nym_credentials::coconut::bandwidth::IssuedBandwidthCredential;
// 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)
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,
},
);
}
}
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)
Ok(IssuedBandwidthCredential::unpack_v1(&cred.credential_data)?)
}
+3 -10
View File
@@ -14,12 +14,10 @@ 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 }
rand = { workspace = true }
rand_chacha = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
sha2 = { workspace = true }
@@ -39,7 +37,7 @@ nym-country-group = { path = "../country-group" }
nym-crypto = { path = "../crypto" }
nym-explorer-client = { path = "../../explorer-api/explorer-client" }
nym-gateway-client = { path = "../client-libs/gateway-client" }
nym-gateway-requests = { path = "../gateway-requests" }
nym-gateway-requests = { path = "../../gateway/gateway-requests" }
nym-metrics = { path = "../nym-metrics" }
nym-nonexhaustive-delayqueue = { path = "../nonexhaustive-delayqueue" }
nym-sphinx = { path = "../nymsphinx" }
@@ -47,15 +45,11 @@ nym-pemstore = { path = "../pemstore" }
nym-topology = { path = "../topology", features = ["serializable"] }
nym-validator-client = { path = "../client-libs/validator-client", default-features = false }
nym-task = { path = "../task" }
nym-credentials-interface = { path = "../credentials-interface" }
nym-credential-storage = { path = "../credential-storage" }
nym-network-defaults = { path = "../network-defaults" }
nym-client-core-config-types = { path = "./config-types", features = [
"disk-persistence",
] }
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]
@@ -118,8 +112,7 @@ tempfile = { workspace = true }
[features]
default = []
cli = ["clap", "comfy-table"]
fs-credentials-storage = ["nym-credential-storage/persistent-storage"]
cli = ["clap"]
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"]
@@ -18,7 +18,7 @@ url.workspace = true
zeroize = { workspace = true, features = ["zeroize_derive"] }
nym-crypto = { path = "../../crypto", features = ["asymmetric"] }
nym-gateway-requests = { path = "../../gateway-requests" }
nym-gateway-requests = { path = "../../../gateway/gateway-requests" }
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx]
workspace = true
@@ -27,12 +27,7 @@ optional = true
[build-dependencies]
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
sqlx = { workspace = true, features = [
"runtime-tokio-rustls",
"sqlite",
"macros",
"migrate",
] }
sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] }
[features]
fs-gateways-storage = ["sqlx"]
fs-gateways-storage = ["sqlx"]
@@ -1,140 +0,0 @@
// 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_credentials_interface::TicketType;
use nym_ecash_time::ecash_today;
use serde::{Deserialize, Serialize};
use time::Date;
#[derive(Serialize, Deserialize)]
pub struct AvailableTicketbook {
pub id: i64,
pub typ: TicketType,
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()),
comfy_table::Cell::new(self.typ),
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 TryFrom<BasicTicketbookInformation> for AvailableTicketbook {
type Error = ClientCoreError;
fn try_from(value: BasicTicketbookInformation) -> Result<Self, Self::Error> {
let typ = value
.ticketbook_type
.parse()
.map_err(|_| ClientCoreError::UnknownTicketType)?;
Ok(AvailableTicketbook {
id: value.id,
typ,
expiration: value.expiration_date,
issued_tickets: value.total_tickets,
claimed_tickets: value.used_tickets,
ticket_size: typ.to_repr().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",
"type",
"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(TryInto::<AvailableTicketbook>::try_into)
.collect::<Result<_, _>>()?,
))
}
@@ -6,7 +6,6 @@ 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;
@@ -40,7 +40,6 @@ use nym_bandwidth_controller::BandwidthController;
use nym_client_core_gateways_storage::{GatewayDetails, GatewaysDetailsStore};
use nym_credential_storage::storage::Storage as CredentialStorage;
use nym_crypto::asymmetric::{encryption, identity};
use nym_gateway_client::client::config::GatewayClientConfig;
use nym_gateway_client::{
AcknowledgementReceiver, GatewayClient, GatewayConfig, MixnetMessageReceiver, PacketRouter,
};
@@ -404,11 +403,6 @@ where
gateway_listener,
);
GatewayClient::new(
GatewayClientConfig::new_default()
.with_disabled_credentials_mode(config.client.disabled_credentials_mode)
.with_response_timeout(
config.debug.gateway_connection.gateway_response_timeout,
),
cfg,
managed_keys.identity_keypair(),
Some(details.derived_aes128_ctr_blake3_hmac_keys),
@@ -416,6 +410,8 @@ where
bandwidth_controller,
shutdown,
)
.with_disabled_credentials_mode(config.client.disabled_credentials_mode)
.with_response_timeout(config.debug.gateway_connection.gateway_response_timeout)
};
gateway_client
@@ -455,7 +451,7 @@ where
Err(ClientCoreError::CustomGatewaySelectionExpected)
} else {
// and make sure to invalidate the task client so we wouldn't cause premature shutdown
shutdown.disarm();
shutdown.mark_as_success();
custom_gateway_transceiver.set_packet_router(packet_router)?;
Ok(custom_gateway_transceiver)
};
@@ -562,7 +558,7 @@ where
if topology_config.disable_refreshing {
// if we're not spawning the refresher, don't cause shutdown immediately
info!("The topology refesher is not going to be started");
shutdown.disarm();
shutdown.mark_as_success();
} else {
// don't spawn the refresher if we don't want to be refreshing the topology.
// only use the initial values obtained
@@ -23,7 +23,7 @@ use crate::{
config::{self, disk_persistence::CommonClientPaths},
error::ClientCoreError,
};
#[cfg(all(not(target_arch = "wasm32"), feature = "fs-credentials-storage"))]
#[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))]
use nym_credential_storage::persistent_storage::PersistentStorage as PersistentCredentialStorage;
pub use nym_client_core_gateways_storage as gateways_storage;
@@ -3,12 +3,10 @@
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;
@@ -113,9 +111,8 @@ impl<C, St> RemoteGateway<C, St> {
impl<C, St> GatewayTransceiver for RemoteGateway<C, St>
where
C: DkgQueryClient + Send + Sync,
St: CredentialStorage,
<St as CredentialStorage>::StorageError: Send + Sync + 'static,
C: Send,
St: Send,
{
fn gateway_identity(&self) -> identity::PublicKey {
self.gateway_client.gateway_identity()
@@ -129,9 +126,8 @@ where
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl<C, St> GatewaySender for RemoteGateway<C, St>
where
C: DkgQueryClient + Send + Sync,
St: CredentialStorage,
<St as CredentialStorage>::StorageError: Send + Sync + 'static,
C: Send,
St: Send,
{
async fn send_mix_packet(&mut self, packet: MixPacket) -> Result<(), ErasedGatewayError> {
self.gateway_client
@@ -458,7 +458,7 @@ impl PacketStatisticsControl {
fn report_rates(&self) {
if let Some((_, rates)) = self.rates.back() {
log::debug!("{}", rates.summary());
log::info!("{}", rates.summary());
log::debug!("{}", rates.detailed_summary());
}
}
@@ -486,7 +486,7 @@ impl PacketStatisticsControl {
// Check what the number of retransmissions was during the recording window
if let Some((_, start_stats)) = self.history.front() {
let delta = self.stats.clone() - start_stats.clone();
log::debug!(
log::info!(
"mix packet retransmissions/real mix packets: {}/{}",
delta.retransmissions_queued,
delta.real_packets_queued,
@@ -453,7 +453,6 @@ where
let mut pending_acks = Vec::with_capacity(fragments.len());
let mut real_messages = Vec::with_capacity(fragments.len());
debug!("Splitting message into {} fragments", fragments.len());
for fragment in fragments {
// we need to clone it because we need to keep it in memory in case we had to retransmit
// it. And then we'd need to recreate entire ACK again.
@@ -474,6 +474,13 @@ where
Poll::Ready(Some((real_messages, conn_id))) => {
log::trace!("handling real_messages: size: {}", real_messages.len());
// This is the last step in the pipeline where we know the type of the message, so
// lets count the number of retransmissions here.
if conn_id == TransmissionLane::Retransmission {
self.stats_tx
.report(PacketStatisticsEvent::RetransmissionQueued);
}
// First store what we got for the given connection id
self.transmission_buffer.store(&conn_id, real_messages);
let real_next = self.pop_next_message().expect("we just added one");
-8
View File
@@ -63,14 +63,6 @@ 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 provided ticket type is invalid")]
UnknownTicketType,
#[error("the gateway id is invalid - {0}")]
UnableToCreatePublicKeyFromGatewayId(Ed25519RecoveryError),
+18 -40
View File
@@ -46,34 +46,13 @@ const MEASUREMENTS: usize = 3;
const CONN_TIMEOUT: Duration = Duration::from_millis(1500);
const PING_TIMEOUT: Duration = Duration::from_millis(1000);
// The abstraction that some of these helpers use
pub trait ConnectableGateway {
fn identity(&self) -> &identity::PublicKey;
fn clients_address(&self) -> String;
fn is_wss(&self) -> bool;
}
impl ConnectableGateway for gateway::Node {
fn identity(&self) -> &identity::PublicKey {
self.identity()
}
fn clients_address(&self) -> String {
self.clients_address()
}
fn is_wss(&self) -> bool {
self.clients_wss_port.is_some()
}
}
struct GatewayWithLatency<'a, G: ConnectableGateway> {
gateway: &'a G,
struct GatewayWithLatency<'a> {
gateway: &'a gateway::Node,
latency: Duration,
}
impl<'a, G: ConnectableGateway> GatewayWithLatency<'a, G> {
fn new(gateway: &'a G, latency: Duration) -> Self {
impl<'a> GatewayWithLatency<'a> {
fn new(gateway: &'a gateway::Node, latency: Duration) -> Self {
GatewayWithLatency { gateway, latency }
}
}
@@ -151,14 +130,11 @@ async fn connect(endpoint: &str) -> Result<WsConn, ClientCoreError> {
JSWebsocket::new(endpoint).map_err(|_| ClientCoreError::GatewayJsConnectionFailure)
}
async fn measure_latency<G>(gateway: &G) -> Result<GatewayWithLatency<G>, ClientCoreError>
where
G: ConnectableGateway,
{
async fn measure_latency(gateway: &gateway::Node) -> Result<GatewayWithLatency, ClientCoreError> {
let addr = gateway.clients_address();
trace!(
"establishing connection to {} ({addr})...",
gateway.identity(),
gateway.identity_key,
);
let mut stream = connect(&addr).await?;
@@ -201,7 +177,7 @@ where
let count = results.len() as u64;
if count == 0 {
return Err(ClientCoreError::NoGatewayMeasurements {
identity: gateway.identity().to_base58_string(),
identity: gateway.identity_key.to_base58_string(),
});
}
@@ -211,11 +187,11 @@ where
Ok(GatewayWithLatency::new(gateway, avg))
}
pub async fn choose_gateway_by_latency<'a, R: Rng, G: ConnectableGateway + Clone>(
pub async fn choose_gateway_by_latency<R: Rng>(
rng: &mut R,
gateways: &[G],
gateways: &[gateway::Node],
must_use_tls: bool,
) -> Result<G, ClientCoreError> {
) -> Result<gateway::Node, ClientCoreError> {
let gateways = filter_by_tls(gateways, must_use_tls)?;
info!(
@@ -247,19 +223,21 @@ pub async fn choose_gateway_by_latency<'a, R: Rng, G: ConnectableGateway + Clone
info!(
"chose gateway {} with average latency of {:?}",
chosen.gateway.identity(),
chosen.latency
chosen.gateway.identity_key, chosen.latency
);
Ok(chosen.gateway.clone())
}
fn filter_by_tls<G: ConnectableGateway>(
gateways: &[G],
fn filter_by_tls(
gateways: &[gateway::Node],
must_use_tls: bool,
) -> Result<Vec<&G>, ClientCoreError> {
) -> Result<Vec<&gateway::Node>, ClientCoreError> {
if must_use_tls {
let filtered = gateways.iter().filter(|g| g.is_wss()).collect::<Vec<_>>();
let filtered = gateways
.iter()
.filter(|g| g.clients_wss_port.is_some())
.collect::<Vec<_>>();
if filtered.is_empty() {
return Err(ClientCoreError::NoWssGateways);
-2
View File
@@ -2,9 +2,7 @@ use std::future::Future;
#[cfg(all(
not(target_arch = "wasm32"),
feature = "cli",
feature = "fs-surb-storage",
feature = "fs-credentials-storage",
feature = "fs-gateways-storage"
))]
pub mod cli_helpers;
+1 -1
View File
@@ -24,7 +24,7 @@ nym-bandwidth-controller = { path = "../../bandwidth-controller" }
nym-credentials = { path = "../../credentials" }
nym-credential-storage = { path = "../../credential-storage" }
nym-crypto = { path = "../../crypto" }
nym-gateway-requests = { path = "../../gateway-requests" }
nym-gateway-requests = { path = "../../../gateway/gateway-requests" }
nym-network-defaults = { path = "../../network-defaults" }
nym-sphinx = { path = "../../nymsphinx" }
nym-pemstore = { path = "../../pemstore" }
@@ -1,89 +0,0 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use si_scale::helpers::bibytes2;
use std::sync::atomic::{AtomicI64, Ordering};
use std::sync::Arc;
use std::time::Duration;
use time::OffsetDateTime;
#[derive(Clone, Default)]
pub struct ClientBandwidth {
inner: Arc<ClientBandwidthInner>,
}
#[derive(Default)]
struct ClientBandwidthInner {
/// the actual bandwidth amount (in bytes) available
available: AtomicI64,
/// defines the timestamp when the bandwidth information has been logged to the logs stream
last_logged_ts: AtomicI64,
/// defines the timestamp when the bandwidth value was last updated
last_updated_ts: AtomicI64,
}
impl ClientBandwidth {
pub(crate) fn new_empty() -> Self {
ClientBandwidth {
inner: Arc::new(ClientBandwidthInner {
available: AtomicI64::new(0),
last_logged_ts: AtomicI64::new(0),
last_updated_ts: AtomicI64::new(0),
}),
}
}
pub(crate) fn remaining(&self) -> i64 {
self.inner.available.load(Ordering::Acquire)
}
pub(crate) fn maybe_log_bandwidth(&self, now: Option<OffsetDateTime>) {
let last = self.last_logged();
let now = now.unwrap_or_else(OffsetDateTime::now_utc);
if last + Duration::from_secs(10) < now {
self.log_bandwidth(Some(now))
}
}
pub(crate) fn log_bandwidth(&self, now: Option<OffsetDateTime>) {
let now = now.unwrap_or_else(OffsetDateTime::now_utc);
let remaining = self.remaining();
let remaining_bi2 = bibytes2(remaining as f64);
if remaining < 0 {
log::warn!("OUT OF BANDWIDTH. remaining: {remaining_bi2}");
} else {
log::info!("remaining bandwidth: {remaining_bi2}");
}
self.inner
.last_logged_ts
.store(now.unix_timestamp(), Ordering::Relaxed)
}
pub(crate) fn update_and_maybe_log(&self, remaining: i64) {
let now = OffsetDateTime::now_utc();
self.inner.available.store(remaining, Ordering::Release);
self.inner
.last_updated_ts
.store(now.unix_timestamp(), Ordering::Relaxed);
self.maybe_log_bandwidth(Some(now))
}
pub(crate) fn update_and_log(&self, remaining: i64) {
let now = OffsetDateTime::now_utc();
self.inner.available.store(remaining, Ordering::Release);
self.inner
.last_updated_ts
.store(now.unix_timestamp(), Ordering::Relaxed);
self.log_bandwidth(Some(now))
}
fn last_logged(&self) -> OffsetDateTime {
// SAFETY: this value is always populated with valid timestamps
OffsetDateTime::from_unix_timestamp(self.inner.last_logged_ts.load(Ordering::Relaxed))
.unwrap()
}
}
@@ -1,13 +1,12 @@
// Copyright 2021-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::bandwidth::ClientBandwidth;
use crate::client::config::GatewayClientConfig;
use crate::error::GatewayClientError;
use crate::packet_router::PacketRouter;
pub use crate::packet_router::{
AcknowledgementReceiver, AcknowledgementSender, MixnetMessageReceiver, MixnetMessageSender,
};
use crate::socket_state::{ws_fd, PartiallyDelegatedHandle, SocketState};
use crate::socket_state::{ws_fd, PartiallyDelegated, SocketState};
use crate::traits::GatewayPacketRouter;
use crate::{cleanup_socket_message, try_decrypt_binary_message};
use futures::{SinkExt, StreamExt};
@@ -24,11 +23,14 @@ 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_sphinx::forwarding::packet::MixPacket;
use nym_task::TaskClient;
use nym_validator_client::nyxd::contract_traits::DkgQueryClient;
use rand::rngs::OsRng;
use std::sync::Arc;
use std::time::Duration;
use tungstenite::protocol::Message;
use url::Url;
@@ -46,7 +48,12 @@ use wasm_utils::websocket::JSWebsocket;
#[cfg(target_arch = "wasm32")]
use wasmtimer::tokio::sleep;
pub mod config;
// Set this to a high value for now, so that we don't risk sporadic timeouts that might cause
// bought bandwidth tokens to not have time to be spent; Once we remove the gateway from the
// bandwidth bridging protocol, we can come back to a smaller timeout value
const DEFAULT_GATEWAY_RESPONSE_TIMEOUT: Duration = Duration::from_secs(5 * 60);
const DEFAULT_RECONNECTION_ATTEMPTS: usize = 10;
const DEFAULT_RECONNECTION_BACKOFF: Duration = Duration::from_secs(5);
pub struct GatewayConfig {
pub gateway_identity: identity::PublicKey,
@@ -72,19 +79,29 @@ impl GatewayConfig {
}
// TODO: this should be refactored into a state machine that keeps track of its authentication state
#[derive(Debug)]
pub struct GatewayClient<C, St = EphemeralCredentialStorage> {
pub cfg: GatewayClientConfig,
authenticated: bool,
bandwidth: ClientBandwidth,
disabled_credentials_mode: bool,
bandwidth_remaining: i64,
gateway_address: String,
gateway_identity: identity::PublicKey,
local_identity: Arc<identity::KeyPair>,
shared_key: Option<Arc<SharedKeys>>,
connection: SocketState,
packet_router: PacketRouter,
response_timeout_duration: Duration,
bandwidth_controller: Option<BandwidthController<C, St>>,
// reconnection related variables
/// Specifies whether client should try to reconnect to gateway on connection failure.
should_reconnect_on_failure: bool,
/// Specifies maximum number of attempts client will try to reconnect to gateway on failure
/// before giving up.
reconnection_attempts: usize,
/// Delay between each subsequent reconnection attempt.
reconnection_backoff: Duration,
// currently unused (but populated)
negotiated_protocol: Option<u8>,
@@ -94,8 +111,7 @@ pub struct GatewayClient<C, St = EphemeralCredentialStorage> {
impl<C, St> GatewayClient<C, St> {
pub fn new(
cfg: GatewayClientConfig,
gateway_config: GatewayConfig,
config: GatewayConfig,
local_identity: Arc<identity::KeyPair>,
// TODO: make it mandatory. if you don't want to pass it, use `new_init`
shared_key: Option<Arc<SharedKeys>>,
@@ -104,21 +120,55 @@ impl<C, St> GatewayClient<C, St> {
task_client: TaskClient,
) -> Self {
GatewayClient {
cfg,
authenticated: false,
bandwidth: ClientBandwidth::new_empty(),
gateway_address: gateway_config.gateway_listener,
gateway_identity: gateway_config.gateway_identity,
disabled_credentials_mode: true,
bandwidth_remaining: 0,
gateway_address: config.gateway_listener,
gateway_identity: config.gateway_identity,
local_identity,
shared_key,
connection: SocketState::NotConnected,
packet_router,
response_timeout_duration: DEFAULT_GATEWAY_RESPONSE_TIMEOUT,
bandwidth_controller,
should_reconnect_on_failure: true,
reconnection_attempts: DEFAULT_RECONNECTION_ATTEMPTS,
reconnection_backoff: DEFAULT_RECONNECTION_BACKOFF,
negotiated_protocol: None,
task_client,
}
}
#[must_use]
pub fn with_disabled_credentials_mode(mut self, disabled_credentials_mode: bool) -> Self {
self.disabled_credentials_mode = disabled_credentials_mode;
self
}
#[must_use]
pub fn with_reconnection_on_failure(mut self, should_reconnect_on_failure: bool) -> Self {
self.should_reconnect_on_failure = should_reconnect_on_failure;
self
}
#[must_use]
pub fn with_response_timeout(mut self, response_timeout_duration: Duration) -> Self {
self.response_timeout_duration = response_timeout_duration;
self
}
#[must_use]
pub fn with_reconnection_attempts(mut self, reconnection_attempts: usize) -> Self {
self.reconnection_attempts = reconnection_attempts;
self
}
#[must_use]
pub fn with_reconnection_backoff(mut self, backoff: Duration) -> Self {
self.reconnection_backoff = backoff;
self
}
pub fn gateway_identity(&self) -> identity::PublicKey {
self.gateway_identity
}
@@ -132,7 +182,7 @@ impl<C, St> GatewayClient<C, St> {
}
pub fn remaining_bandwidth(&self) -> i64 {
self.bandwidth.remaining()
self.bandwidth_remaining
}
#[cfg(not(target_arch = "wasm32"))]
@@ -208,21 +258,18 @@ impl<C, St> GatewayClient<C, St> {
info!("Attempting gateway reconnection...");
self.authenticated = false;
for i in 1..self.cfg.connection.reconnection_attempts {
info!("reconnection attempt {}...", i);
for i in 1..self.reconnection_attempts {
info!("attempt {}...", i);
if self.try_reconnect().await.is_ok() {
info!("managed to reconnect!");
return Ok(());
}
sleep(self.cfg.connection.reconnection_backoff).await;
sleep(self.reconnection_backoff).await;
}
// final attempt (done separately to be able to return a proper error)
info!(
"reconnection attempt {}",
self.cfg.connection.reconnection_attempts
);
info!("attempt {}", self.reconnection_attempts);
match self.try_reconnect().await {
Ok(_) => {
info!("managed to reconnect!");
@@ -231,7 +278,7 @@ impl<C, St> GatewayClient<C, St> {
Err(err) => {
error!(
"failed to reconnect after {} attempts",
self.cfg.connection.reconnection_attempts
self.reconnection_attempts
);
Err(err)
}
@@ -247,7 +294,7 @@ impl<C, St> GatewayClient<C, St> {
_ => return Err(GatewayClientError::ConnectionInInvalidState),
};
let timeout = sleep(self.cfg.connection.response_timeout_duration);
let timeout = sleep(self.response_timeout_duration);
tokio::pin!(timeout);
loop {
@@ -416,7 +463,7 @@ impl<C, St> GatewayClient<C, St> {
ws_stream,
self.local_identity.as_ref(),
self.gateway_identity,
self.cfg.bandwidth.require_tickets,
!self.disabled_credentials_mode,
)
.await
.map_err(GatewayClientError::RegistrationFailure),
@@ -478,7 +525,7 @@ impl<C, St> GatewayClient<C, St> {
self_address,
encrypted_address,
iv,
self.cfg.bandwidth.require_tickets,
!self.disabled_credentials_mode,
)
.into();
@@ -490,8 +537,7 @@ impl<C, St> GatewayClient<C, St> {
} => {
self.check_gateway_protocol(protocol_version)?;
self.authenticated = status;
self.bandwidth.update_and_maybe_log(bandwidth_remaining);
self.bandwidth_remaining = bandwidth_remaining;
self.negotiated_protocol = protocol_version;
log::debug!("authenticated: {status}, bandwidth remaining: {bandwidth_remaining}");
self.task_client.send_status_msg(Box::new(
@@ -530,74 +576,56 @@ impl<C, St> GatewayClient<C, St> {
}
}
async fn claim_ecash_bandwidth(
async fn claim_coconut_bandwidth(
&mut self,
credential: CredentialSpendingData,
) -> Result<(), GatewayClientError> {
let mut rng = OsRng;
let iv = IV::new_random(&mut rng);
let msg = ClientControlRequest::new_enc_ecash_credential(
let msg = ClientControlRequest::new_enc_coconut_bandwidth_credential_v2(
credential,
self.shared_key.as_ref().unwrap(),
iv,
)
.into();
let bandwidth_remaining = match self.send_websocket_message(msg).await? {
self.bandwidth_remaining = match self.send_websocket_message(msg).await? {
ServerResponse::Bandwidth { available_total } => Ok(available_total),
ServerResponse::Error { message } => Err(GatewayClientError::GatewayError(message)),
ServerResponse::TypedError { error } => {
Err(GatewayClientError::TypedGatewayError(error))
}
_ => Err(GatewayClientError::UnexpectedResponse),
}?;
// TODO: create tracing span
info!("managed to claim ecash bandwidth");
self.bandwidth.update_and_log(bandwidth_remaining);
Ok(())
}
async fn try_claim_testnet_bandwidth(&mut self) -> Result<(), GatewayClientError> {
let msg = ClientControlRequest::ClaimFreeTestnetBandwidth.into();
let bandwidth_remaining = match self.send_websocket_message(msg).await? {
self.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),
}?;
info!("managed to claim testnet bandwidth");
self.bandwidth.update_and_log(bandwidth_remaining);
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);
}
if self.shared_key.is_none() {
return Err(GatewayClientError::NoSharedKeyAvailable);
}
if self.bandwidth_controller.is_none() && self.cfg.bandwidth.require_tickets {
if self.bandwidth_controller.is_none() && !self.disabled_credentials_mode {
return Err(GatewayClientError::NoBandwidthControllerAvailable);
}
warn!("Not enough bandwidth. Trying to get more bandwidth, this might take a while");
if !self.cfg.bandwidth.require_tickets {
if self.disabled_credentials_mode {
info!("The client is running in disabled credentials mode - attempting to claim bandwidth without a credential");
return self.try_claim_testnet_bandwidth().await;
}
@@ -613,52 +641,49 @@ impl<C, St> GatewayClient<C, St> {
negotiated_protocol: Some(gateway_protocol),
});
}
let gateway_id = self.gateway_identity().to_base58_string();
let prepared_credential = self
.unchecked_bandwidth_controller()
.prepare_ecash_ticket(self.gateway_identity.to_bytes(), TICKETS_TO_SPEND)
.bandwidth_controller
.as_ref()
.unwrap()
.prepare_bandwidth_credential(&gateway_id)
.await?;
match self.claim_ecash_bandwidth(prepared_credential.data).await {
Ok(_) => Ok(()),
Err(err) => {
error!("failed to claim ecash bandwidth with the gateway...: {err}");
if err.is_ticket_replay() {
warn!("this was due to our ticket being replayed! have you messed with the database file?")
} else {
// TODO: tracing span
info!("attempting to revert ticket withdrawal...");
self.unchecked_bandwidth_controller()
.attempt_revert_ticket_usage(prepared_credential.metadata)
.await?;
}
self.claim_coconut_bandwidth(prepared_credential.data)
.await?;
self.bandwidth_controller
.as_ref()
.unwrap()
.consume_credential(prepared_credential.credential_id, &gateway_id)
.await?;
Err(err)
}
}
Ok(())
}
fn estimate_required_bandwidth(&self, packets: &[MixPacket]) -> i64 {
packets
.iter()
.map(|packet| packet.packet().len())
.sum::<usize>() as i64
}
pub async fn batch_send_mix_packets(
&mut self,
packets: Vec<MixPacket>,
) -> Result<(), GatewayClientError>
where
C: DkgQueryClient + Send + Sync,
St: CredentialStorage,
<St as CredentialStorage>::StorageError: Send + Sync + 'static,
{
) -> Result<(), GatewayClientError> {
debug!("Sending {} mix packets", packets.len());
if !self.authenticated {
return Err(GatewayClientError::NotAuthenticated);
}
let bandwidth_remaining = self.bandwidth.remaining();
if bandwidth_remaining < self.cfg.bandwidth.remaining_bandwidth_threshold {
self.cfg
.bandwidth
.ensure_above_cutoff(bandwidth_remaining)?;
self.claim_bandwidth().await?;
if self.estimate_required_bandwidth(&packets) > self.bandwidth_remaining {
return Err(GatewayClientError::NotEnoughBandwidth(
self.estimate_required_bandwidth(&packets),
self.bandwidth_remaining,
));
}
if !self.connection.is_established() {
return Err(GatewayClientError::ConnectionNotEstablished);
}
@@ -678,7 +703,7 @@ impl<C, St> GatewayClient<C, St> {
.batch_send_websocket_messages_without_response(messages)
.await
{
if err.is_closed_connection() && self.cfg.connection.should_reconnect_on_failure {
if err.is_closed_connection() && self.should_reconnect_on_failure {
self.attempt_reconnection().await
} else {
Err(err)
@@ -693,7 +718,7 @@ impl<C, St> GatewayClient<C, St> {
msg: Message,
) -> Result<(), GatewayClientError> {
if let Err(err) = self.send_websocket_message_without_response(msg).await {
if err.is_closed_connection() && self.cfg.connection.should_reconnect_on_failure {
if err.is_closed_connection() && self.should_reconnect_on_failure {
debug!("Going to attempt a reconnection");
self.attempt_reconnection().await
} else {
@@ -717,23 +742,19 @@ impl<C, St> GatewayClient<C, St> {
}
// TODO: possibly make responses optional
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,
{
pub async fn send_mix_packet(
&mut self,
mix_packet: MixPacket,
) -> Result<(), GatewayClientError> {
if !self.authenticated {
return Err(GatewayClientError::NotAuthenticated);
}
let bandwidth_remaining = self.bandwidth.remaining();
if bandwidth_remaining < self.cfg.bandwidth.remaining_bandwidth_threshold {
self.cfg
.bandwidth
.ensure_above_cutoff(bandwidth_remaining)?;
self.claim_bandwidth().await?;
if (mix_packet.packet().len() as i64) > self.bandwidth_remaining {
return Err(GatewayClientError::NotEnoughBandwidth(
mix_packet.packet().len() as i64,
self.bandwidth_remaining,
));
}
if !self.connection.is_established() {
return Err(GatewayClientError::ConnectionNotEstablished);
}
@@ -779,7 +800,7 @@ impl<C, St> GatewayClient<C, St> {
let partially_delegated =
match std::mem::replace(&mut self.connection, SocketState::Invalid) {
SocketState::Available(conn) => {
PartiallyDelegatedHandle::split_and_listen_for_mixnet_messages(
PartiallyDelegated::split_and_listen_for_mixnet_messages(
*conn,
self.packet_router.clone(),
Arc::clone(
@@ -787,7 +808,6 @@ impl<C, St> GatewayClient<C, St> {
.as_ref()
.expect("no shared key present even though we're authenticated!"),
),
self.bandwidth.clone(),
self.task_client.clone(),
)
}
@@ -828,12 +848,10 @@ impl<C, St> GatewayClient<C, St> {
self.establish_connection().await?;
}
let shared_key = self.perform_initial_authentication().await?;
let bandwidth_remaining = self.bandwidth.remaining();
if bandwidth_remaining < self.cfg.bandwidth.remaining_bandwidth_threshold {
self.cfg
.bandwidth
.ensure_above_cutoff(bandwidth_remaining)?;
info!("Claiming more bandwidth with existing credentials. Stop the process now if you don't want that to happen.");
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);
self.claim_bandwidth().await?;
}
@@ -868,16 +886,20 @@ impl GatewayClient<InitOnly, EphemeralCredentialStorage> {
let packet_router = PacketRouter::new(ack_tx, mix_tx, task_client.clone());
GatewayClient {
cfg: GatewayClientConfig::default().with_disabled_credentials_mode(true),
authenticated: false,
bandwidth: ClientBandwidth::new_empty(),
disabled_credentials_mode: true,
bandwidth_remaining: 0,
gateway_address: gateway_listener.to_string(),
gateway_identity,
local_identity,
shared_key: None,
connection: SocketState::NotConnected,
packet_router,
response_timeout_duration: DEFAULT_GATEWAY_RESPONSE_TIMEOUT,
bandwidth_controller: None,
should_reconnect_on_failure: false,
reconnection_attempts: DEFAULT_RECONNECTION_ATTEMPTS,
reconnection_backoff: DEFAULT_RECONNECTION_BACKOFF,
negotiated_protocol: None,
task_client,
}
@@ -896,16 +918,20 @@ impl GatewayClient<InitOnly, EphemeralCredentialStorage> {
assert!(self.shared_key.is_some());
GatewayClient {
cfg: self.cfg,
authenticated: self.authenticated,
bandwidth: self.bandwidth,
disabled_credentials_mode: self.disabled_credentials_mode,
bandwidth_remaining: self.bandwidth_remaining,
gateway_address: self.gateway_address,
gateway_identity: self.gateway_identity,
local_identity: self.local_identity,
shared_key: self.shared_key,
connection: self.connection,
packet_router,
response_timeout_duration: self.response_timeout_duration,
bandwidth_controller,
should_reconnect_on_failure: self.should_reconnect_on_failure,
reconnection_attempts: self.reconnection_attempts,
reconnection_backoff: self.reconnection_backoff,
negotiated_protocol: self.negotiated_protocol,
task_client,
}
@@ -1,135 +0,0 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::error::GatewayClientError;
use nym_network_defaults::TicketTypeRepr::V1MixnetEntry;
use si_scale::helpers::bibytes2;
use std::time::Duration;
#[derive(Debug, Default, Clone, Copy)]
pub struct GatewayClientConfig {
pub connection: Connection,
pub bandwidth: BandwidthTickets,
}
impl GatewayClientConfig {
pub fn new_default() -> Self {
Default::default()
}
#[must_use]
pub fn with_disabled_credentials_mode(mut self, disabled_credentials_mode: bool) -> Self {
self.bandwidth.require_tickets = !disabled_credentials_mode;
self
}
#[must_use]
pub fn with_reconnection_on_failure(mut self, should_reconnect_on_failure: bool) -> Self {
self.connection.should_reconnect_on_failure = should_reconnect_on_failure;
self
}
#[must_use]
pub fn with_response_timeout(mut self, response_timeout_duration: Duration) -> Self {
self.connection.response_timeout_duration = response_timeout_duration;
self
}
#[must_use]
pub fn with_reconnection_attempts(mut self, reconnection_attempts: usize) -> Self {
self.connection.reconnection_attempts = reconnection_attempts;
self
}
#[must_use]
pub fn with_reconnection_backoff(mut self, backoff: Duration) -> Self {
self.connection.reconnection_backoff = backoff;
self
}
}
#[derive(Debug, Clone, Copy)]
pub struct Connection {
/// Specifies the timeout for gateway responses
pub response_timeout_duration: Duration,
/// Specifies whether client should try to reconnect to gateway on connection failure.
pub should_reconnect_on_failure: bool,
/// Specifies maximum number of attempts client will try to reconnect to gateway on failure
/// before giving up.
pub reconnection_attempts: usize,
/// Delay between each subsequent reconnection attempt.
pub reconnection_backoff: Duration,
}
impl Connection {
// Set this to a high value for now, so that we don't risk sporadic timeouts that might cause
// bought bandwidth tokens to not have time to be spent; Once we remove the gateway from the
// bandwidth bridging protocol, we can come back to a smaller timeout value
pub const DEFAULT_RESPONSE_TIMEOUT: Duration = Duration::from_secs(5 * 60);
pub const DEFAULT_RECONNECTION_ATTEMPTS: usize = 10;
pub const DEFAULT_RECONNECTION_BACKOFF: Duration = Duration::from_secs(5);
}
impl Default for Connection {
fn default() -> Self {
Connection {
response_timeout_duration: Self::DEFAULT_RESPONSE_TIMEOUT,
should_reconnect_on_failure: true,
reconnection_attempts: Self::DEFAULT_RECONNECTION_ATTEMPTS,
reconnection_backoff: Self::DEFAULT_RECONNECTION_BACKOFF,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct BandwidthTickets {
/// specifies whether this client will be sending bandwidth tickets or will attempt to use 'free' testnet bandwidth instead
pub require_tickets: bool,
/// specifies threshold (in bytes) under which the client should send another ticket to the gateway
pub remaining_bandwidth_threshold: i64,
/// specifies threshold (in bytes) under which the client will NOT send any tickets because it got accused of double spending and got its bandwidth revoked
/// if not specified, the client will always send tickets
pub cutoff_remaining_bandwidth_threshold: Option<i64>,
}
impl BandwidthTickets {
// TO BE CHANGED \/
pub const DEFAULT_REQUIRES_TICKETS: bool = false;
// 20% of entry ticket value
pub const DEFAULT_REMAINING_BANDWIDTH_THRESHOLD: i64 =
(V1MixnetEntry.bandwidth_value() / 5) as i64;
pub const DEFAULT_CUTOFF_REMAINING_BANDWIDTH_THRESHOLD: Option<i64> = None;
pub fn ensure_above_cutoff(&self, available: i64) -> Result<(), GatewayClientError> {
if let Some(cutoff) = self.cutoff_remaining_bandwidth_threshold {
if available < cutoff {
let available_bi2 = bibytes2(available as f64);
let cutoff_bi2 = bibytes2(cutoff as f64);
return Err(GatewayClientError::BandwidthBelowCutoffValue {
available_bi2,
cutoff_bi2,
});
}
}
Ok(())
}
}
impl Default for BandwidthTickets {
fn default() -> Self {
BandwidthTickets {
require_tickets: Self::DEFAULT_REQUIRES_TICKETS,
remaining_bandwidth_threshold: Self::DEFAULT_REMAINING_BANDWIDTH_THRESHOLD,
cutoff_remaining_bandwidth_threshold:
Self::DEFAULT_CUTOFF_REMAINING_BANDWIDTH_THRESHOLD,
}
}
}
+3 -21
View File
@@ -1,26 +1,21 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
#[cfg(target_arch = "wasm32")]
use gloo_utils::errors::JsError;
use nym_gateway_requests::registration::handshake::error::HandshakeError;
use nym_gateway_requests::SimpleGatewayRequestsError;
use std::io;
use thiserror::Error;
use tungstenite::Error as WsError;
#[cfg(target_arch = "wasm32")]
use gloo_utils::errors::JsError;
#[derive(Debug, Error)]
pub enum GatewayClientError {
#[error("Connection to the gateway is not established")]
ConnectionNotEstablished,
#[error("gateway returned an error response: {0}")]
#[error("Gateway returned an error response: {0}")]
GatewayError(String),
#[error("gateway returned an error response: {0}")]
TypedGatewayError(SimpleGatewayRequestsError),
#[error("There was a network error: {0}")]
NetworkError(#[from] WsError),
@@ -67,12 +62,6 @@ pub enum GatewayClientError {
#[error("There are no more bandwidth credentials acquired. Please buy some more if you want to use the mixnet")]
NoMoreBandwidthCredentials,
#[error("the current available bandwidth ({available_bi2}) is below the minimum cutoff threshold off {cutoff_bi2}")]
BandwidthBelowCutoffValue {
available_bi2: String,
cutoff_bi2: String,
},
#[error("Received an unexpected response")]
UnexpectedResponse,
@@ -124,11 +113,4 @@ impl GatewayClientError {
_ => false,
}
}
pub fn is_ticket_replay(&self) -> bool {
match self {
GatewayClientError::TypedGatewayError(err) => err.is_ticket_replay(),
_ => false,
}
}
}
+5 -3
View File
@@ -6,7 +6,7 @@ use log::warn;
use nym_gateway_requests::BinaryResponse;
use tungstenite::{protocol::Message, Error as WsError};
pub use client::{config::GatewayClientConfig, GatewayClient, GatewayConfig};
pub use client::{GatewayClient, GatewayConfig};
pub use nym_gateway_requests::registration::handshake::SharedKeys;
pub use packet_router::{
AcknowledgementReceiver, AcknowledgementSender, MixnetMessageReceiver, MixnetMessageSender,
@@ -14,7 +14,6 @@ pub use packet_router::{
};
pub use traits::GatewayPacketRouter;
mod bandwidth;
pub mod client;
pub mod error;
pub mod packet_router;
@@ -52,7 +51,10 @@ pub(crate) fn try_decrypt_binary_message(
BinaryResponse::PushedMixMessage(plaintext) => Some(plaintext),
},
Err(err) => {
warn!("message received from the gateway was malformed! - {err}",);
warn!(
"message received from the gateway was malformed! - {:?}",
err
);
None
}
}
@@ -70,8 +70,8 @@ impl PacketRouter {
Ok(())
}
pub fn disarm(&mut self) {
self.shutdown.disarm();
pub fn mark_as_success(&mut self) {
self.shutdown.mark_as_success();
}
}
@@ -1,7 +1,6 @@
// Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::bandwidth::ClientBandwidth;
use crate::error::GatewayClientError;
use crate::packet_router::PacketRouter;
use crate::traits::GatewayPacketRouter;
@@ -11,13 +10,16 @@ use futures::stream::{SplitSink, SplitStream};
use futures::{SinkExt, StreamExt};
use log::*;
use nym_gateway_requests::registration::handshake::SharedKeys;
use nym_gateway_requests::{ServerResponse, SimpleGatewayRequestsError};
use nym_gateway_requests::ServerResponse;
use nym_task::TaskClient;
use std::os::raw::c_int as RawFd;
use std::sync::Arc;
use tungstenite::{protocol::Message, Error as WsError};
use si_scale::helpers::bibytes2;
use std::os::raw::c_int as RawFd;
use std::sync::atomic::{AtomicI64, Ordering};
use std::sync::Arc;
use std::time::Duration;
use time::OffsetDateTime;
use tungstenite::Message;
#[cfg(unix)]
use std::os::fd::AsRawFd;
#[cfg(not(target_arch = "wasm32"))]
@@ -40,7 +42,6 @@ type WsConn = JSWebsocket;
// by some other task, however, we can notify it to get the stream back.
type SplitStreamReceiver = oneshot::Receiver<Result<SplitStream<WsConn>, GatewayClientError>>;
type SplitStreamSender = oneshot::Sender<Result<SplitStream<WsConn>, GatewayClientError>>;
pub(crate) fn ws_fd(_conn: &WsConn) -> Option<RawFd> {
#[cfg(unix)]
@@ -52,204 +53,92 @@ pub(crate) fn ws_fd(_conn: &WsConn) -> Option<RawFd> {
None
}
// disgusting? absolutely, but does the trick for now
static LAST_LOGGED_BANDWIDTH_TS: AtomicI64 = AtomicI64::new(0);
fn maybe_log_bandwidth(remaining: i64) {
// SAFETY: this value is always populated with valid timestamps
let last =
OffsetDateTime::from_unix_timestamp(LAST_LOGGED_BANDWIDTH_TS.load(Ordering::Relaxed))
.unwrap();
let now = OffsetDateTime::now_utc();
if last + Duration::from_secs(10) < now {
log::info!("remaining bandwidth: {}", bibytes2(remaining as f64));
LAST_LOGGED_BANDWIDTH_TS.store(now.unix_timestamp(), Ordering::Relaxed)
}
}
#[derive(Debug)]
pub(crate) struct PartiallyDelegatedHandle {
pub(crate) struct PartiallyDelegated {
sink_half: SplitSink<WsConn, Message>,
// this could have been simplified by a notify as opposed to oneshot, but let's not change what ain't broke
delegated_stream: (SplitStreamReceiver, oneshot::Sender<()>),
ws_fd: Option<RawFd>,
}
struct PartiallyDelegatedRouter {
packet_router: PacketRouter,
shared_key: Arc<SharedKeys>,
client_bandwidth: ClientBandwidth,
stream_return: SplitStreamSender,
stream_return_requester: oneshot::Receiver<()>,
}
impl PartiallyDelegatedRouter {
fn new(
packet_router: PacketRouter,
shared_key: Arc<SharedKeys>,
client_bandwidth: ClientBandwidth,
stream_return: SplitStreamSender,
stream_return_requester: oneshot::Receiver<()>,
) -> PartiallyDelegatedRouter {
PartiallyDelegatedRouter {
packet_router,
shared_key,
client_bandwidth,
stream_return,
stream_return_requester,
}
}
async fn run(mut self, mut split_stream: SplitStream<WsConn>, mut task_client: TaskClient) {
let mut chunked_stream = (&mut split_stream).ready_chunks(8);
let ret: Result<_, GatewayClientError> = loop {
tokio::select! {
biased;
// received system-wide shutdown
_ = task_client.recv() => {
log::trace!("GatewayClient listener: Received shutdown");
log::debug!("GatewayClient listener: Exiting");
return;
}
// received request to stop the task and return the stream
_ = &mut self.stream_return_requester => {
log::debug!("received request to return the split ws stream");
break Ok(())
}
socket_msgs = chunked_stream.next() => {
if let Err(err) = self.handle_socket_messages(socket_msgs) {
break Err(err)
impl PartiallyDelegated {
fn recover_received_plaintexts(
ws_msgs: Vec<Message>,
shared_key: &SharedKeys,
) -> Result<Vec<Vec<u8>>, GatewayClientError> {
let mut plaintexts = Vec::with_capacity(ws_msgs.len());
for ws_msg in ws_msgs {
match ws_msg {
Message::Binary(bin_msg) => {
// this function decrypts the request and checks the MAC
if let Some(plaintext) = try_decrypt_binary_message(bin_msg, shared_key) {
plaintexts.push(plaintext)
}
}
}
};
// I think that in the future we should perhaps have some sequence number system, i.e.
// so each request/response pair can be easily identified, so that if messages are
// not ordered (for some peculiar reason) we wouldn't lose anything.
// This would also require NOT discarding any text responses here.
let return_res = match ret {
Err(err) => self.stream_return.send(Err(err)),
Ok(_) => {
self.packet_router.disarm();
task_client.disarm();
self.stream_return.send(Ok(split_stream))
}
};
if return_res.is_err() {
warn!("failed to return the split stream back on the oneshot channel")
}
}
fn handle_socket_messages(
&self,
msgs: Option<Vec<Result<Message, WsError>>>,
) -> Result<(), GatewayClientError> {
let ws_msgs = cleanup_socket_messages(msgs)?;
let plaintexts = self.recover_received_plaintexts(ws_msgs)?;
if !plaintexts.is_empty() {
self.packet_router.route_received(plaintexts)?
}
Ok(())
}
fn handle_binary_message(&self, binary_msg: Vec<u8>) -> Result<Vec<u8>, GatewayClientError> {
// this function decrypts the request and checks the MAC
match try_decrypt_binary_message(binary_msg, &self.shared_key) {
Some(plaintext) => Ok(plaintext),
None => {
error!("failed to decrypt and verify received message!");
Err(GatewayClientError::MalformedResponse)
}
}
}
// only returns an error on **critical** failures
fn handle_text_message(&self, text: String) -> Result<(), GatewayClientError> {
// if we fail to deserialise the response, return a hard error. we can't handle garbage
match ServerResponse::try_from(text).map_err(|_| GatewayClientError::MalformedResponse)? {
ServerResponse::Send {
remaining_bandwidth,
} => {
self.client_bandwidth
.update_and_maybe_log(remaining_bandwidth);
Ok(())
}
ServerResponse::Error { message } => {
error!("[1] gateway failure: {message}");
Err(GatewayClientError::GatewayError(message))
}
ServerResponse::TypedError { error } => {
match error {
SimpleGatewayRequestsError::OutOfBandwidth {
required,
available,
} => {
let available_bi2 = bibytes2(available as f64);
let required_bi2 = bibytes2(required as f64);
warn!("run out of bandwidth when attempting to send the message! we got {available_bi2} available, but needed at least {required_bi2} to send the previous message");
self.client_bandwidth.update_and_log(available);
// UNIMPLEMENTED: we should stop sending messages until we recover bandwidth
Ok(())
}
_ => {
error!("[2] gateway failure: {error}");
Err(GatewayClientError::TypedGatewayError(error))
}
}
}
other => {
let name = other.name();
warn!("received illegal message of type '{name}' in an authenticated client");
Ok(())
}
}
}
fn recover_received_plaintext(
&self,
message: Message,
) -> Result<Option<Vec<u8>>, GatewayClientError> {
match message {
Message::Binary(bin_msg) => {
let plaintext = self.handle_binary_message(bin_msg)?;
Ok(Some(plaintext))
}
// I think that in the future we should perhaps have some sequence number system, i.e.
// so each request/response pair can be easily identified, so that if messages are
// not ordered (for some peculiar reason) we wouldn't lose anything.
// This would also require NOT discarding any text responses here.
// TODO: those can return the "send confirmations" - perhaps it should be somehow worked around?
Message::Text(text) => {
trace!(
// TODO: those can return the "send confirmations" - perhaps it should be somehow worked around?
Message::Text(text) => {
trace!(
"received a text message - probably a response to some previous query! - {text}",
);
self.handle_text_message(text)?;
Ok(None)
}
_ => {
debug!("received websocket message that's neither 'Binary' nor 'Text'. it's going to get ignored");
Ok(None)
}
}
}
match ServerResponse::try_from(text)
.map_err(|_| GatewayClientError::MalformedResponse)?
{
ServerResponse::Send {
remaining_bandwidth,
} => maybe_log_bandwidth(remaining_bandwidth),
ServerResponse::Error { message } => {
error!("gateway failure: {message}");
return Err(GatewayClientError::GatewayError(message));
}
other => {
warn!(
"received illegal message of type {} in an authenticated client",
other.name()
)
}
}
fn recover_received_plaintexts(
&self,
messages: Vec<Message>,
) -> Result<Vec<Vec<u8>>, GatewayClientError> {
let mut plaintexts = Vec::new();
for ws_msg in messages {
if let Some(plaintext) = self.recover_received_plaintext(ws_msg)? {
plaintexts.push(plaintext)
continue;
}
_ => continue,
}
}
Ok(plaintexts)
}
fn spawn(self, split_stream: SplitStream<WsConn>, task_client: TaskClient) {
let fut = async move { self.run(split_stream, task_client).await };
#[cfg(target_arch = "wasm32")]
wasm_bindgen_futures::spawn_local(fut);
#[cfg(not(target_arch = "wasm32"))]
tokio::spawn(fut);
fn route_socket_messages(
ws_msgs: Vec<Message>,
packet_router: &PacketRouter,
shared_key: &SharedKeys,
) -> Result<(), GatewayClientError> {
let plaintexts = Self::recover_received_plaintexts(ws_msgs, shared_key)?;
packet_router.route_received(plaintexts)
}
}
impl PartiallyDelegatedHandle {
pub(crate) fn split_and_listen_for_mixnet_messages(
conn: WsConn,
packet_router: PacketRouter,
mut packet_router: PacketRouter,
shared_key: Arc<SharedKeys>,
client_bandwidth: ClientBandwidth,
shutdown: TaskClient,
mut shutdown: TaskClient,
) -> Self {
// when called for, it NEEDS TO yield back the stream so that we could merge it and
// read control request responses.
@@ -257,18 +146,58 @@ impl PartiallyDelegatedHandle {
let (stream_sender, stream_receiver) = oneshot::channel();
let ws_fd = ws_fd(&conn);
let (sink, stream) = conn.split();
PartiallyDelegatedRouter::new(
packet_router,
shared_key,
client_bandwidth,
stream_sender,
notify_receiver,
)
.spawn(stream, shutdown);
let (sink, mut stream) = conn.split();
PartiallyDelegatedHandle {
let mixnet_receiver_future = async move {
let mut notify_receiver = notify_receiver;
let mut chunk_stream = (&mut stream).ready_chunks(8);
let ret_err = loop {
tokio::select! {
_ = shutdown.recv() => {
log::trace!("GatewayClient listener: Received shutdown");
log::debug!("GatewayClient listener: Exiting");
return;
}
_ = &mut notify_receiver => {
break Ok(());
}
msgs = chunk_stream.next() => {
let ws_msgs = match cleanup_socket_messages(msgs) {
Err(err) => break Err(err),
Ok(msgs) => msgs
};
if let Err(err) = Self::route_socket_messages(ws_msgs, &packet_router, shared_key.as_ref()) {
log::error!("Route socket messages failed: {err}");
break Err(err)
}
}
};
};
if match ret_err {
Err(err) => stream_sender.send(Err(err)),
Ok(_) => {
packet_router.mark_as_success();
shutdown.mark_as_success();
stream_sender.send(Ok(stream))
}
}
.is_err()
{
warn!("failed to send back `mixnet_receiver_future` result on the oneshot channel")
}
};
#[cfg(target_arch = "wasm32")]
wasm_bindgen_futures::spawn_local(mixnet_receiver_future);
#[cfg(not(target_arch = "wasm32"))]
tokio::spawn(mixnet_receiver_future);
PartiallyDelegated {
ws_fd,
sink_half: sink,
delegated_stream: (stream_receiver, notify_sender),
@@ -337,7 +266,7 @@ impl PartiallyDelegatedHandle {
#[derive(Debug)]
pub(crate) enum SocketState {
Available(Box<WsConn>),
PartiallyDelegated(PartiallyDelegatedHandle),
PartiallyDelegated(PartiallyDelegated),
NotConnected,
Invalid,
}
@@ -17,7 +17,6 @@ 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"] }
@@ -27,10 +26,9 @@ 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-compact-ecash = { path = "../../nym_offline_compact_ecash" }
nym-coconut = { path = "../../nymcoconut" }
nym-network-defaults = { path = "../../network-defaults" }
nym-api-requests = { path = "../../../nym-api/nym-api-requests" }
@@ -90,6 +88,4 @@ default = ["http-client"]
http-client = ["cosmrs/rpc"]
generate-ts = []
contract-testing = ["nym-mixnet-contract-common/contract-testing"]
# Features below are added to make clippy happy, it seems like they're unused we should remove them
tendermint-rpc-http-client = ["tendermint-rpc/http-client"]
tendermint-rpc-websocket-client = ["tendermint-rpc/websocket-client"]
@@ -8,14 +8,10 @@ use crate::{
nym_api, DirectSigningReqwestRpcValidatorClient, QueryReqwestRpcValidatorClient,
ReqwestRpcClient, ValidatorClientError,
};
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::coconut::models::FreePassNonceResponse;
use nym_api_requests::coconut::{
BlindSignRequestBody, BlindedSignatureResponse, FreePassRequest, VerifyCredentialBody,
VerifyCredentialResponse,
};
use nym_api_requests::models::{DescribedGateway, MixNodeBondAnnotated};
use nym_api_requests::models::{
@@ -23,10 +19,8 @@ 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;
@@ -35,7 +29,7 @@ pub use nym_mixnet_contract_common::{
};
// re-export the type to not break existing imports
pub use crate::coconut::EcashApiClient;
pub use crate::coconut::CoconutApiClient;
#[cfg(feature = "http-client")]
use crate::rpc::http_client;
@@ -381,73 +375,24 @@ impl NymApiClient {
Ok(self.nym_api.blind_sign(request_body).await?)
}
pub async fn verify_ecash_ticket(
pub async fn verify_bandwidth_credential(
&self,
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> {
request_body: &VerifyCredentialBody,
) -> Result<VerifyCredentialResponse, ValidatorClientError> {
Ok(self
.nym_api
.batch_redeem_ecash_tickets(request_body)
.verify_bandwidth_credential(request_body)
.await?)
}
pub async fn spent_credentials_filter(
&self,
) -> Result<SpentCredentialsResponse, ValidatorClientError> {
Ok(self.nym_api.double_spending_filter_v1().await?)
pub async fn free_pass_nonce(&self) -> Result<FreePassNonceResponse, ValidatorClientError> {
Ok(self.nym_api.free_pass_nonce().await?)
}
pub async fn partial_expiration_date_signatures(
pub async fn issue_free_pass_credential(
&self,
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?)
request: &FreePassRequest,
) -> Result<BlindedSignatureResponse, ValidatorClientError> {
Ok(self.nym_api.free_pass(request).await?)
}
}
@@ -4,40 +4,26 @@
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 EcashApiClient {
pub struct CoconutApiClient {
pub api_client: NymApiClient,
pub verification_key: VerificationKeyAuth,
pub verification_key: VerificationKey,
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 EcashApiError {
pub enum CoconutApiError {
// TODO: ask @BN whether this is a correct error message
#[error("the provided key share hasn't been verified")]
UnverifiedShare,
@@ -57,7 +43,7 @@ pub enum EcashApiError {
#[error("the provided verification key is malformed: {source}")]
MalformedVerificationKey {
#[from]
source: CompactEcashError,
source: CoconutError,
},
#[error("the provided account address is malformed: {source}")]
@@ -67,29 +53,29 @@ pub enum EcashApiError {
},
}
impl TryFrom<ContractVKShare> for EcashApiClient {
type Error = EcashApiError;
impl TryFrom<ContractVKShare> for CoconutApiClient {
type Error = CoconutApiError;
fn try_from(share: ContractVKShare) -> Result<Self, Self::Error> {
if !share.verified {
return Err(EcashApiError::UnverifiedShare);
return Err(CoconutApiError::UnverifiedShare);
}
let url_address = Url::parse(&share.announce_address)?;
Ok(EcashApiClient {
Ok(CoconutApiClient {
api_client: NymApiClient::new(url_address),
verification_key: VerificationKeyAuth::try_from_bs58(&share.share)?,
verification_key: VerificationKey::try_from_bs58(&share.share)?,
node_id: share.node_index,
cosmos_address: share.owner.as_str().parse()?,
})
}
}
pub async fn all_ecash_api_clients<C>(
pub async fn all_coconut_api_clients<C>(
client: &C,
epoch_id: EpochId,
) -> Result<Vec<EcashApiClient>, EcashApiError>
) -> Result<Vec<CoconutApiClient>, CoconutApiError>
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, Config, EcashApiClient};
pub use client::{Client, CoconutApiClient, Config};
pub use nym_api_requests::*;
pub use nym_http_api_client::UserAgent;
@@ -2,33 +2,16 @@
// SPDX-License-Identifier: Apache-2.0
use crate::nym_api::error::NymAPIError;
use crate::nym_api::routes::{ecash, CORE_STATUS_COUNT, SINCE_ARG};
use crate::nym_api::routes::{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::{
ecash::{
coconut::{
models::{
EpochCredentialsResponse, IssuedCredentialResponse, IssuedCredentialsResponse,
IssuedTicketbook, IssuedTicketbookBody, SpentCredentialsResponse,
EpochCredentialsResponse, IssuedCredential, IssuedCredentialBody,
IssuedCredentialResponse, IssuedCredentialsResponse,
},
BlindSignRequestBody, BlindedSignatureResponse, CredentialsRequestBody,
PartialCoinIndicesSignatureResponse, PartialExpirationDateSignatureResponse,
VerifyEcashCredentialBody,
VerifyCredentialBody, VerifyCredentialResponse,
},
models::{
ComputeRewardEstParam, DescribedGateway, GatewayBondAnnotated, GatewayCoreStatusResponse,
@@ -39,11 +22,17 @@ pub use nym_api_requests::{
},
};
pub use nym_coconut_dkg_common::types::EpochId;
pub use nym_http_api_client::Client;
use nym_http_api_client::{ApiClient, NO_PARAMS};
use nym_mixnet_contract_common::mixnode::MixNodeDetails;
use nym_mixnet_contract_common::{GatewayBond, IdentityKeyRef, MixId};
pub fn rfc_3339_date() -> Vec<BorrowedFormatItem<'static>> {
time::format_description::parse("[year]-[month]-[day]").unwrap()
}
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;
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
@@ -431,6 +420,36 @@ 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,
@@ -438,8 +457,9 @@ pub trait NymApiClientExt: ApiClient {
self.post_json(
&[
routes::API_VERSION,
routes::ECASH_ROUTES,
routes::ECASH_BLIND_SIGN,
routes::COCONUT_ROUTES,
routes::BANDWIDTH,
routes::COCONUT_BLIND_SIGN,
],
NO_PARAMS,
request_body,
@@ -447,15 +467,16 @@ pub trait NymApiClientExt: ApiClient {
.await
}
async fn verify_ecash_ticket(
async fn verify_bandwidth_credential(
&self,
request_body: &VerifyEcashTicketBody,
) -> Result<EcashTicketVerificationResponse, NymAPIError> {
request_body: &VerifyCredentialBody,
) -> Result<VerifyCredentialResponse, NymAPIError> {
self.post_json(
&[
routes::API_VERSION,
routes::ECASH_ROUTES,
routes::VERIFY_ECASH_TICKET,
routes::COCONUT_ROUTES,
routes::BANDWIDTH,
routes::COCONUT_VERIFY_BANDWIDTH_CREDENTIAL,
],
NO_PARAMS,
request_body,
@@ -463,139 +484,6 @@ 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,
@@ -603,8 +491,9 @@ pub trait NymApiClientExt: ApiClient {
self.get_json(
&[
routes::API_VERSION,
routes::ECASH_ROUTES,
routes::ECASH_EPOCH_CREDENTIALS,
routes::COCONUT_ROUTES,
routes::BANDWIDTH,
routes::COCONUT_EPOCH_CREDENTIALS,
&dkg_epoch.to_string(),
],
NO_PARAMS,
@@ -619,8 +508,9 @@ pub trait NymApiClientExt: ApiClient {
self.get_json(
&[
routes::API_VERSION,
routes::ECASH_ROUTES,
routes::ECASH_ISSUED_CREDENTIAL,
routes::COCONUT_ROUTES,
routes::BANDWIDTH,
routes::COCONUT_ISSUED_CREDENTIAL,
&credential_id.to_string(),
],
NO_PARAMS,
@@ -635,8 +525,9 @@ pub trait NymApiClientExt: ApiClient {
self.post_json(
&[
routes::API_VERSION,
routes::ECASH_ROUTES,
routes::ECASH_ISSUED_CREDENTIALS,
routes::COCONUT_ROUTES,
routes::BANDWIDTH,
routes::COCONUT_ISSUED_CREDENTIALS,
],
NO_PARAMS,
&CredentialsRequestBody {
@@ -12,27 +12,16 @@ pub const DETAILED: &str = "detailed";
pub const DETAILED_UNFILTERED: &str = "detailed-unfiltered";
pub const ACTIVE: &str = "active";
pub const REWARDED: &str = "rewarded";
pub const DOUBLE_SPENDING_FILTER_V1: &str = "double-spending-filter-v1";
pub const COCONUT_ROUTES: &str = "coconut";
pub const BANDWIDTH: &str = "bandwidth";
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 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 STATUS_ROUTES: &str = "status";
pub const MIXNODE: &str = "mixnode";
@@ -49,7 +38,5 @@ pub const COMPUTE_REWARD_ESTIMATION: &str = "compute-reward-estimation";
pub const AVG_UPTIME: &str = "avg_uptime";
pub const STAKE_SATURATION: &str = "stake-saturation";
pub const INCLUSION_CHANCE: &str = "inclusion-probability";
pub const SUBMIT_GATEWAY: &str = "submit-gateway-monitoring-results";
pub const SUBMIT_NODE: &str = "submit-node-monitoring-results";
pub const SERVICE_PROVIDERS: &str = "services";
@@ -0,0 +1,100 @@
// 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(),
};
}
}
@@ -0,0 +1,153 @@
// 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,11 +51,6 @@ 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,
@@ -261,9 +256,6 @@ 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,
@@ -1,123 +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 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(),
};
}
}
@@ -1,149 +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_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,
deposit_amount: Coin,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
let req = EcashExecuteMsg::DepositTicketBookFunds {
identity_key: public_key,
};
self.execute_ecash_contract(fee, req, "Ecash::Deposit".to_string(), vec![deposit_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 update_admin(
&self,
admin: String,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
let req = EcashExecuteMsg::UpdateAdmin { admin };
self.execute_ecash_contract(fee, req, "Ecash::UpdateAdmin".to_string(), vec![])
.await
}
async fn update_deposit_value(
&self,
new_deposit: Coin,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
let req = EcashExecuteMsg::UpdateDepositValue {
new_deposit: new_deposit.into(),
};
self.execute_ecash_contract(fee, req, "Ecash::UpdateDepositValue".to_string(), 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::{mock_coin, 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(), mock_coin(), 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
ExecuteMsg::UpdateAdmin { admin } => client.update_admin(admin, None).ignore(),
ExecuteMsg::UpdateDepositValue { new_deposit } => client
.update_deposit_value(new_deposit.into(), None)
.ignore(),
};
}
}
@@ -8,32 +8,34 @@ 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;
@@ -46,7 +48,7 @@ pub trait NymContractsProvider {
fn vesting_contract_address(&self) -> Option<&AccountId>;
// coconut-related
fn ecash_contract_address(&self) -> Option<&AccountId>;
fn coconut_bandwidth_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>;
@@ -57,7 +59,7 @@ pub struct TypedNymContracts {
pub mixnet_contract_address: Option<AccountId>,
pub vesting_contract_address: Option<AccountId>,
pub ecash_contract_address: Option<AccountId>,
pub coconut_bandwidth_contract_address: Option<AccountId>,
pub group_contract_address: Option<AccountId>,
pub multisig_contract_address: Option<AccountId>,
pub coconut_dkg_contract_address: Option<AccountId>,
@@ -76,8 +78,8 @@ impl TryFrom<NymContracts> for TypedNymContracts {
.vesting_contract_address
.map(|addr| addr.parse())
.transpose()?,
ecash_contract_address: value
.ecash_contract_address
coconut_bandwidth_contract_address: value
.coconut_bandwidth_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, VoteInfo, VoteListResponse, VoteResponse, VoterDetail,
ProposalListResponse, ProposalResponse, VoteListResponse, VoteResponse, VoterDetail,
VoterListResponse, VoterResponse,
};
use cw_utils::ThresholdResponse;
@@ -134,28 +134,6 @@ 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 ecash_contract_address = self
.ecash_contract_address()
let coconut_bandwidth_contract_address = self
.coconut_bandwidth_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: ecash_contract_address.to_string(),
contract_addr: coconut_bandwidth_contract_address.to_string(),
msg: to_binary(&release_funds_req)?,
funds: vec![],
});
@@ -2,33 +2,31 @@
// SPDX-License-Identifier: Apache-2.0
use crate::nyxd::cosmwasm_client::client_traits::CosmWasmClient;
use crate::nyxd::cosmwasm_client::helpers::{
compress_wasm_code, parse_msg_responses, CheckResponse,
};
use crate::nyxd::cosmwasm_client::helpers::{compress_wasm_code, 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;
@@ -54,6 +52,20 @@ 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)]
@@ -120,7 +132,8 @@ where
.await?
.check_response()?;
let logs = parse_raw_logs(&tx_res.tx_result.log)?;
let logs = parse_raw_logs(tx_res.tx_result.log)?;
let events = tx_res.tx_result.events;
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(),
@@ -130,8 +143,9 @@ 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_tx_attribute(&tx_res, "store_code", "code_id")
let code_id = find_attribute(&events, "store_code", "code_id")
.unwrap()
.value
.parse()
.unwrap();
@@ -142,7 +156,7 @@ where
compressed_checksum,
code_id,
logs,
events: tx_res.tx_result.events,
events,
transaction_hash: tx_res.hash,
gas_info,
})
@@ -184,7 +198,8 @@ where
.await?
.check_response()?;
let logs = parse_raw_logs(&tx_res.tx_result.log)?;
let logs = parse_raw_logs(tx_res.tx_result.log)?;
let events = tx_res.tx_result.events;
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(),
@@ -193,15 +208,16 @@ 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_tx_attribute(&tx_res, "instantiate", "_contract_address")
let contract_address = find_attribute(&events, "instantiate", "_contract_address")
.unwrap()
.value
.parse()
.unwrap();
Ok(InstantiateResult {
contract_address,
logs,
events: tx_res.tx_result.events,
events,
transaction_hash: tx_res.hash,
gas_info,
})
@@ -215,7 +231,7 @@ where
fee: Fee,
memo: impl Into<String> + Send + 'static,
) -> Result<ChangeAdminResult, NyxdError> {
let change_admin_msg = MsgUpdateAdmin {
let change_admin_msg = sealed::cosmwasm::MsgUpdateAdmin {
sender: sender_address.clone(),
new_admin: new_admin.clone(),
contract: contract_address.clone(),
@@ -247,7 +263,7 @@ where
fee: Fee,
memo: impl Into<String> + Send + 'static,
) -> Result<ChangeAdminResult, NyxdError> {
let change_admin_msg = MsgClearAdmin {
let change_admin_msg = sealed::cosmwasm::MsgClearAdmin {
sender: sender_address.clone(),
contract: contract_address.clone(),
}
@@ -339,11 +355,10 @@ 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,
})
@@ -386,8 +401,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,
})
@@ -716,3 +731,167 @@ 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,87 +2,9 @@
// 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,11 +3,10 @@
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
@@ -34,25 +33,6 @@ 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
@@ -69,7 +49,7 @@ fn parse_raw_str_logs(raw: &str) -> Result<Vec<Log>, NyxdError> {
Ok(logs)
}
pub fn parse_raw_logs<S: AsRef<str>>(raw: S) -> Result<Vec<Log>, NyxdError> {
pub fn parse_raw_logs(raw: String) -> Result<Vec<Log>, NyxdError> {
parse_raw_str_logs(raw.as_ref())
}
@@ -23,8 +23,6 @@ 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")]
@@ -300,8 +298,8 @@ where
}
#[cfg(any(
feature = "tendermint-rpc-http-client",
feature = "tendermint-rpc-websocket-client"
feature = "tendermint-rpc/http-client",
feature = "tendermint-rpc/websocket-client"
))]
async fn wait_until_healthy<T>(&self, timeout: T) -> Result<(), Error>
where
@@ -30,7 +30,6 @@ use prost::Message;
use serde::Serialize;
pub use cosmrs::abci::GasInfo;
pub use cosmrs::abci::MsgResponse;
pub type ContractCodeId = u64;
@@ -241,7 +240,7 @@ pub struct UploadResult {
pub gas_info: GasInfo,
}
#[derive(Debug, Default)]
#[derive(Debug)]
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
@@ -263,11 +262,6 @@ impl InstantiateOptions {
admin,
}
}
pub fn with_admin(mut self, admin: AccountId) -> Self {
self.admin = Some(admin);
self
}
}
#[derive(Debug, Serialize)]
@@ -313,7 +307,7 @@ pub struct MigrateResult {
pub struct ExecuteResult {
pub logs: Vec<Log>,
pub msg_responses: Vec<MsgResponse>,
pub data: Vec<u8>,
pub events: Vec<abci::Event>,
@@ -32,12 +32,6 @@ 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,
@@ -148,12 +142,6 @@ 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,16 +3,11 @@
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| {
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()
let attribute = event
.attributes
.iter()
.find(|attr| attr.key == attribute_key)?;
Some(attribute.value.clone())
}
@@ -245,8 +245,8 @@ impl<C, S> NyxdClient<C, S> {
self.config.contracts.vesting_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_coconut_bandwidth_contract_address(&mut self, address: AccountId) {
self.config.contracts.coconut_bandwidth_contract_address = Some(address);
}
pub fn set_multisig_contract_address(&mut self, address: AccountId) {
@@ -267,8 +267,11 @@ impl<C, S> NymContractsProvider for NyxdClient<C, S> {
self.config.contracts.vesting_contract_address.as_ref()
}
fn ecash_contract_address(&self) -> Option<&AccountId> {
self.config.contracts.ecash_contract_address.as_ref()
fn coconut_bandwidth_contract_address(&self) -> Option<&AccountId> {
self.config
.contracts
.coconut_bandwidth_contract_address
.as_ref()
}
fn dkg_contract_address(&self) -> Option<&AccountId> {
@@ -381,14 +384,6 @@ 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
@@ -820,8 +815,8 @@ where
}
#[cfg(any(
feature = "tendermint-rpc-http-client",
feature = "tendermint-rpc-websocket-client"
feature = "tendermint-rpc/http-client",
feature = "tendermint-rpc/websocket-client"
))]
async fn wait_until_healthy<T>(&self, timeout: T) -> Result<(), Error>
where
@@ -300,8 +300,8 @@ pub trait TendermintRpcClient {
}
#[cfg(any(
feature = "tendermint-rpc-http-client",
feature = "tendermint-rpc-websocket-client"
feature = "tendermint-rpc/http-client",
feature = "tendermint-rpc/websocket-client"
))]
/// Poll the `/health` endpoint until it returns a successful result or
/// the given `timeout` has elapsed.
@@ -518,8 +518,8 @@ mod non_wasm {
}
#[cfg(any(
feature = "tendermint-rpc-http-client",
feature = "tendermint-rpc-websocket-client"
feature = "tendermint-rpc/http-client",
feature = "tendermint-rpc/websocket-client"
))]
async fn wait_until_healthy<T>(&self, timeout: T) -> Result<(), Error>
where
+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" }
@@ -0,0 +1,194 @@
// 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,22 +7,29 @@ use anyhow::bail;
use clap::Parser;
use nym_credential_storage::initialise_persistent_storage;
use nym_credential_utils::utils;
use nym_credentials_interface::TicketType;
use nym_crypto::asymmetric::identity;
use nym_validator_client::nyxd::Coin;
use std::path::PathBuf;
#[derive(Debug, Parser)]
pub struct Args {
/// Specify which type of ticketbook should be issued
#[clap(long, default_value_t = TicketType::default())]
pub(crate) ticketbook_type: TicketType,
/// 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() {
@@ -33,24 +40,16 @@ 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;
let private_id_key: identity::PrivateKey = nym_pemstore::load_key(private_id_key)?;
utils::issue_credential(
&client,
&persistent_storage,
&private_id_key.to_bytes(),
args.ticketbook_type,
)
.await?;
utils::issue_credential(&client, coin, &persistent_storage, args.recovery_dir).await?;
Ok(())
}
+11 -9
View File
@@ -3,20 +3,22 @@
use clap::{Args, Subcommand};
pub mod import_ticket_book;
pub mod issue_ticket_book;
pub mod recover_ticket_book;
pub mod generate_freepass;
pub mod import_credential;
pub mod issue_credentials;
pub mod recover_credentials;
#[derive(Debug, Args)]
#[clap(args_conflicts_with_subcommands = true, subcommand_required = true)]
pub struct Ecash {
pub struct Coconut {
#[clap(subcommand)]
pub command: EcashCommands,
pub command: CoconutCommands,
}
#[derive(Debug, Subcommand)]
pub enum EcashCommands {
IssueTicketBook(issue_ticket_book::Args),
RecoverTicketBook(recover_ticket_book::Args),
ImportTicketBook(import_ticket_book::Args),
pub enum CoconutCommands {
GenerateFreepass(generate_freepass::Args),
IssueCredentials(issue_credentials::Args),
RecoverCredentials(recover_credentials::Args),
ImportCredential(import_credential::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::utils;
use nym_credential_utils::{recovery_storage, utils};
use std::path::PathBuf;
#[derive(Debug, Parser)]
@@ -14,6 +14,10 @@ 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<()> {
@@ -33,9 +37,12 @@ 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_deposits(&client, &persistent_storage).await?;
let recovered =
utils::recover_credentials(&client, &recovery_storage, &persistent_storage).await?;
println!("recovered {recovered} ticketbooks");
// TODO: denom?
println!("recovered {recovered} worth of credentials");
Ok(())
}
-28
View File
@@ -123,21 +123,6 @@ 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) => {
@@ -240,17 +225,4 @@ 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")
}
}
}
@@ -4,25 +4,21 @@
use std::str::FromStr;
use clap::Parser;
use cosmwasm_std::Coin;
use log::{debug, info};
use nym_ecash_contract_common::msg::InstantiateMsg;
use nym_coconut_bandwidth_contract_common::msg::InstantiateMsg;
use nym_validator_client::nyxd::AccountId;
#[derive(Debug, Parser)]
pub struct Args {
#[clap(long)]
pub group_addr: Option<AccountId>,
pub pool_addr: String,
#[clap(long)]
pub multisig_addr: Option<AccountId>,
#[clap(long)]
pub holding_account: AccountId,
#[clap(long, default_value = "75000000unym")]
pub deposit_amount: Coin,
pub mix_denom: Option<String>,
}
pub async fn generate(args: Args) {
@@ -30,25 +26,21 @@ 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)
let multisig_addr = args.multisig_addr.unwrap_or_else(|| {
let address = std::env::var(nym_network_defaults::var_names::REWARDING_VALIDATOR_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::MULTISIG_CONTRACT_ADDRESS)
.expect("Multisig address has to be set");
AccountId::from_str(address.as_str())
.expect("Failed converting multisig address to AccountId")
let mix_denom = args.mix_denom.unwrap_or_else(|| {
std::env::var(nym_network_defaults::var_names::MIX_DENOM).expect("Mix denom has to be set")
});
let instantiate_msg = InstantiateMsg {
holding_account: args.holding_account.to_string(),
group_addr: group_addr.to_string(),
pool_addr: args.pool_addr,
multisig_addr: multisig_addr.to_string(),
deposit_amount: args.deposit_amount,
mix_denom,
};
debug!("instantiate_msg: {:?}", instantiate_msg);
@@ -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 {
EcashBandwidth(ecash_bandwidth::Args),
CoconutBandwidth(coconut_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 ecash_contract_address: Option<AccountId>,
pub coconut_bandwidth_contract_address: Option<AccountId>,
#[clap(long)]
pub coconut_dkg_contract_address: Option<AccountId>,
@@ -33,12 +33,14 @@ pub async fn generate(args: Args) {
debug!("Received arguments: {:?}", args);
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_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 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)
@@ -56,7 +58,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: ecash_contract_address.to_string(),
coconut_bandwidth_contract_address: coconut_bandwidth_contract_address.to_string(),
coconut_dkg_contract_address: coconut_dkg_contract_address.to_string(),
};
@@ -4,6 +4,9 @@
// 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";
// attributes that are used in multiple places
pub const DEPOSIT_VALUE: &str = "deposit-value";
pub const DEPOSIT_INFO: &str = "deposit-info";
@@ -87,9 +87,6 @@ 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,7 +2,6 @@
// 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.
@@ -32,23 +31,6 @@ 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,
@@ -1,21 +0,0 @@
[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"]
@@ -1,71 +0,0 @@
// 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 }
}
}
@@ -1,11 +0,0 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_schema::cw_serde;
use cosmwasm_std::Coin;
#[cw_serde]
pub struct PoolCounters {
pub total_deposited: Coin,
pub total_redeemed: Coin,
}
@@ -1,76 +0,0 @@
// 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>,
}
@@ -1,68 +0,0 @@
// 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: Coin, amount: Coin },
#[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 ticket book size 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!")]
TicketBookSizeChanged { at_init: u64, current: u64 },
#[error("the provided tickets redemption commitment is malformed")]
MalformedRedemptionCommitment,
#[error("the account blacklisting hasn't been fully implemented yet")]
UnimplementedBlacklisting,
}
@@ -1,4 +0,0 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub const BANDWIDTH_PROPOSAL_ID: &str = "proposal_id";
@@ -1,9 +0,0 @@
// 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";
pub const DEPOSIT_ID: &str = "deposit-id";
pub const WASM_EVENT_NAME: &str = "wasm";
pub const PROPOSAL_ID_ATTRIBUTE_NAME: &str = "proposal_id";
@@ -1,13 +0,0 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod blacklist;
pub mod counters;
pub mod deposit;
pub mod error;
pub mod event_attributes;
pub mod events;
pub mod msg;
pub mod redeem_credential;
pub use error::EcashContractError;
@@ -1,84 +0,0 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_schema::cw_serde;
use cosmwasm_std::Coin;
#[cfg(feature = "schema")]
use crate::blacklist::{BlacklistedAccountResponse, PagedBlacklistedAccountResponse};
#[cfg(feature = "schema")]
use crate::deposit::{DepositResponse, PagedDepositsResponse};
#[cfg(feature = "schema")]
use cosmwasm_schema::QueryResponses;
#[cw_serde]
pub struct InstantiateMsg {
pub holding_account: String,
pub multisig_addr: String,
pub group_addr: String,
pub deposit_amount: Coin,
}
#[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,
},
UpdateAdmin {
admin: String,
},
UpdateDepositValue {
new_deposit: Coin,
},
// TODO: properly implement
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>,
},
}
#[cw_serde]
pub struct MigrateMsg {}
@@ -1,5 +0,0 @@
// 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,10 +47,4 @@ 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,
}
+3 -12
View File
@@ -8,31 +8,22 @@ license.workspace = true
[dependencies]
async-trait = { workspace = true }
bincode = { workspace = true, optional = true }
log = { workspace = true }
thiserror = { workspace = true }
serde = { workspace = true, features = ["derive"], optional = true }
tokio = { workspace = true, features = ["sync"] }
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", "time"]
features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"]
[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"]
@@ -1,69 +0,0 @@
/*
* 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 type of the associated ticketbook
ticketbook_type TEXT 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)
);
+88 -201
View File
@@ -1,34 +1,23 @@
// Copyright 2023-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
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 crate::models::{CredentialUsage, StoredIssuedCredential};
use std::sync::Arc;
use tokio::sync::RwLock;
use zeroize::Zeroizing;
#[derive(Clone)]
pub struct MemoryEcachTicketbookManager {
inner: Arc<RwLock<EcashCredentialManagerInner>>,
pub struct CoconutCredentialManager {
inner: Arc<RwLock<CoconutCredentialManagerInner>>,
}
#[derive(Default)]
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>>,
struct CoconutCredentialManagerInner {
credentials: Vec<StoredIssuedCredential>,
credential_usage: Vec<CredentialUsage>,
_next_id: i64,
}
impl EcashCredentialManagerInner {
impl CoconutCredentialManagerInner {
fn next_id(&mut self) -> i64 {
let next = self._next_id;
self._next_id += 1;
@@ -36,210 +25,108 @@ impl EcashCredentialManagerInner {
}
}
// 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 {
impl CoconutCredentialManager {
/// Creates new empty instance of the `CoconutCredentialManager`.
pub fn new() -> Self {
MemoryEcachTicketbookManager {
CoconutCredentialManager {
inner: Default::default(),
}
}
pub(crate) async fn cleanup_expired(&self) {
let mut guard = self.inner.write().await;
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);
}
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,
})
}
pub async fn get_next_unspent_ticketbook_and_update(
&self,
tickets: u32,
) -> Option<RetrievedTicketbook> {
let mut guard = self.inner.write().await;
async fn bandwidth_voucher_spent(&self, id: i64) -> bool {
self.inner
.read()
.await
.credential_usage
.iter()
.any(|c| c.credential_id == id)
}
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),
});
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(crate) async fn revert_ticketbook_withdrawal(
pub async fn get_next_unspect_freepass(
&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
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
}
pub(crate) async fn insert_pending_ticketbook(&self, ticketbook: &IssuanceTicketBook) {
/// Consumes in the database the specified credential.
///
/// # Arguments
///
/// * `id`: Database id.
pub async fn consume_coconut_credential(&self, id: i64, gateway_id: &str) {
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(),
},
);
guard.credential_usage.push(CredentialUsage {
credential_id: id,
gateway_id_bs58: gateway_id.to_string(),
});
}
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(),
})
/// 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;
}
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(),
ticketbook_type: t.ticketbook.ticketbook_type().to_string(),
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(all(not(target_arch = "wasm32"), feature = "persistent-storage"))]
#[cfg(not(target_arch = "wasm32"))]
pub mod sqlite;
+84 -250
View File
@@ -1,282 +1,116 @@
// Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::models::{
BasicTicketbookInformation, RawExpirationDateSignatures, StoredIssuedTicketbook,
StoredPendingTicketbook,
};
use nym_ecash_time::Date;
use sqlx::{Executor, Sqlite, Transaction};
use crate::models::StoredIssuedCredential;
#[derive(Clone)]
pub struct SqliteEcashTicketbookManager {
pub struct CoconutCredentialManager {
connection_pool: sqlx::SqlitePool,
}
impl SqliteEcashTicketbookManager {
/// Creates new instance of the `EcashTicketbookManager` with the provided sqlite connection pool.
impl CoconutCredentialManager {
/// Creates new instance of the `CoconutCredentialManager` with the provided sqlite connection pool.
///
/// # Arguments
///
/// * `connection_pool`: database connection pool to use.
pub fn new(connection_pool: sqlx::SqlitePool) -> Self {
SqliteEcashTicketbookManager { connection_pool }
CoconutCredentialManager { connection_pool }
}
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(
pub async fn insert_issued_credential(
&self,
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(())
}
#[allow(clippy::too_many_arguments)]
pub(crate) async fn insert_new_ticketbook(
&self,
serialisation_revision: u8,
data: &[u8],
expiration_date: Date,
typ: &str,
credential_type: String,
serialization_revision: u8,
credential_data: &[u8],
epoch_id: u32,
total_tickets: u32,
used_tickets: u32,
) -> Result<(), sqlx::Error> {
sqlx::query!(
r#"
INSERT INTO ecash_ticketbook
(serialization_revision, ticketbook_data, expiration_date, ticketbook_type, epoch_id, total_tickets, used_tickets)
VALUES (?, ?, ?, ?, ?, ?, ?)
INSERT INTO coconut_credentials(serialization_revision, credential_type, credential_data, epoch_id, expired)
VALUES (?, ?, ?, ?, false)
"#,
serialisation_revision,
data,
expiration_date,
typ,
epoch_id,
total_tickets,
used_tickets,
serialization_revision, credential_type, credential_data, epoch_id
).execute(&self.connection_pool).await?;
Ok(())
}
pub(crate) async fn get_ticketbooks_info(
pub async fn get_next_unspect_freepass(
&self,
) -> Result<Vec<BasicTicketbookInformation>, sqlx::Error> {
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
sqlx::query_as(
r#"
SELECT id, expiration_date, ticketbook_type, epoch_id, total_tickets, used_tickets
FROM ecash_ticketbook
"#,
)
.fetch_all(&self.connection_pool)
.await
}
pub(crate) async fn decrease_used_ticketbook_tickets(
&self,
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#"
UPDATE ecash_ticketbook
SET used_tickets = used_tickets - ?
WHERE id = ?
AND used_tickets = ?
"#,
reverted_spent,
ticketbook_id,
expected_current_total_spent
)
.execute(&self.connection_pool)
.await?
.rows_affected();
Ok(affected > 0)
}
pub(crate) async fn get_pending_ticketbooks(
&self,
) -> 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!(
"DELETE FROM pending_issuance WHERE deposit_id = ?",
pending_id
)
.execute(&self.connection_pool)
.await?;
Ok(())
}
pub(crate) async fn get_master_verification_key(
&self,
epoch_id: i64,
) -> Result<Option<Vec<u8>>, sqlx::Error> {
sqlx::query!(
"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
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
"#,
)
.bind(tickets)
.bind(deadline)
.fetch_optional(executor)
.await
}
)
.bind(gateway_id)
.fetch_optional(&self.connection_pool)
.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(())
pub async fn get_next_unspect_bandwidth_voucher(
&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(
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
"#,
)
.fetch_optional(&self.connection_pool)
.await
}
/// 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(
&self,
id: i64,
gateway_id: &str,
) -> Result<(), sqlx::Error> {
sqlx::query!(
"INSERT INTO credential_usage (credential_id, gateway_id_bs58) VALUES (?, ?)",
id,
gateway_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> {
sqlx::query!(
"UPDATE coconut_credentials SET expired = TRUE WHERE id = ?",
id
)
.execute(&self.connection_pool)
.await?;
Ok(())
}
}
@@ -1,30 +1,26 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::backends::memory::MemoryEcachTicketbookManager;
use std::fmt::{self, Debug, Formatter};
use crate::backends::memory::CoconutCredentialManager;
use crate::error::StorageError;
use crate::models::{BasicTicketbookInformation, RetrievedPendingTicketbook, RetrievedTicketbook};
use crate::models::{StorableIssuedCredential, StoredIssuedCredential};
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 {
storage_manager: MemoryEcachTicketbookManager,
coconut_credential_manager: CoconutCredentialManager,
}
impl Default for EphemeralStorage {
fn default() -> Self {
EphemeralStorage {
storage_manager: MemoryEcachTicketbookManager::new(),
coconut_credential_manager: CoconutCredentialManager::new(),
}
}
}
@@ -39,135 +35,55 @@ impl Debug for EphemeralStorage {
impl Storage for EphemeralStorage {
type StorageError = StorageError;
async fn cleanup_expired(&self) -> Result<(), Self::StorageError> {
self.storage_manager.cleanup_expired().await;
Ok(())
}
async fn insert_pending_ticketbook(
async fn insert_issued_credential<'a>(
&self,
ticketbook: &IssuanceTicketBook,
) -> Result<(), Self::StorageError> {
self.storage_manager
.insert_pending_ticketbook(ticketbook)
.await;
Ok(())
}
async fn insert_issued_ticketbook(
&self,
ticketbook: &IssuedTicketBook,
bandwidth_credential: StorableIssuedCredential<'a>,
) -> 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)
self.coconut_credential_manager
.insert_issued_credential(
bandwidth_credential.credential_type,
bandwidth_credential.serialization_revision,
bandwidth_credential.credential_data,
bandwidth_credential.epoch_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(
async fn get_next_unspent_credential(
&self,
tickets: u32,
) -> Result<Option<RetrievedTicketbook>, Self::StorageError> {
Ok(self
.storage_manager
.get_next_unspent_ticketbook_and_update(tickets)
.await)
}
async fn attempt_revert_ticketbook_withdrawal(
&self,
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)
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 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 mark_expired(&self, id: i64) -> Result<(), Self::StorageError> {
self.coconut_credential_manager.mark_expired(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,9 +9,6 @@ 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),
@@ -22,17 +19,6 @@ 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(),
}
}
}

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