Compare commits

...

35 Commits

Author SHA1 Message Date
benedetta davico 1a8d586eca v1.77 Update ci-contracts-upload-binaries.yml 2024-05-23 10:47:19 +02:00
benedetta davico 21d84b40bb Add ecash to contracts upload CI 2024-05-15 11:26:00 +02:00
Simon Wicky d72f7bf882 cargo fmt and lock and small test fix 2024-05-07 15:55:35 +02:00
Simon Wicky d4d55d11f0 fix tests api tests and add constants 2024-05-07 13:51:28 +02:00
Simon Wicky 1feeba26b5 adapt nym validator rewarder and sdk 2024-05-07 13:51:28 +02:00
Simon Wicky c76c8e5b4c credential import 2024-05-07 13:51:28 +02:00
Simon Wicky 0d69bfbef7 client ask for more bandwidth if it runs out 2024-05-07 13:51:27 +02:00
Simon Wicky 0d133187e0 bandwidth expiration for gateways 2024-05-07 13:51:27 +02:00
Simon Wicky 382056bce2 accept credentials on gateway 2024-05-07 13:51:27 +02:00
Simon Wicky 9441057e31 ecash verifier to replace coconut verifier 2024-05-07 13:51:27 +02:00
Simon Wicky 3a3cba06b1 credential storage on gateway 2024-05-07 13:51:27 +02:00
Simon Wicky facc1338aa API routes in the client lib 2024-05-07 13:51:27 +02:00
Simon Wicky 1df5ae9c2e API route for spending online and offline ecash 2024-05-07 13:51:27 +02:00
Simon Wicky d57ff735df spent credential storage on validators 2024-05-07 13:51:26 +02:00
Simon Wicky 24358176fd bloom filter for API 2024-05-07 13:51:26 +02:00
Simon Wicky 4cc1b65e0a credential storage for spending on client 2024-05-07 13:51:26 +02:00
Simon Wicky 3c7b7c7a8f credential preperation for the client 2024-05-07 13:51:26 +02:00
Simon Wicky 82d427aa60 credential spending models for client 2024-05-07 13:51:26 +02:00
Simon Wicky aa60001fbd credential spending models 2024-05-07 13:51:26 +02:00
Simon Wicky 587161f658 allow offline verification flag 2024-05-07 13:51:26 +02:00
Simon Wicky 24d75e10cf freepass issuance API side 2024-05-07 13:51:25 +02:00
Simon Wicky 7247e11b9e freepass issuance client side 2024-05-07 13:51:25 +02:00
Simon Wicky 0c1eb4a2ce utils for credential issuance 2024-05-07 13:51:25 +02:00
Simon Wicky 0e8742579c credential and signature storage client side 2024-05-07 13:51:25 +02:00
Simon Wicky e968a271ce add issuance logic client-side 2024-05-07 13:51:25 +02:00
Simon Wicky db3bd66cf1 modify issued_credential table 2024-05-07 13:51:25 +02:00
Simon Wicky fb340c4028 change API routes for new blind signing 2024-05-07 13:51:25 +02:00
Simon Wicky 7ed11617d2 add signatures cache on API 2024-05-07 13:51:25 +02:00
Simon Wicky 5f656e69b5 adapt issued credential storage on API 2024-05-07 13:51:24 +02:00
Simon Wicky 452a5b45fe adapt api model for credential issuance 2024-05-07 13:51:24 +02:00
Simon Wicky 3e635473a6 change types from coconut to ecash types 2024-05-07 13:51:24 +02:00
Simon Wicky 2194b05310 change contract traits from coconut to ecash 2024-05-07 13:50:18 +02:00
Simon Wicky 5195a71251 add ecash smart contract 2024-05-07 09:42:52 +02:00
Simon Wicky b1114536ea minor changes in coconut benchmarks 2024-05-07 09:38:55 +02:00
Simon Wicky 98be79d309 add offline ecash library 2024-05-07 09:38:06 +02:00
163 changed files with 14172 additions and 2414 deletions
@@ -35,7 +35,7 @@ jobs:
- name: Install Rust stable
uses: actions-rs/toolchain@v1
with:
toolchain: stable
toolchain: 1.77
target: wasm32-unknown-unknown
override: true
@@ -60,6 +60,7 @@ jobs:
cp contracts/target/wasm32-unknown-unknown/release/cw4_group.wasm $OUTPUT_DIR
cp contracts/target/wasm32-unknown-unknown/release/nym_service_provider_directory.wasm $OUTPUT_DIR
cp contracts/target/wasm32-unknown-unknown/release/nym_name_service.wasm $OUTPUT_DIR
cp contracts/target/wasm32-unknown-unknown/release/nym_ecash.wasm $OUTPUT_DIR
- name: Deploy branch to CI www
continue-on-error: true
Generated
+149 -15
View File
@@ -684,6 +684,12 @@ dependencies = [
"zeroize",
]
[[package]]
name = "bit-vec"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
[[package]]
name = "bitcoin_hashes"
version = "0.11.0"
@@ -792,6 +798,17 @@ dependencies = [
"generic-array 0.14.7",
]
[[package]]
name = "bloomfilter"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b64d54e47a7f4fd723f082e8f11429f3df6ba8adaeca355a76556f9f0602bbcf"
dependencies = [
"bit-vec",
"getrandom 0.2.10",
"siphasher",
]
[[package]]
name = "bls12_381"
version = "0.8.0"
@@ -1017,9 +1034,9 @@ dependencies = [
[[package]]
name = "chrono"
version = "0.4.31"
version = "0.4.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38"
checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
dependencies = [
"android-tzdata",
"iana-time-zone",
@@ -1027,7 +1044,7 @@ dependencies = [
"num-traits",
"serde",
"wasm-bindgen",
"windows-targets 0.48.5",
"windows-targets 0.52.4",
]
[[package]]
@@ -1086,6 +1103,17 @@ dependencies = [
"zeroize",
]
[[package]]
name = "clap"
version = "2.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c"
dependencies = [
"bitflags 1.3.2",
"textwrap 0.11.0",
"unicode-width",
]
[[package]]
name = "clap"
version = "3.2.25"
@@ -1095,7 +1123,7 @@ dependencies = [
"bitflags 1.3.2",
"clap_lex 0.2.4",
"indexmap 1.9.3",
"textwrap",
"textwrap 0.16.0",
]
[[package]]
@@ -1504,6 +1532,32 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "criterion"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b01d6de93b2b6c65e17c634a26653a29d107b3c98c607c765bf38d041531cd8f"
dependencies = [
"atty",
"cast",
"clap 2.34.0",
"criterion-plot 0.4.5",
"csv",
"itertools 0.10.5",
"lazy_static",
"num-traits",
"oorandom",
"plotters",
"rayon",
"regex",
"serde",
"serde_cbor",
"serde_derive",
"serde_json",
"tinytemplate",
"walkdir",
]
[[package]]
name = "criterion"
version = "0.4.0"
@@ -1515,7 +1569,7 @@ dependencies = [
"cast",
"ciborium",
"clap 3.2.25",
"criterion-plot",
"criterion-plot 0.5.0",
"itertools 0.10.5",
"lazy_static",
"num-traits",
@@ -1530,6 +1584,16 @@ dependencies = [
"walkdir",
]
[[package]]
name = "criterion-plot"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2673cc8207403546f45f5fd319a974b1e6983ad1a3ee7e6041650013be041876"
dependencies = [
"cast",
"itertools 0.10.5",
]
[[package]]
name = "criterion-plot"
version = "0.5.0"
@@ -3907,9 +3971,9 @@ dependencies = [
[[package]]
name = "itertools"
version = "0.12.0"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25db6b064527c5d482d0423354fcd07a89a2dfe07b67892e62411946db7f07b0"
checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
dependencies = [
"either",
]
@@ -5029,6 +5093,7 @@ dependencies = [
"anyhow",
"async-trait",
"bip39",
"bloomfilter",
"bs58 0.5.0",
"cfg-if",
"clap 4.4.7",
@@ -5042,21 +5107,23 @@ dependencies = [
"futures",
"getset",
"humantime-serde",
"itertools 0.12.0",
"itertools 0.12.1",
"k256",
"log",
"nym-api-requests",
"nym-bandwidth-controller",
"nym-bin-common",
"nym-coconut",
"nym-coconut-bandwidth-contract-common",
"nym-coconut-dkg-common",
"nym-compact-ecash",
"nym-config",
"nym-contracts-common",
"nym-credential-storage",
"nym-credentials",
"nym-credentials-interface",
"nym-crypto",
"nym-dkg",
"nym-ecash-contract-common",
"nym-gateway-client",
"nym-inclusion-probability",
"nym-mixnet-contract-common",
@@ -5106,6 +5173,7 @@ dependencies = [
"cosmwasm-std",
"ecdsa 0.16.8",
"getset",
"nym-compact-ecash",
"nym-credentials-interface",
"nym-crypto",
"nym-mixnet-contract-common",
@@ -5138,6 +5206,7 @@ dependencies = [
"nym-credentials",
"nym-credentials-interface",
"nym-crypto",
"nym-ecash-contract-common",
"nym-network-defaults",
"nym-validator-client",
"rand 0.7.3",
@@ -5233,7 +5302,6 @@ dependencies = [
"nym-bandwidth-controller",
"nym-bin-common",
"nym-client-core",
"nym-coconut-bandwidth-contract-common",
"nym-coconut-dkg-common",
"nym-config",
"nym-contracts-common",
@@ -5242,6 +5310,7 @@ dependencies = [
"nym-credentials",
"nym-credentials-interface",
"nym-crypto",
"nym-ecash-contract-common",
"nym-id",
"nym-mixnet-contract-common",
"nym-multisig-contract-common",
@@ -5446,7 +5515,7 @@ version = "0.5.0"
dependencies = [
"bls12_381",
"bs58 0.5.0",
"criterion",
"criterion 0.4.0",
"digest 0.9.0",
"doc-comment",
"ff 0.13.0",
@@ -5487,6 +5556,28 @@ dependencies = [
"nym-multisig-contract-common",
]
[[package]]
name = "nym-compact-ecash"
version = "0.1.0"
dependencies = [
"bls12_381",
"bs58 0.5.0",
"chrono",
"criterion 0.3.6",
"digest 0.9.0",
"ff 0.13.0",
"getset",
"group 0.13.0",
"itertools 0.12.1",
"nym-pemstore",
"rand 0.8.5",
"rayon",
"serde",
"sha2 0.9.9",
"thiserror",
"zeroize",
]
[[package]]
name = "nym-config"
version = "0.1.0"
@@ -5541,6 +5632,7 @@ dependencies = [
"nym-bandwidth-controller",
"nym-client-core",
"nym-coconut",
"nym-compact-ecash",
"nym-config",
"nym-credential-storage",
"nym-credentials",
@@ -5555,11 +5647,13 @@ version = "0.1.0"
dependencies = [
"bincode",
"bls12_381",
"chrono",
"cosmrs 0.15.0 (git+https://github.com/jstuczyn/cosmos-rust?branch=nym-temp/all-validator-features)",
"log",
"nym-api-requests",
"nym-credentials-interface",
"nym-crypto",
"nym-ecash-contract-common",
"nym-validator-client",
"rand 0.7.3",
"serde",
@@ -5574,6 +5668,7 @@ version = "0.1.0"
dependencies = [
"bls12_381",
"nym-coconut",
"nym-compact-ecash",
"serde",
"thiserror",
]
@@ -5611,7 +5706,7 @@ dependencies = [
"bitvec",
"bls12_381",
"bs58 0.5.0",
"criterion",
"criterion 0.4.0",
"ff 0.13.0",
"group 0.13.0",
"lazy_static",
@@ -5627,6 +5722,16 @@ dependencies = [
"zeroize",
]
[[package]]
name = "nym-ecash-contract-common"
version = "0.1.0"
dependencies = [
"cosmwasm-schema",
"cosmwasm-std",
"cw2",
"nym-multisig-contract-common",
]
[[package]]
name = "nym-ephemera-common"
version = "0.1.0"
@@ -5689,7 +5794,9 @@ dependencies = [
"anyhow",
"async-trait",
"bip39",
"bloomfilter",
"bs58 0.5.0",
"chrono",
"clap 4.4.7",
"colored",
"dashmap",
@@ -5779,6 +5886,7 @@ dependencies = [
"futures",
"generic-array 0.14.7",
"log",
"nym-compact-ecash",
"nym-credentials",
"nym-credentials-interface",
"nym-crypto",
@@ -6341,7 +6449,7 @@ dependencies = [
"blake3",
"chacha20 0.9.1",
"chacha20poly1305 0.10.1",
"criterion",
"criterion 0.4.0",
"curve25519-dalek 3.2.0",
"fastrand 1.9.0",
"getrandom 0.2.10",
@@ -6832,11 +6940,12 @@ dependencies = [
"itertools 0.10.5",
"log",
"nym-api-requests",
"nym-coconut",
"nym-coconut-bandwidth-contract-common",
"nym-coconut-dkg-common",
"nym-compact-ecash",
"nym-config",
"nym-contracts-common",
"nym-ecash-contract-common",
"nym-ephemera-common",
"nym-group-contract-common",
"nym-http-api-client",
@@ -6872,9 +6981,9 @@ dependencies = [
"humantime 2.1.0",
"humantime-serde",
"nym-bin-common",
"nym-coconut",
"nym-coconut-bandwidth-contract-common",
"nym-coconut-dkg-common",
"nym-compact-ecash",
"nym-config",
"nym-credentials",
"nym-crypto",
@@ -9014,6 +9123,16 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_cbor"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5"
dependencies = [
"half",
"serde",
]
[[package]]
name = "serde_derive"
version = "1.0.197"
@@ -9274,6 +9393,12 @@ dependencies = [
"rand_core 0.6.4",
]
[[package]]
name = "siphasher"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
[[package]]
name = "slab"
version = "0.4.9"
@@ -9928,6 +10053,15 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "textwrap"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
dependencies = [
"unicode-width",
]
[[package]]
name = "textwrap"
version = "0.16.0"
+2
View File
@@ -33,6 +33,7 @@ members = [
"common/commands",
"common/config",
"common/cosmwasm-smart-contracts/coconut-bandwidth-contract",
"common/cosmwasm-smart-contracts/ecash-contract",
"common/cosmwasm-smart-contracts/coconut-dkg",
"common/cosmwasm-smart-contracts/contracts-common",
# "common/cosmwasm-smart-contracts/ephemera",
@@ -61,6 +62,7 @@ members = [
"common/node-tester-utils",
"common/nonexhaustive-delayqueue",
"common/nymcoconut",
"common/nym_offline_compact_ecash",
"common/nym-id",
"common/nym-metrics",
"common/nymsphinx",
+1
View File
@@ -21,6 +21,7 @@ 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"
+45 -14
View File
@@ -5,21 +5,24 @@ use crate::error::BandwidthControllerError;
use nym_credential_storage::models::StorableIssuedCredential;
use nym_credential_storage::storage::Storage;
use nym_credentials::coconut::bandwidth::{CredentialType, IssuanceBandwidthCredential};
use nym_credentials::coconut::utils::obtain_aggregate_signature;
use nym_credentials::coconut::utils::{
obtain_aggregate_signature, obtain_coin_indices_signatures, obtain_expiration_date_signatures,
signatures_to_string,
};
use nym_credentials::obtain_aggregate_verification_key;
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::coconut::all_ecash_api_clients;
use nym_validator_client::nyxd::contract_traits::DkgQueryClient;
use nym_validator_client::nyxd::Coin;
use nym_validator_client::nyxd::contract_traits::EcashSigningClient;
use rand::rngs::OsRng;
use state::State;
use zeroize::Zeroizing;
pub mod state;
pub async fn deposit<C>(client: &C, amount: Coin) -> Result<State, BandwidthControllerError>
pub async fn deposit<C>(client: &C, client_id: &[u8]) -> Result<State, BandwidthControllerError>
where
C: CoconutBandwidthSigningClient + Sync,
C: EcashSigningClient + Sync,
{
let mut rng = OsRng;
let signing_key = identity::PrivateKey::new(&mut rng);
@@ -27,8 +30,7 @@ where
let tx_hash = client
.deposit(
amount.clone(),
CredentialType::Voucher.to_string(),
CredentialType::TicketBook.to_string(),
signing_key.public_key().to_base58_string(),
encryption_key.public_key().to_base58_string(),
None,
@@ -37,7 +39,7 @@ where
.transaction_hash;
let voucher =
IssuanceBandwidthCredential::new_voucher(amount, tx_hash, signing_key, encryption_key);
IssuanceBandwidthCredential::new_voucher(tx_hash, client_id, signing_key, encryption_key);
let state = State { voucher };
@@ -55,7 +57,7 @@ where
<St as Storage>::StorageError: Send + Sync + 'static,
{
// temporary
assert!(state.voucher.typ().is_voucher());
assert!(state.voucher.typ().is_ticketbook());
let epoch_id = client.get_current_epoch().await?.epoch_id;
let threshold = client
@@ -63,11 +65,40 @@ where
.await?
.ok_or(BandwidthControllerError::NoThreshold)?;
let coconut_api_clients = all_coconut_api_clients(client, epoch_id).await?;
let ecash_api_clients = all_ecash_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);
let verification_key = obtain_aggregate_verification_key(&ecash_api_clients)?;
log::info!("Querying wallet signatures");
let wallet = obtain_aggregate_signature(&state.voucher, &ecash_api_clients, threshold).await?;
log::info!("Querying expiration date signatures");
let exp_date_sig =
obtain_expiration_date_signatures(&ecash_api_clients, &verification_key, threshold).await?;
log::info!("Checking coin indices signatures presence");
if !storage
.is_coin_indices_sig_present(epoch_id.to_string())
.await
.map_err(|err| BandwidthControllerError::CredentialStorageError(Box::new(err)))?
{
log::info!("Querying coin indices signatures");
let coin_indices_signatures =
obtain_coin_indices_signatures(&ecash_api_clients, &verification_key, threshold)
.await?;
storage
.insert_coin_indices_sig(
epoch_id.to_string(),
signatures_to_string(&coin_indices_signatures),
)
.await
.map_err(|err| BandwidthControllerError::CredentialStorageError(Box::new(err)))?;
}
let issued = state
.voucher
.to_issued_credential(wallet, exp_date_sig, epoch_id);
// make sure the data gets zeroized after persisting it
let credential_data = Zeroizing::new(issued.pack_v1());
+3 -3
View File
@@ -1,9 +1,9 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_coconut::CoconutError;
use nym_credential_storage::error::StorageError;
use nym_credentials::error::Error as CredentialsError;
use nym_credentials_interface::CompactEcashError;
use nym_crypto::asymmetric::encryption::KeyRecoveryError;
use nym_crypto::asymmetric::identity::Ed25519RecoveryError;
use nym_validator_client::coconut::CoconutApiError;
@@ -28,8 +28,8 @@ pub enum BandwidthControllerError {
#[error(transparent)]
StorageError(#[from] StorageError),
#[error("Coconut error - {0}")]
CoconutError(#[from] CoconutError),
#[error("Ecash error - {0}")]
EcashError(#[from] CompactEcashError),
#[error("Validator client error - {0}")]
ValidatorError(#[from] ValidatorClientError),
+93 -34
View File
@@ -3,16 +3,23 @@
use crate::error::BandwidthControllerError;
use crate::utils::stored_credential_to_issued_bandwidth;
use log::{debug, error, warn};
use log::info;
use log::{error, warn};
use nym_credential_storage::models::StorableIssuedCredential;
use nym_credentials::coconut::utils::{obtain_coin_indices_signatures, signatures_to_string};
use nym_credentials_interface::{constants, PayInfo, VerificationKeyAuth};
use nym_credential_storage::storage::Storage;
use nym_credentials::coconut::bandwidth::issued::BandwidthCredentialIssuedDataVariant;
use nym_credentials::coconut::bandwidth::CredentialSpendingData;
use nym_credentials::coconut::utils::obtain_aggregate_verification_key;
use nym_credentials::coconut::utils::signatures_from_string;
use nym_credentials::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::coconut::all_ecash_api_clients;
use nym_validator_client::nym_api::EpochId;
use nym_validator_client::nyxd::contract_traits::DkgQueryClient;
use zeroize::Zeroizing;
pub mod acquire;
pub mod error;
@@ -34,6 +41,9 @@ pub struct PreparedCredential {
/// The database id of the stored credential.
pub credential_id: i64,
///the updated credential after the payment
pub updated_credential: IssuedBandwidthCredential,
}
pub struct RetrievedCredential {
@@ -50,7 +60,6 @@ impl<C, St: Storage> BandwidthController<C, St> {
/// It marks any retrieved intermediate credentials as expired.
pub async fn get_next_usable_credential(
&self,
gateway_id: &str,
) -> Result<RetrievedCredential, BandwidthControllerError>
where
<St as Storage>::StorageError: Send + Sync + 'static,
@@ -58,7 +67,7 @@ impl<C, St: Storage> BandwidthController<C, St> {
loop {
let Some(maybe_next) = self
.storage
.get_next_unspent_credential(gateway_id)
.get_next_unspent_credential()
.await
.map_err(|err| BandwidthControllerError::CredentialStorageError(Box::new(err)))?
else {
@@ -69,23 +78,16 @@ impl<C, St: Storage> BandwidthController<C, St> {
// 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
Ok(credential) => {
if credential.expired() {
warn!("the credential (id: {id}) has already expired! The expiration was set to {}", credential.expiration_date_formatted());
self.storage.mark_expired(id).await.map_err(|err| {
BandwidthControllerError::CredentialStorageError(Box::new(err))
})?;
continue;
}
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
}
},
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);
@@ -105,51 +107,108 @@ impl<C, St: Storage> BandwidthController<C, St> {
async fn get_aggregate_verification_key(
&self,
epoch_id: EpochId,
) -> Result<VerificationKey, BandwidthControllerError>
) -> Result<VerificationKeyAuth, BandwidthControllerError>
where
C: DkgQueryClient + Sync + Send,
<St as Storage>::StorageError: Send + Sync + 'static,
{
let coconut_api_clients = all_coconut_api_clients(&self.client, epoch_id).await?;
let coconut_api_clients = all_ecash_api_clients(&self.client, epoch_id).await?;
Ok(obtain_aggregate_verification_key(&coconut_api_clients)?)
}
pub async fn prepare_bandwidth_credential(
pub async fn prepare_ecash_credential(
&self,
gateway_id: &str,
provider_pk: [u8; 32],
) -> Result<PreparedCredential, BandwidthControllerError>
where
C: DkgQueryClient + Sync + Send,
<St as Storage>::StorageError: Send + Sync + 'static,
{
let retrieved_credential = self.get_next_usable_credential(gateway_id).await?;
let retrieved_credential = self.get_next_usable_credential().await?;
let epoch_id = retrieved_credential.credential.epoch_id();
let credential_id = retrieved_credential.credential_id;
let verification_key = self.get_aggregate_verification_key(epoch_id).await?;
let spend_request = retrieved_credential
.credential
.prepare_for_spending(&verification_key)?;
let coin_indices_signatures_bs58 = self
.storage
.get_coin_indices_sig(epoch_id.to_string())
.await
.ok();
let coin_indices_signatures = match coin_indices_signatures_bs58 {
Some(epoch_signatures) => signatures_from_string(epoch_signatures.signatures)?,
None => {
info!("We're missing some signatures, let's query them now");
//let's try to query them if we don't have them at that point
let ecash_api_client = all_ecash_api_clients(&self.client, epoch_id).await?;
let threshold = self
.client
.get_current_epoch_threshold()
.await?
.ok_or(BandwidthControllerError::NoThreshold)?;
let coin_indices_signatures =
obtain_coin_indices_signatures(&ecash_api_client, &verification_key, threshold)
.await?;
self.storage
.insert_coin_indices_sig(
epoch_id.to_string(),
signatures_to_string(&coin_indices_signatures),
)
.await
.map_err(|err| {
BandwidthControllerError::CredentialStorageError(Box::new(err))
})?;
coin_indices_signatures
}
};
let pay_info = PayInfo::generate_pay_info(provider_pk);
// the below would only be executed once we know where we want to spend it (i.e. which gateway and stuff)
let spend_request = retrieved_credential.credential.prepare_for_spending(
&verification_key,
pay_info,
coin_indices_signatures,
)?;
Ok(PreparedCredential {
data: spend_request,
epoch_id,
credential_id,
updated_credential: retrieved_credential.credential,
})
}
pub async fn consume_credential(
pub async fn update_ecash_wallet(
&self,
credential: IssuedBandwidthCredential,
id: i64,
gateway_id: &str,
) -> Result<(), BandwidthControllerError>
where
<St as Storage>::StorageError: Send + Sync + 'static,
{
// JS: shouldn't we send some contract/validator/gateway message here to actually, you know,
// consume it?
let consumed = credential.wallet().l() >= constants::NB_TICKETS;
// make sure the data gets zeroized after persisting it
let credential_data = Zeroizing::new(credential.pack_v1());
let storable = StorableIssuedCredential {
serialization_revision: credential.current_serialization_revision(),
credential_data: credential_data.as_ref(),
credential_type: credential.typ().to_string(),
epoch_id: credential
.epoch_id()
.try_into()
.expect("our epoch is has run over u32::MAX!"),
};
self.storage
.consume_coconut_credential(id, gateway_id)
.update_issued_credential(storable, id, consumed)
.await
.map_err(|err| BandwidthControllerError::CredentialStorageError(Box::new(err)))
}
@@ -3,10 +3,12 @@
use async_trait::async_trait;
use log::{debug, error};
use nym_credential_storage::storage::Storage as CredentialStorage;
use nym_crypto::asymmetric::identity;
use nym_gateway_client::GatewayClient;
pub use nym_gateway_client::{GatewayPacketRouter, PacketRouter};
use nym_sphinx::forwarding::packet::MixPacket;
use nym_validator_client::nyxd::contract_traits::DkgQueryClient;
use std::fmt::Debug;
use std::os::raw::c_int as RawFd;
use thiserror::Error;
@@ -111,8 +113,9 @@ impl<C, St> RemoteGateway<C, St> {
impl<C, St> GatewayTransceiver for RemoteGateway<C, St>
where
C: Send,
St: Send,
C: DkgQueryClient + Send + Sync,
St: CredentialStorage,
<St as CredentialStorage>::StorageError: Send + Sync + 'static,
{
fn gateway_identity(&self) -> identity::PublicKey {
self.gateway_client.gateway_identity()
@@ -126,8 +129,9 @@ where
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl<C, St> GatewaySender for RemoteGateway<C, St>
where
C: Send,
St: Send,
C: DkgQueryClient + Send + Sync,
St: CredentialStorage,
<St as CredentialStorage>::StorageError: Send + Sync + 'static,
{
async fn send_mix_packet(&mut self, packet: MixPacket) -> Result<(), ErasedGatewayError> {
self.gateway_client
+47 -45
View File
@@ -23,12 +23,13 @@ use nym_gateway_requests::{
BinaryRequest, ClientControlRequest, ServerResponse, CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION,
CURRENT_PROTOCOL_VERSION,
};
use nym_network_defaults::{REMAINING_BANDWIDTH_THRESHOLD, TOKENS_TO_BURN};
use nym_network_defaults::REMAINING_BANDWIDTH_THRESHOLD;
use nym_sphinx::forwarding::packet::MixPacket;
use nym_task::TaskClient;
use nym_validator_client::nyxd::contract_traits::DkgQueryClient;
use rand::rngs::OsRng;
use std::convert::TryFrom;
use std::sync::atomic::{AtomicI64, Ordering};
use std::sync::Arc;
use std::time::Duration;
use tungstenite::protocol::Message;
@@ -83,7 +84,7 @@ impl GatewayConfig {
pub struct GatewayClient<C, St = EphemeralCredentialStorage> {
authenticated: bool,
disabled_credentials_mode: bool,
bandwidth_remaining: i64,
bandwidth_remaining: Arc<AtomicI64>,
gateway_address: String,
gateway_identity: identity::PublicKey,
local_identity: Arc<identity::KeyPair>,
@@ -122,7 +123,7 @@ impl<C, St> GatewayClient<C, St> {
GatewayClient {
authenticated: false,
disabled_credentials_mode: true,
bandwidth_remaining: 0,
bandwidth_remaining: Arc::new(AtomicI64::new(0)),
gateway_address: config.gateway_listener,
gateway_identity: config.gateway_identity,
local_identity,
@@ -182,7 +183,7 @@ impl<C, St> GatewayClient<C, St> {
}
pub fn remaining_bandwidth(&self) -> i64 {
self.bandwidth_remaining
self.bandwidth_remaining.load(Ordering::Acquire)
}
#[cfg(not(target_arch = "wasm32"))]
@@ -528,7 +529,8 @@ impl<C, St> GatewayClient<C, St> {
} => {
self.check_gateway_protocol(protocol_version)?;
self.authenticated = status;
self.bandwidth_remaining = bandwidth_remaining;
self.bandwidth_remaining
.store(bandwidth_remaining, Ordering::Release);
self.negotiated_protocol = protocol_version;
log::debug!("authenticated: {status}, bandwidth remaining: {bandwidth_remaining}");
Ok(())
@@ -564,35 +566,38 @@ impl<C, St> GatewayClient<C, St> {
}
}
async fn claim_coconut_bandwidth(
async fn claim_ecash_bandwidth(
&mut self,
credential: CredentialSpendingData,
) -> Result<(), GatewayClientError> {
let mut rng = OsRng;
let iv = IV::new_random(&mut rng);
let msg = ClientControlRequest::new_enc_coconut_bandwidth_credential_v2(
let msg = ClientControlRequest::new_enc_ecash_credential(
credential,
self.shared_key.as_ref().unwrap(),
iv,
)
.into();
self.bandwidth_remaining = match self.send_websocket_message(msg).await? {
let bandwidth_remaining = match self.send_websocket_message(msg).await? {
ServerResponse::Bandwidth { available_total } => Ok(available_total),
ServerResponse::Error { message } => Err(GatewayClientError::GatewayError(message)),
_ => Err(GatewayClientError::UnexpectedResponse),
}?;
self.bandwidth_remaining
.store(bandwidth_remaining, Ordering::Release);
Ok(())
}
async fn try_claim_testnet_bandwidth(&mut self) -> Result<(), GatewayClientError> {
let msg = ClientControlRequest::ClaimFreeTestnetBandwidth.into();
self.bandwidth_remaining = match self.send_websocket_message(msg).await? {
let bandwidth_remaining = match self.send_websocket_message(msg).await? {
ServerResponse::Bandwidth { available_total } => Ok(available_total),
ServerResponse::Error { message } => Err(GatewayClientError::GatewayError(message)),
_ => Err(GatewayClientError::UnexpectedResponse),
}?;
self.bandwidth_remaining
.store(bandwidth_remaining, Ordering::Release);
Ok(())
}
@@ -629,49 +634,45 @@ impl<C, St> GatewayClient<C, St> {
negotiated_protocol: Some(gateway_protocol),
});
}
let gateway_id = self.gateway_identity().to_base58_string();
let prepared_credential = self
.bandwidth_controller
.as_ref()
.unwrap()
.prepare_bandwidth_credential(&gateway_id)
.prepare_ecash_credential(self.gateway_identity.to_bytes())
.await?;
self.claim_coconut_bandwidth(prepared_credential.data)
.await?;
self.claim_ecash_bandwidth(prepared_credential.data).await?;
self.bandwidth_controller
.as_ref()
.unwrap()
.consume_credential(prepared_credential.credential_id, &gateway_id)
.update_ecash_wallet(
prepared_credential.updated_credential,
prepared_credential.credential_id,
)
.await?;
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> {
) -> Result<(), GatewayClientError>
where
C: DkgQueryClient + Send + Sync,
St: CredentialStorage,
<St as CredentialStorage>::StorageError: Send + Sync + 'static,
{
debug!("Sending {} mix packets", packets.len());
if !self.authenticated {
return Err(GatewayClientError::NotAuthenticated);
}
if self.estimate_required_bandwidth(&packets) > self.bandwidth_remaining {
return Err(GatewayClientError::NotEnoughBandwidth(
self.estimate_required_bandwidth(&packets),
self.bandwidth_remaining,
));
let bandwidth_remaining = self.bandwidth_remaining.load(Ordering::Acquire);
if bandwidth_remaining < REMAINING_BANDWIDTH_THRESHOLD {
self.claim_bandwidth().await?;
}
if !self.connection.is_established() {
return Err(GatewayClientError::ConnectionNotEstablished);
}
@@ -730,19 +731,20 @@ impl<C, St> GatewayClient<C, St> {
}
// TODO: possibly make responses optional
pub async fn send_mix_packet(
&mut self,
mix_packet: MixPacket,
) -> Result<(), GatewayClientError> {
pub async fn send_mix_packet(&mut self, mix_packet: MixPacket) -> Result<(), GatewayClientError>
where
C: DkgQueryClient + Send + Sync,
St: CredentialStorage,
<St as CredentialStorage>::StorageError: Send + Sync + 'static,
{
if !self.authenticated {
return Err(GatewayClientError::NotAuthenticated);
}
if (mix_packet.packet().len() as i64) > self.bandwidth_remaining {
return Err(GatewayClientError::NotEnoughBandwidth(
mix_packet.packet().len() as i64,
self.bandwidth_remaining,
));
let bandwidth_remaining = self.bandwidth_remaining.load(Ordering::Acquire);
if bandwidth_remaining < REMAINING_BANDWIDTH_THRESHOLD {
self.claim_bandwidth().await?;
}
if !self.connection.is_established() {
return Err(GatewayClientError::ConnectionNotEstablished);
}
@@ -796,6 +798,7 @@ impl<C, St> GatewayClient<C, St> {
.as_ref()
.expect("no shared key present even though we're authenticated!"),
),
self.bandwidth_remaining.clone(),
self.shutdown.clone(),
)
}
@@ -836,10 +839,9 @@ impl<C, St> GatewayClient<C, St> {
self.establish_connection().await?;
}
let shared_key = self.perform_initial_authentication().await?;
if self.bandwidth_remaining < REMAINING_BANDWIDTH_THRESHOLD {
info!("Claiming more bandwidth for your tokens. This will use {} token(s) from your wallet. \
Stop the process now if you don't want that to happen.", TOKENS_TO_BURN);
let bandwidth_remaining = self.bandwidth_remaining.load(Ordering::Acquire);
if bandwidth_remaining < REMAINING_BANDWIDTH_THRESHOLD {
info!("Claiming more bandwidth with existing credentials. Stop the process now if you don't want that to happen.");
self.claim_bandwidth().await?;
}
@@ -876,7 +878,7 @@ impl GatewayClient<InitOnly, EphemeralCredentialStorage> {
GatewayClient {
authenticated: false,
disabled_credentials_mode: true,
bandwidth_remaining: 0,
bandwidth_remaining: Arc::new(AtomicI64::new(0)),
gateway_address: gateway_listener.to_string(),
gateway_identity,
local_identity,
@@ -79,6 +79,7 @@ impl PartiallyDelegated {
fn recover_received_plaintexts(
ws_msgs: Vec<Message>,
shared_key: &SharedKeys,
bandwidth_remaining: Arc<AtomicI64>,
) -> Result<Vec<Vec<u8>>, GatewayClientError> {
let mut plaintexts = Vec::with_capacity(ws_msgs.len());
for ws_msg in ws_msgs {
@@ -104,7 +105,11 @@ impl PartiallyDelegated {
{
ServerResponse::Send {
remaining_bandwidth,
} => maybe_log_bandwidth(remaining_bandwidth),
} => {
maybe_log_bandwidth(remaining_bandwidth);
bandwidth_remaining
.store(remaining_bandwidth, std::sync::atomic::Ordering::Release)
}
ServerResponse::Error { message } => {
error!("gateway failure: {message}");
return Err(GatewayClientError::GatewayError(message));
@@ -129,8 +134,10 @@ impl PartiallyDelegated {
ws_msgs: Vec<Message>,
packet_router: &PacketRouter,
shared_key: &SharedKeys,
bandwidth_remaining: Arc<AtomicI64>,
) -> Result<(), GatewayClientError> {
let plaintexts = Self::recover_received_plaintexts(ws_msgs, shared_key)?;
let plaintexts =
Self::recover_received_plaintexts(ws_msgs, shared_key, bandwidth_remaining)?;
packet_router.route_received(plaintexts)
}
@@ -138,6 +145,7 @@ impl PartiallyDelegated {
conn: WsConn,
mut packet_router: PacketRouter,
shared_key: Arc<SharedKeys>,
bandwidth_remaining: Arc<AtomicI64>,
mut shutdown: TaskClient,
) -> Self {
// when called for, it NEEDS TO yield back the stream so that we could merge it and
@@ -169,7 +177,7 @@ impl PartiallyDelegated {
Ok(msgs) => msgs
};
if let Err(err) = Self::route_socket_messages(ws_msgs, &packet_router, shared_key.as_ref()) {
if let Err(err) = Self::route_socket_messages(ws_msgs, &packet_router, shared_key.as_ref(), bandwidth_remaining.clone()) {
log::error!("Route socket messages failed: {err}");
break Err(err)
}
@@ -18,6 +18,7 @@ nym-ephemera-common = { path = "../../cosmwasm-smart-contracts/ephemera" }
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-name-service-common = { path = "../../cosmwasm-smart-contracts/name-service" }
nym-group-contract-common = { path = "../../cosmwasm-smart-contracts/group-contract" }
@@ -32,7 +33,7 @@ url = { workspace = true, features = ["serde"] }
tokio = { workspace = true, features = ["sync", "time"] }
futures = { workspace = true }
nym-coconut = { path = "../../nymcoconut" }
nym-compact-ecash = { path = "../../nym_offline_compact_ecash" }
nym-network-defaults = { path = "../../network-defaults" }
nym-api-requests = { path = "../../../nym-api/nym-api-requests" }
@@ -8,10 +8,14 @@ use crate::{
nym_api, DirectSigningReqwestRpcValidatorClient, QueryReqwestRpcValidatorClient,
ReqwestRpcClient, ValidatorClientError,
};
use nym_api_requests::coconut::models::FreePassNonceResponse;
use nym_api_requests::coconut::models::{
FreePassNonceResponse, SpentCredentialsResponse, VerifyCredentialBody,
VerifyEcashCredentialResponse,
};
use nym_api_requests::coconut::{
BlindSignRequestBody, BlindedSignatureResponse, FreePassRequest, VerifyCredentialBody,
VerifyCredentialResponse,
BlindSignRequestBody, BlindedSignatureResponse, FreePassRequest,
PartialCoinIndicesSignatureResponse, PartialExpirationDateSignatureResponse,
VerifyCredentialResponse, VerifyEcashCredentialBody,
};
use nym_api_requests::models::{DescribedGateway, MixNodeBondAnnotated};
use nym_api_requests::models::{
@@ -351,6 +355,48 @@ impl NymApiClient {
.await?)
}
pub async fn verify_offline_credential(
&self,
request_body: &VerifyEcashCredentialBody,
) -> Result<VerifyEcashCredentialResponse, ValidatorClientError> {
Ok(self.nym_api.verify_offline_credential(request_body).await?)
}
pub async fn verify_online_credential(
&self,
request_body: &VerifyEcashCredentialBody,
) -> Result<VerifyEcashCredentialResponse, ValidatorClientError> {
Ok(self.nym_api.verify_online_credential(request_body).await?)
}
pub async fn spent_credentials(
&self,
) -> Result<SpentCredentialsResponse, ValidatorClientError> {
Ok(self.nym_api.spent_credentials().await?)
}
pub async fn expiration_date_signatures(
&self,
) -> Result<PartialExpirationDateSignatureResponse, ValidatorClientError> {
Ok(self.nym_api.expiration_date_signatures().await?)
}
pub async fn expiration_date_signatures_timestamp(
&self,
timestamp: u64,
) -> Result<PartialExpirationDateSignatureResponse, ValidatorClientError> {
Ok(self
.nym_api
.expiration_date_signatures_timestamp(&timestamp.to_string())
.await?)
}
pub async fn coin_indices_signatures(
&self,
) -> Result<PartialCoinIndicesSignatureResponse, ValidatorClientError> {
Ok(self.nym_api.coin_indices_signatures().await?)
}
pub async fn free_pass_nonce(&self) -> Result<FreePassNonceResponse, ValidatorClientError> {
Ok(self.nym_api.free_pass_nonce().await?)
}
@@ -4,9 +4,10 @@
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 thiserror::Error;
use url::Url;
@@ -14,7 +15,7 @@ use url::Url;
#[derive(Clone)]
pub struct CoconutApiClient {
pub api_client: NymApiClient,
pub verification_key: VerificationKey,
pub verification_key: VerificationKeyAuth,
pub node_id: NodeIndex,
pub cosmos_address: cosmrs::AccountId,
}
@@ -43,7 +44,7 @@ pub enum CoconutApiError {
#[error("the provided verification key is malformed: {source}")]
MalformedVerificationKey {
#[from]
source: CoconutError,
source: CompactEcashError,
},
#[error("the provided account address is malformed: {source}")]
@@ -65,14 +66,14 @@ impl TryFrom<ContractVKShare> for CoconutApiClient {
Ok(CoconutApiClient {
api_client: NymApiClient::new(url_address),
verification_key: VerificationKey::try_from_bs58(&share.share)?,
verification_key: VerificationKeyAuth::try_from_bs58(&share.share)?,
node_id: share.node_index,
cosmos_address: share.owner.as_str().parse()?,
})
}
}
pub async fn all_coconut_api_clients<C>(
pub async fn all_ecash_api_clients<C>(
client: &C,
epoch_id: EpochId,
) -> Result<Vec<CoconutApiClient>, CoconutApiError>
@@ -8,10 +8,11 @@ pub use nym_api_requests::{
coconut::{
models::{
EpochCredentialsResponse, IssuedCredential, IssuedCredentialBody,
IssuedCredentialResponse, IssuedCredentialsResponse,
IssuedCredentialResponse, IssuedCredentialsResponse, SpentCredentialsResponse,
},
BlindSignRequestBody, BlindedSignatureResponse, CredentialsRequestBody,
VerifyCredentialBody, VerifyCredentialResponse,
PartialCoinIndicesSignatureResponse, PartialExpirationDateSignatureResponse,
VerifyCredentialResponse, VerifyEcashCredentialBody,
},
models::{
ComputeRewardEstParam, DescribedGateway, GatewayBondAnnotated, GatewayCoreStatusResponse,
@@ -31,7 +32,9 @@ use nym_service_provider_directory_common::response::ServicesListResponse;
pub mod error;
pub mod routes;
use nym_api_requests::coconut::models::FreePassNonceResponse;
use nym_api_requests::coconut::models::{
FreePassNonceResponse, VerifyCredentialBody, VerifyEcashCredentialResponse,
};
use nym_api_requests::coconut::FreePassRequest;
pub use nym_http_api_client::Client;
@@ -439,6 +442,100 @@ pub trait NymApiClientExt: ApiClient {
.await
}
async fn verify_offline_credential(
&self,
request_body: &VerifyEcashCredentialBody,
) -> Result<VerifyEcashCredentialResponse, NymAPIError> {
self.post_json(
&[
routes::API_VERSION,
routes::COCONUT_ROUTES,
routes::BANDWIDTH,
routes::ECASH_VERIFY_OFFLINE_CREDENTIAL,
],
NO_PARAMS,
request_body,
)
.await
}
async fn verify_online_credential(
&self,
request_body: &VerifyEcashCredentialBody,
) -> Result<VerifyEcashCredentialResponse, NymAPIError> {
self.post_json(
&[
routes::API_VERSION,
routes::COCONUT_ROUTES,
routes::BANDWIDTH,
routes::ECASH_VERIFY_ONLINE_CREDENTIAL,
],
NO_PARAMS,
request_body,
)
.await
}
async fn spent_credentials(&self) -> Result<SpentCredentialsResponse, NymAPIError> {
self.get_json(
&[
routes::API_VERSION,
routes::COCONUT_ROUTES,
routes::BANDWIDTH,
routes::SPENT_CREDENTIALS,
],
NO_PARAMS,
)
.await
}
async fn expiration_date_signatures(
&self,
) -> Result<PartialExpirationDateSignatureResponse, NymAPIError> {
self.get_json(
&[
routes::API_VERSION,
routes::COCONUT_ROUTES,
routes::BANDWIDTH,
routes::EXPIRATION_DATE_SIGNATURES,
],
NO_PARAMS,
)
.await
}
async fn expiration_date_signatures_timestamp(
&self,
timestamp: &str,
) -> Result<PartialExpirationDateSignatureResponse, NymAPIError> {
self.get_json(
&[
routes::API_VERSION,
routes::COCONUT_ROUTES,
routes::BANDWIDTH,
routes::EXPIRATION_DATE_SIGNATURES,
timestamp,
],
NO_PARAMS,
)
.await
}
async fn coin_indices_signatures(
&self,
) -> Result<PartialCoinIndicesSignatureResponse, NymAPIError> {
self.get_json(
&[
routes::API_VERSION,
routes::COCONUT_ROUTES,
routes::BANDWIDTH,
routes::COIN_INDICES_SIGNATURES,
],
NO_PARAMS,
)
.await
}
async fn epoch_credentials(
&self,
dkg_epoch: EpochId,
@@ -14,11 +14,17 @@ pub const ACTIVE: &str = "active";
pub const REWARDED: &str = "rewarded";
pub const COCONUT_ROUTES: &str = "coconut";
pub const BANDWIDTH: &str = "bandwidth";
pub const SPENT_CREDENTIALS: &str = "spent-credentials";
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 ECASH_VERIFY_OFFLINE_CREDENTIAL: &str = "verify-offline-credential";
pub const ECASH_VERIFY_ONLINE_CREDENTIAL: &str = "verify-online-credential";
pub const COCONUT_VERIFY_BANDWIDTH_CREDENTIAL: &str = "verify-bandwidth-credential";
pub const EXPIRATION_DATE_SIGNATURES: &str = "expiration-date-signatures";
pub const EXPIRATION_DATE_SIGNATURES_TIMESTAMP: &str = "expiration-date-signatures-ts";
pub const COIN_INDICES_SIGNATURES: &str = "coin-indices-signatures";
pub const COCONUT_EPOCH_CREDENTIALS: &str = "epoch-credentials";
pub const COCONUT_ISSUED_CREDENTIAL: &str = "issued-credential";
pub const COCONUT_ISSUED_CREDENTIALS: &str = "issued-credentials";
@@ -1,100 +0,0 @@
// Copyright 2022-2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::collect_paged;
use crate::nyxd::contract_traits::NymContractsProvider;
use crate::nyxd::error::NyxdError;
use crate::nyxd::CosmWasmClient;
use async_trait::async_trait;
use nym_coconut_bandwidth_contract_common::msg::QueryMsg as CoconutBandwidthQueryMsg;
use nym_coconut_bandwidth_contract_common::spend_credential::{
PagedSpendCredentialResponse, SpendCredential, SpendCredentialResponse,
};
use serde::Deserialize;
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait CoconutBandwidthQueryClient {
async fn query_coconut_bandwidth_contract<T>(
&self,
query: CoconutBandwidthQueryMsg,
) -> Result<T, NyxdError>
where
for<'a> T: Deserialize<'a>;
async fn get_spent_credential(
&self,
blinded_serial_number: String,
) -> Result<SpendCredentialResponse, NyxdError> {
self.query_coconut_bandwidth_contract(CoconutBandwidthQueryMsg::GetSpentCredential {
blinded_serial_number,
})
.await
}
async fn get_all_spent_credential_paged(
&self,
start_after: Option<String>,
limit: Option<u32>,
) -> Result<PagedSpendCredentialResponse, NyxdError> {
self.query_coconut_bandwidth_contract(CoconutBandwidthQueryMsg::GetAllSpentCredentials {
limit,
start_after,
})
.await
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait PagedCoconutBandwidthQueryClient: CoconutBandwidthQueryClient {
async fn get_all_spent_credentials(&self) -> Result<Vec<SpendCredential>, NyxdError> {
collect_paged!(self, get_all_spent_credential_paged, spend_credentials)
}
}
#[async_trait]
impl<T> PagedCoconutBandwidthQueryClient for T where T: CoconutBandwidthQueryClient {}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl<C> CoconutBandwidthQueryClient for C
where
C: CosmWasmClient + NymContractsProvider + Send + Sync,
{
async fn query_coconut_bandwidth_contract<T>(
&self,
query: CoconutBandwidthQueryMsg,
) -> Result<T, NyxdError>
where
for<'a> T: Deserialize<'a>,
{
let coconut_bandwidth_contract_address = self
.coconut_bandwidth_contract_address()
.ok_or_else(|| NyxdError::unavailable_contract_address("coconut bandwidth contract"))?;
self.query_contract_smart(coconut_bandwidth_contract_address, &query)
.await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::nyxd::contract_traits::tests::IgnoreValue;
// it's enough that this compiles and clippy is happy about it
#[allow(dead_code)]
fn all_query_variants_are_covered<C: CoconutBandwidthQueryClient + Send + Sync>(
client: C,
msg: CoconutBandwidthQueryMsg,
) {
match msg {
CoconutBandwidthQueryMsg::GetSpentCredential {
blinded_serial_number,
} => client.get_spent_credential(blinded_serial_number).ignore(),
CoconutBandwidthQueryMsg::GetAllSpentCredentials { limit, start_after } => client
.get_all_spent_credential_paged(start_after, limit)
.ignore(),
};
}
}
@@ -1,153 +0,0 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::nyxd::contract_traits::NymContractsProvider;
use crate::nyxd::cosmwasm_client::types::ExecuteResult;
use crate::nyxd::error::NyxdError;
use crate::nyxd::{Coin, Fee, SigningCosmWasmClient};
use crate::signing::signer::OfflineSigner;
use async_trait::async_trait;
use nym_coconut_bandwidth_contract_common::spend_credential::SpendCredentialData;
use nym_coconut_bandwidth_contract_common::{
deposit::DepositData, msg::ExecuteMsg as CoconutBandwidthExecuteMsg,
};
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait CoconutBandwidthSigningClient {
async fn execute_coconut_bandwidth_contract(
&self,
fee: Option<Fee>,
msg: CoconutBandwidthExecuteMsg,
memo: String,
funds: Vec<Coin>,
) -> Result<ExecuteResult, NyxdError>;
async fn deposit(
&self,
amount: Coin,
info: String,
verification_key: String,
encryption_key: String,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
let req = CoconutBandwidthExecuteMsg::DepositFunds {
data: DepositData::new(info, verification_key, encryption_key),
};
self.execute_coconut_bandwidth_contract(
fee,
req,
"CoconutBandwidth::Deposit".to_string(),
vec![amount],
)
.await
}
async fn spend_credential(
&self,
funds: Coin,
blinded_serial_number: String,
gateway_cosmos_address: String,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
let req = CoconutBandwidthExecuteMsg::SpendCredential {
data: SpendCredentialData::new(
funds.into(),
blinded_serial_number,
gateway_cosmos_address,
),
};
self.execute_coconut_bandwidth_contract(
fee,
req,
"CoconutBandwidth::SpendCredential".to_string(),
vec![],
)
.await
}
async fn release_funds(
&self,
amount: Coin,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
self.execute_coconut_bandwidth_contract(
fee,
CoconutBandwidthExecuteMsg::ReleaseFunds {
funds: amount.into(),
},
"CoconutBandwidth::ReleaseFunds".to_string(),
vec![],
)
.await
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl<C> CoconutBandwidthSigningClient for C
where
C: SigningCosmWasmClient + NymContractsProvider + Sync,
NyxdError: From<<Self as OfflineSigner>::Error>,
{
async fn execute_coconut_bandwidth_contract(
&self,
fee: Option<Fee>,
msg: CoconutBandwidthExecuteMsg,
memo: String,
funds: Vec<Coin>,
) -> Result<ExecuteResult, NyxdError> {
let coconut_bandwidth_contract_address = self
.coconut_bandwidth_contract_address()
.ok_or_else(|| NyxdError::unavailable_contract_address("coconut bandwidth contract"))?;
let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier())));
let signer_address = &self.signer_addresses()?[0];
self.execute(
signer_address,
coconut_bandwidth_contract_address,
&msg,
fee,
memo,
funds,
)
.await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::nyxd::contract_traits::tests::{mock_coin, IgnoreValue};
// it's enough that this compiles and clippy is happy about it
#[allow(dead_code)]
fn all_execute_variants_are_covered<C: CoconutBandwidthSigningClient + Send + Sync>(
client: C,
msg: CoconutBandwidthExecuteMsg,
) {
match msg {
CoconutBandwidthExecuteMsg::DepositFunds { data } => client
.deposit(
mock_coin(),
data.deposit_info().to_string(),
data.identity_key().to_string(),
data.encryption_key().to_string(),
None,
)
.ignore(),
CoconutBandwidthExecuteMsg::SpendCredential { data } => client
.spend_credential(
mock_coin(),
data.blinded_serial_number().to_string(),
data.gateway_cosmos_address().to_string(),
None,
)
.ignore(),
CoconutBandwidthExecuteMsg::ReleaseFunds { funds } => {
client.release_funds(funds.into(), None).ignore()
}
};
}
}
@@ -0,0 +1,101 @@
// 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_ecash_contract_common::blacklist::BlacklistedAccountResponse;
use nym_ecash_contract_common::msg::QueryMsg as EcashQueryMsg;
use nym_ecash_contract_common::spend_credential::{
EcashSpentCredential, EcashSpentCredentialResponse, PagedEcashSpentCredentialResponse,
};
use serde::Deserialize;
#[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_spent_credential(
&self,
serial_number: String,
) -> Result<EcashSpentCredentialResponse, NyxdError> {
self.query_ecash_contract(EcashQueryMsg::GetSpentCredential { serial_number })
.await
}
async fn get_all_spent_credential_paged(
&self,
start_after: Option<String>,
limit: Option<u32>,
) -> Result<PagedEcashSpentCredentialResponse, NyxdError> {
self.query_ecash_contract(EcashQueryMsg::GetAllSpentCredentials { limit, start_after })
.await
}
async fn get_blacklisted_account(
&self,
public_key: String,
) -> Result<BlacklistedAccountResponse, NyxdError> {
self.query_ecash_contract(EcashQueryMsg::GetBlacklistedAccount { public_key })
.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_spent_credentials(&self) -> Result<Vec<EcashSpentCredential>, NyxdError> {
collect_paged!(self, get_all_spent_credential_paged, spend_credentials)
}
}
#[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
.coconut_bandwidth_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;
// 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::GetSpentCredential { serial_number } => {
client.get_spent_credential(serial_number).ignore()
}
EcashQueryMsg::GetAllSpentCredentials { limit, start_after } => client
.get_all_spent_credential_paged(start_after, limit)
.ignore(),
EcashQueryMsg::GetBlacklistedAccount { public_key } => {
client.get_blacklisted_account(public_key).ignore()
}
};
}
}
@@ -0,0 +1,143 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::nyxd::contract_traits::NymContractsProvider;
use crate::nyxd::cosmwasm_client::types::ExecuteResult;
use crate::nyxd::error::NyxdError;
use crate::nyxd::{Coin, Fee, SigningCosmWasmClient};
use crate::signing::signer::OfflineSigner;
use async_trait::async_trait;
use nym_ecash_contract_common::events::TICKET_BOOK_VALUE;
use nym_ecash_contract_common::msg::ExecuteMsg as EcashExecuteMsg;
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait EcashSigningClient {
async fn execute_ecash_contract(
&self,
fee: Option<Fee>,
msg: EcashExecuteMsg,
memo: String,
funds: Vec<Coin>,
) -> Result<ExecuteResult, NyxdError>;
async fn deposit(
&self,
info: String,
verification_key: String,
encryption_key: String,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
let req = EcashExecuteMsg::DepositFunds {
deposit_info: info,
identity_key: verification_key,
encryption_key,
};
let amount = Coin::new(TICKET_BOOK_VALUE, "unym");
self.execute_ecash_contract(fee, req, "Ecash::Deposit".to_string(), vec![amount])
.await
}
async fn prepare_credential(
&self,
serial_number: String,
gateway_cosmos_address: String,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
let req = EcashExecuteMsg::PrepareCredential {
serial_number,
gateway_cosmos_address,
};
self.execute_ecash_contract(fee, req, "Ecash::PrepareCredential".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 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::IgnoreValue;
// 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::DepositFunds {
deposit_info,
identity_key,
encryption_key,
} => client
.deposit(
deposit_info.to_string(),
identity_key.to_string(),
encryption_key.to_string(),
None,
)
.ignore(),
EcashExecuteMsg::PrepareCredential {
serial_number,
gateway_cosmos_address,
} => client
.prepare_credential(
serial_number.to_string(),
gateway_cosmos_address.to_string(),
None,
)
.ignore(),
EcashExecuteMsg::SpendCredential {
serial_number: _,
gateway_cosmos_address: _,
} => unimplemented!(), //no spend credential method for the client
EcashExecuteMsg::AddToBlacklist { public_key: _ } => unimplemented!(), //no add to blacklist method on client
EcashExecuteMsg::ProposeToBlacklist { public_key } => {
client.propose_for_blacklist(public_key, None).ignore()
}
};
}
}
@@ -8,8 +8,8 @@ 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 ephemera_query_client;
pub mod group_query_client;
pub mod mixnet_query_client;
@@ -19,8 +19,8 @@ pub mod sp_directory_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 ephemera_signing_client;
pub mod group_signing_client;
pub mod mixnet_signing_client;
@@ -30,10 +30,8 @@ pub mod sp_directory_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 ephemera_query_client::{EphemeraQueryClient, PagedEphemeraQueryClient};
pub use group_query_client::{GroupQueryClient, PagedGroupQueryClient};
pub use mixnet_query_client::{MixnetQueryClient, PagedMixnetQueryClient};
@@ -43,8 +41,8 @@ pub use sp_directory_query_client::{PagedSpDirectoryQueryClient, SpDirectoryQuer
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 ephemera_signing_client::EphemeraSigningClient;
pub use group_signing_client::GroupSigningClient;
pub use mixnet_signing_client::MixnetSigningClient;
@@ -5,8 +5,8 @@ use crate::nyxd::error::NyxdError;
use itertools::Itertools;
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 theirs logs
+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-service-provider-directory-common = { path = "../cosmwasm-smart-contracts/service-provider-directory" }
nym-name-service-common = { path = "../cosmwasm-smart-contracts/name-service" }
nym-sphinx = { path = "../../common/nymsphinx" }
@@ -9,12 +9,15 @@ 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::coconut::bandwidth::bandwidth_credential_params;
use nym_credentials::coconut::utils::freepass_exp_date_timestamp;
use nym_credentials::coconut::utils::today_timestamp;
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_credentials_interface::aggregate_expiration_signatures;
use nym_credentials_interface::VerificationKeyAuth;
use nym_validator_client::coconut::all_ecash_api_clients;
use nym_validator_client::nyxd::contract_traits::{DkgQueryClient, NymContractsProvider};
use nym_validator_client::nyxd::CosmWasmClient;
use nym_validator_client::signing::AccountData;
@@ -24,6 +27,7 @@ use std::io::Write;
use std::path::PathBuf;
use std::sync::Arc;
use time::format_description::well_known::Rfc3339;
use time::macros::time;
use time::OffsetDateTime;
use zeroize::Zeroizing;
@@ -55,17 +59,17 @@ pub struct Args {
async fn get_freepass(
api_clients: Vec<CoconutApiClient>,
aggregate_vk: &VerificationKey,
aggregate_vk: &VerificationKeyAuth,
threshold: u64,
epoch_id: EpochId,
signing_account: &AccountData,
expiration_date: OffsetDateTime,
expiration_date_ts: u64,
) -> anyhow::Result<IssuedBandwidthCredential> {
let issuance_pass = IssuanceBandwidthCredential::new_freepass(Some(expiration_date));
let issuance_pass = IssuanceBandwidthCredential::new_freepass(expiration_date_ts);
let signing_data = issuance_pass.prepare_for_signing();
let credential_shares = Arc::new(tokio::sync::Mutex::new(Vec::new()));
let wallet_shares = Arc::new(tokio::sync::Mutex::new(Vec::new()));
let signatures_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
@@ -77,35 +81,65 @@ async fn get_freepass(
match issuance_pass
.obtain_partial_freepass_credential(
&client.api_client,
client.node_id,
signing_account,
&client.verification_key,
signing_data.clone(),
)
.await
{
Ok(partial_credential) => {
credential_shares
.lock()
.await
.push((partial_credential, client.node_id).into());
Ok(partial_wallet) => {
wallet_shares.lock().await.push(partial_wallet);
}
Err(err) => {
error!("failed to obtain partial free pass from {api_url}: {err}")
}
}
info!("contacting {api_url} for expiration date signatures");
match client
.api_client
.expiration_date_signatures_timestamp(expiration_date_ts)
.await
{
Ok(signature) => {
let index = client.node_id;
let share = client.verification_key.clone();
signatures_shares
.lock()
.await
.push((index, share, signature.signs));
}
Err(err) => {
error!("failed to obtain expiration date signature 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();
let wallet_shares = Arc::into_inner(wallet_shares).unwrap().into_inner();
let signatures_shares = Arc::into_inner(signatures_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());
if wallet_shares.len() < threshold as usize {
bail!("we managed to obtain only {} partial credentials while the minimum threshold is {threshold}", wallet_shares.len());
}
let signature = issuance_pass.aggregate_signature_shares(aggregate_vk, &credential_shares)?;
Ok(issuance_pass.into_issued_credential(signature, epoch_id))
let wallet =
issuance_pass.aggregate_signature_shares(aggregate_vk, &wallet_shares, signing_data)?;
if signatures_shares.len() < threshold as usize {
bail!("we managed to obtain only {} partial expiration date signatures while the minimum threshold is {threshold}", signatures_shares.len());
}
let exp_date_sigs = aggregate_expiration_signatures(
bandwidth_credential_params(),
aggregate_vk,
expiration_date_ts,
&signatures_shares,
)?;
Ok(issuance_pass.into_issued_credential(wallet, exp_date_sigs, epoch_id))
}
pub async fn execute(args: Args, client: SigningClient) -> anyhow::Result<()> {
@@ -142,13 +176,18 @@ pub async fn execute(args: Args, client: SigningClient) -> anyhow::Result<()> {
None => OffsetDateTime::from_unix_timestamp(args.expiration_timestamp.unwrap())?,
};
let now = OffsetDateTime::now_utc();
//SAFETY : positive timestamp, so conversion to unsigned will not fail
let expiration_date_ts: u64 = expiration_date
.replace_time(time!(0:00))
.unix_timestamp()
.try_into()
.unwrap();
if expiration_date > now + MAX_FREE_PASS_VALIDITY {
if expiration_date_ts > freepass_exp_date_timestamp() {
bail!("the provided free pass request has too long expiry (expiry is set to on {expiration_date})")
}
if expiration_date < now {
if expiration_date_ts < today_timestamp() {
bail!("the provided free pass expiry is set in the past!")
}
@@ -162,7 +201,7 @@ pub async fn execute(args: Args, client: SigningClient) -> anyhow::Result<()> {
.get_current_epoch_threshold()
.await?
.ok_or(anyhow!("no threshold available"))?;
let api_clients = all_coconut_api_clients(&client, epoch_id).await?;
let api_clients = all_ecash_api_clients(&client, epoch_id).await?;
if api_clients.len() < threshold as usize {
bail!(
@@ -181,7 +220,7 @@ pub async fn execute(args: Args, client: SigningClient) -> anyhow::Result<()> {
threshold,
epoch_id,
&signing_account,
expiration_date,
expiration_date_ts,
)
.await?;
let credential_data = Zeroizing::new(free_pass.pack_v1());
@@ -7,7 +7,7 @@ use anyhow::bail;
use clap::Parser;
use nym_credential_storage::initialise_persistent_storage;
use nym_credential_utils::utils;
use nym_validator_client::nyxd::Coin;
use nym_crypto::asymmetric::identity;
use std::path::PathBuf;
#[derive(Debug, Parser)]
@@ -16,20 +16,12 @@ pub struct Args {
#[clap(long)]
pub(crate) client_config: PathBuf,
/// The amount of utokens the credential will hold.
#[clap(long, default_value = "0")]
pub(crate) amount: u64,
/// Path to a directory used to store recovery files for unconsumed deposits
#[clap(long)]
pub(crate) recovery_dir: PathBuf,
}
pub async fn execute(args: Args, client: SigningClient) -> anyhow::Result<()> {
if args.amount == 0 {
bail!("did not specify credential amount")
}
let loaded = CommonConfigsWrapper::try_load(args.client_config)?;
if let Ok(id) = loaded.try_get_id() {
@@ -40,16 +32,24 @@ pub async fn execute(args: Args, client: SigningClient) -> anyhow::Result<()> {
bail!("the loaded config does not have a credentials store information")
};
let Ok(private_id_key) = loaded.try_get_private_id_key() else {
bail!("the loaded config does not have a public id key information")
};
println!(
"using credentials store at '{}'",
credentials_store.display()
);
let denom = &client.current_chain_details().mix_denom.base;
let coin = Coin::new(args.amount as u128, denom);
let persistent_storage = initialise_persistent_storage(credentials_store).await;
utils::issue_credential(&client, coin, &persistent_storage, args.recovery_dir).await?;
let private_id_key: identity::PrivateKey = nym_pemstore::load_key(private_id_key)?;
utils::issue_credential(
&client,
&private_id_key.to_bytes(),
&persistent_storage,
args.recovery_dir,
)
.await?;
Ok(())
}
+28
View File
@@ -123,6 +123,21 @@ impl CommonConfigsWrapper {
}
}
pub(crate) fn try_get_private_id_key(&self) -> anyhow::Result<PathBuf> {
match self {
CommonConfigsWrapper::NymClients(cfg) => Ok(cfg
.storage_paths
.inner
.keys
.private_identity_key_file
.clone()),
CommonConfigsWrapper::NymApi(_cfg) => {
todo!() //SW this will depend on the new network monitor structure. Ping @Drazen
}
CommonConfigsWrapper::Unknown(cfg) => cfg.try_get_private_id_key(),
}
}
pub(crate) fn try_get_credentials_store(&self) -> anyhow::Result<PathBuf> {
match self {
CommonConfigsWrapper::NymClients(cfg) => {
@@ -225,4 +240,17 @@ impl UnknownConfigWrapper {
bail!("no 'credentials_database_path' field present in the config")
}
}
pub(crate) fn try_get_private_id_key(&self) -> anyhow::Result<PathBuf> {
let id_val = self
.find_value("keys.private_identity_key_file")
.ok_or_else(|| {
anyhow!("no 'keys.private_identity_key_file' field present in the config")
})?;
if let toml::Value::String(pub_id_key) = id_val {
Ok(pub_id_key.parse()?)
} else {
bail!("no 'keys.private_identity_key_file' field present in the config")
}
}
}
@@ -6,13 +6,13 @@ use std::str::FromStr;
use clap::Parser;
use log::{debug, info};
use nym_coconut_bandwidth_contract_common::msg::InstantiateMsg;
use nym_ecash_contract_common::msg::InstantiateMsg;
use nym_validator_client::nyxd::AccountId;
#[derive(Debug, Parser)]
pub struct Args {
#[clap(long)]
pub pool_addr: String,
pub group_addr: Option<AccountId>,
#[clap(long)]
pub multisig_addr: Option<AccountId>,
@@ -26,8 +26,15 @@ pub async fn generate(args: Args) {
debug!("Received arguments: {:?}", args);
let group_addr = args.group_addr.unwrap_or_else(|| {
let address = std::env::var(nym_network_defaults::var_names::GROUP_CONTRACT_ADDRESS)
.expect("Multisig address has to be set");
AccountId::from_str(address.as_str())
.expect("Failed converting multisig address to AccountId")
});
let multisig_addr = args.multisig_addr.unwrap_or_else(|| {
let address = std::env::var(nym_network_defaults::var_names::REWARDING_VALIDATOR_ADDRESS)
let address = std::env::var(nym_network_defaults::var_names::MULTISIG_CONTRACT_ADDRESS)
.expect("Multisig address has to be set");
AccountId::from_str(address.as_str())
.expect("Failed converting multisig address to AccountId")
@@ -38,7 +45,7 @@ pub async fn generate(args: Args) {
});
let instantiate_msg = InstantiateMsg {
pool_addr: args.pool_addr,
group_addr: group_addr.to_string(),
multisig_addr: multisig_addr.to_string(),
mix_denom,
};
@@ -0,0 +1,16 @@
[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]
cosmwasm-std = { workspace = true }
cosmwasm-schema = { workspace = true }
cw2 = { workspace = true, optional = true }
nym-multisig-contract-common = { path = "../multisig-contract" }
[features]
schema = ["cw2"]
@@ -0,0 +1,110 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_schema::cw_serde;
#[cw_serde]
pub struct BlacklistedAccount {
public_key: String,
}
impl BlacklistedAccount {
pub fn new(public_key: String) -> Self {
BlacklistedAccount { public_key }
}
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<BlacklistedAccount>,
}
impl BlacklistedAccountResponse {
pub fn new(account: Option<BlacklistedAccount>) -> Self {
BlacklistedAccountResponse { account }
}
}
#[cw_serde]
pub struct BlacklistProposal {
public_key: String,
proposal_id: u64,
}
impl BlacklistProposal {
pub fn new(public_key: String, proposal_id: u64) -> Self {
BlacklistProposal {
public_key,
proposal_id,
}
}
pub fn public_key(&self) -> &str {
&self.public_key
}
pub fn proposal_id(&self) -> u64 {
self.proposal_id
}
}
#[cw_serde]
pub struct PagedBlacklistProposalResponse {
pub accounts: Vec<BlacklistProposal>,
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 PagedBlacklistProposalResponse {
pub fn new(
accounts: Vec<BlacklistProposal>,
per_page: usize,
start_next_after: Option<String>,
) -> Self {
PagedBlacklistProposalResponse {
accounts,
per_page,
start_next_after,
}
}
}
#[cw_serde]
pub struct BlacklistProposalResponse {
pub account: Option<BlacklistProposal>,
}
impl BlacklistProposalResponse {
pub fn new(account: Option<BlacklistProposal>) -> Self {
BlacklistProposalResponse { account }
}
}
@@ -0,0 +1,4 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub const BANDWIDTH_PROPOSAL_ID: &str = "proposal_id";
@@ -0,0 +1,20 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
// event types
pub const DEPOSITED_FUNDS_EVENT_TYPE: &str = "deposited-funds";
// a 'wasm-' prefix is added to all cosmwasm events
pub const COSMWASM_DEPOSITED_FUNDS_EVENT_TYPE: &str = "wasm-deposited-funds";
// attributes that are used in multiple places
pub const DEPOSIT_VALUE: &str = "deposit-value";
pub const DEPOSIT_INFO: &str = "deposit-info";
pub const DEPOSIT_IDENTITY_KEY: &str = "deposit-identity-key";
pub const DEPOSIT_ENCRYPTION_KEY: &str = "deposit-encryption-key";
pub const TICKET_BOOK_VALUE: u128 = 50_000_000;
pub const TICKET_VALUE: u128 = 50_000;
pub const BLACKLIST_PROPOSAL_ID: &str = "proposal_id";
pub const BLACKLIST_PROPOSAL_REPLY_ID: u64 = 7759;
@@ -0,0 +1,5 @@
pub mod blacklist;
pub mod event_attributes;
pub mod events;
pub mod msg;
pub mod spend_credential;
@@ -0,0 +1,58 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_schema::cw_serde;
#[cfg(feature = "schema")]
use crate::blacklist::BlacklistedAccountResponse;
#[cfg(feature = "schema")]
use cosmwasm_schema::QueryResponses;
#[cw_serde]
pub struct InstantiateMsg {
pub multisig_addr: String,
pub group_addr: String,
pub mix_denom: String,
}
#[cw_serde]
pub enum ExecuteMsg {
DepositFunds {
deposit_info: String,
identity_key: String,
encryption_key: String,
},
PrepareCredential {
serial_number: String,
gateway_cosmos_address: String,
},
SpendCredential {
serial_number: String,
gateway_cosmos_address: String,
},
ProposeToBlacklist {
public_key: String,
},
AddToBlacklist {
public_key: String,
},
}
#[cw_serde]
#[cfg_attr(feature = "schema", derive(QueryResponses))]
pub enum QueryMsg {
#[cfg_attr(feature = "schema", returns(BlacklistedAccountResponse))]
GetBlacklistedAccount { public_key: String },
#[cfg_attr(feature = "schema", returns(SpendCredentialResponse))]
GetSpentCredential { serial_number: String },
#[cfg_attr(feature = "schema", returns(PagedSpendCredentialResponse))]
GetAllSpentCredentials {
limit: Option<u32>,
start_after: Option<String>,
},
}
// #[cw_serde]
// pub struct MigrateMsg {}
@@ -0,0 +1,77 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::msg::ExecuteMsg;
use cosmwasm_schema::cw_serde;
use cosmwasm_std::{from_binary, CosmosMsg, WasmMsg};
#[cw_serde]
pub struct EcashSpentCredential {
serial_number: String,
gateway_cosmos_address: String,
}
impl EcashSpentCredential {
pub fn new(serial_number: String, gateway_cosmos_address: String) -> Self {
EcashSpentCredential {
serial_number,
gateway_cosmos_address,
}
}
pub fn serial_number(&self) -> &str {
&self.serial_number
}
}
#[cw_serde]
pub struct PagedEcashSpentCredentialResponse {
pub spend_credentials: Vec<EcashSpentCredential>,
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 PagedEcashSpentCredentialResponse {
pub fn new(
spend_credentials: Vec<EcashSpentCredential>,
per_page: usize,
start_next_after: Option<String>,
) -> Self {
PagedEcashSpentCredentialResponse {
spend_credentials,
per_page,
start_next_after,
}
}
}
#[cw_serde]
pub struct EcashSpentCredentialResponse {
pub spend_credential: Option<EcashSpentCredential>,
}
impl EcashSpentCredentialResponse {
pub fn new(spend_credential: Option<EcashSpentCredential>) -> Self {
EcashSpentCredentialResponse { spend_credential }
}
}
pub fn check_proposal(msgs: Vec<CosmosMsg>) -> bool {
if let Some(CosmosMsg::Wasm(WasmMsg::Execute {
contract_addr: _,
msg,
funds: _,
})) = msgs.first()
{
if let Ok(ExecuteMsg::SpendCredential {
serial_number: _,
gateway_cosmos_address: _,
}) = from_binary::<ExecuteMsg>(msg)
{
return true;
}
}
false
}
@@ -0,0 +1,25 @@
/*
* Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
* SPDX-License-Identifier: Apache-2.0
*/
CREATE TABLE coin_indices_signatures
(
epoch_id TEXT NOT NULL PRIMARY KEY,
signatures TEXT NOT NULL
);
CREATE TABLE ecash_credentials
(
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
-- introduce a way for us to introduce breaking changes in serialization
serialization_revision INTEGER NOT NULL,
-- the best we can do without enums
credential_type TEXT CHECK ( credential_type IN ('TicketBook', 'FreeBandwidthPass') ) NOT NULL,
credential_data BLOB NOT NULL UNIQUE,
epoch_id INTEGER NOT NULL,
expired BOOLEAN NOT NULL,
consumed BOOLEAN NOT NULL
);
@@ -1,19 +1,20 @@
// Copyright 2023-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::models::{CredentialUsage, StoredIssuedCredential};
use crate::models::CoinIndicesSignature;
use crate::models::StoredIssuedCredential;
use std::sync::Arc;
use tokio::sync::RwLock;
#[derive(Clone)]
pub struct CoconutCredentialManager {
inner: Arc<RwLock<CoconutCredentialManagerInner>>,
coin_indices_sig: Arc<RwLock<Vec<CoinIndicesSignature>>>,
}
#[derive(Default)]
struct CoconutCredentialManagerInner {
credentials: Vec<StoredIssuedCredential>,
credential_usage: Vec<CredentialUsage>,
_next_id: i64,
}
@@ -30,6 +31,7 @@ impl CoconutCredentialManager {
pub fn new() -> Self {
CoconutCredentialManager {
inner: Default::default(),
coin_indices_sig: Default::default(),
}
}
@@ -49,56 +51,33 @@ impl CoconutCredentialManager {
credential_type,
epoch_id,
expired: false,
consumed: false,
})
}
async fn bandwidth_voucher_spent(&self, id: i64) -> bool {
self.inner
.read()
.await
.credential_usage
.iter()
.any(|c| c.credential_id == id)
}
async fn freepass_spent(&self, id: i64, gateway_id: &str) -> bool {
self.inner
.read()
.await
.credential_usage
.iter()
.any(|c| c.credential_id == id && c.gateway_id_bs58 == gateway_id)
}
/// Tries to retrieve one of the stored, unused credentials.
pub async fn get_next_unspect_bandwidth_voucher(&self) -> Option<StoredIssuedCredential> {
pub async fn get_next_unspent_ticketbook(&self) -> Option<StoredIssuedCredential> {
let guard = self.inner.read().await;
for credential in guard
.credentials
.iter()
.filter(|c| c.credential_type == "BandwidthVoucher")
.filter(|c| c.credential_type == "TicketBook")
{
if !self.bandwidth_voucher_spent(credential.id).await {
if !credential.consumed && !credential.expired {
return Some(credential.clone());
}
}
None
}
pub async fn get_next_unspect_freepass(
&self,
gateway_id: &str,
) -> Option<StoredIssuedCredential> {
pub async fn get_next_unspent_freepass(&self) -> 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 {
if !credential.consumed && !credential.expired {
return Some(credential.clone());
}
}
@@ -110,14 +89,55 @@ impl CoconutCredentialManager {
/// # Arguments
///
/// * `id`: Database id.
pub async fn consume_coconut_credential(&self, id: i64, gateway_id: &str) {
pub async fn consume_coconut_credential(&self, id: i64) {
let mut guard = self.inner.write().await;
guard.credential_usage.push(CredentialUsage {
credential_id: id,
gateway_id_bs58: gateway_id.to_string(),
if let Some(cred) = guard.credentials.get_mut(id as usize) {
cred.consumed = true;
}
}
pub async fn update_issued_credential(&self, credential_data: &[u8], id: i64, consumed: bool) {
let mut guard = self.inner.write().await;
if let Some(cred) = guard.credentials.get_mut(id as usize) {
cred.credential_data = credential_data.to_vec();
cred.consumed = consumed;
}
}
/// Inserts provided coin_indices_signatures into the database.
///
/// # Arguments
///
/// * `epoch_id`: Id of the epoch.
/// * `coin_indices_signatures` : The coin indices signatures for the epoch
pub async fn insert_coin_indices_sig(&self, epoch_id: String, coin_indices_sig: String) {
let mut signatures = self.coin_indices_sig.write().await;
signatures.push(CoinIndicesSignature {
epoch_id,
signatures: coin_indices_sig,
});
}
/// Check if coin indices signatures are present for a given epoch
///
/// # Arguments
///
/// * `epoch_id`: Id of the epoch.
pub async fn is_coin_indices_sig_present(&self, epoch_id: String) -> bool {
let sigs = self.coin_indices_sig.read().await;
sigs.iter().any(|s| s.epoch_id == epoch_id)
}
/// Get coin_indices_signatures of a given epoch.
///
/// # Arguments
///
/// * `epoch_id`: Id of the epoch.
pub async fn get_coin_indices_sig(&self, epoch_id: String) -> Option<CoinIndicesSignature> {
let sigs = self.coin_indices_sig.read().await;
sigs.iter().find(|s| s.epoch_id == epoch_id).cloned()
}
/// Marks the specified credential as expired
///
/// # Arguments
@@ -1,6 +1,7 @@
// Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::models::CoinIndicesSignature;
use crate::models::StoredIssuedCredential;
#[derive(Clone)]
@@ -27,50 +28,45 @@ impl CoconutCredentialManager {
) -> Result<(), sqlx::Error> {
sqlx::query!(
r#"
INSERT INTO coconut_credentials(serialization_revision, credential_type, credential_data, epoch_id, expired)
VALUES (?, ?, ?, ?, false)
INSERT INTO ecash_credentials(serialization_revision, credential_type, credential_data, epoch_id, expired, consumed)
VALUES (?, ?, ?, ?, false, false)
"#,
serialization_revision, credential_type, credential_data, epoch_id
).execute(&self.connection_pool).await?;
Ok(())
}
pub async fn get_next_unspect_freepass(
pub async fn get_next_unspent_freepass(
&self,
gateway_id: &str,
) -> Result<Option<StoredIssuedCredential>, sqlx::Error> {
// get a credential of freepass type that doesn't appear in `credential_usage` for the provided gateway_id
// get a credential of freepass type
sqlx::query_as(
r#"
SELECT *
FROM coconut_credentials
WHERE coconut_credentials.credential_type == "FreeBandwidthPass" AND coconut_credentials.expired = false
AND NOT EXISTS (SELECT 1
FROM credential_usage
WHERE credential_usage.credential_id = coconut_credentials.id
AND credential_usage.gateway_id_bs58 == ?)
ORDER BY coconut_credentials.id
FROM ecash_credentials
WHERE credential_type == "FreeBandwidthPass"
AND expired = false
AND consumed = false
ORDER BY id ASC
LIMIT 1
"#,
)
.bind(gateway_id)
.fetch_optional(&self.connection_pool)
.await
}
pub async fn get_next_unspect_bandwidth_voucher(
pub async fn get_next_unspent_ticketbook(
&self,
) -> Result<Option<StoredIssuedCredential>, sqlx::Error> {
// get a credential of bandwidth voucher type that doesn't appear in `credential_usage` for any gateway_id
// get a credential of bandwidth voucher type
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
FROM ecash_credentials
WHERE credential_type == "TicketBook"
AND expired = false
AND consumed = false
ORDER BY id ASC
LIMIT 1
"#,
)
@@ -84,21 +80,32 @@ impl CoconutCredentialManager {
///
/// * `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> {
pub async fn consume_coconut_credential(&self, id: i64) -> Result<(), sqlx::Error> {
sqlx::query!(
"INSERT INTO credential_usage (credential_id, gateway_id_bs58) VALUES (?, ?)",
id,
gateway_id
"UPDATE ecash_credentials SET consumed = TRUE WHERE id = ?",
id
)
.execute(&self.connection_pool)
.await?;
Ok(())
}
pub async fn update_issued_credential(
&self,
credential_data: &[u8],
id: i64,
consumed: bool,
) -> Result<(), sqlx::Error> {
sqlx::query!(
"UPDATE ecash_credentials SET credential_data = ?, consumed = ? WHERE id = ?",
credential_data,
consumed,
id
)
.execute(&self.connection_pool)
.await?;
Ok(())
}
/// Marks the specified credential as expired
///
/// # Arguments
@@ -106,11 +113,65 @@ impl CoconutCredentialManager {
/// * `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 = ?",
"UPDATE ecash_credentials SET expired = TRUE WHERE id = ?",
id
)
.execute(&self.connection_pool)
.await?;
Ok(())
}
/// Inserts provided coin_indices_signatures into the database.
///
/// # Arguments
///
/// * `epoch_id`: Id of the epoch.
/// * `coin_indices_signatures` : The coin indices signatures for the epoch
pub async fn insert_coin_indices_sig(
&self,
epoch_id: String,
coin_indices_sig: String,
) -> Result<(), sqlx::Error> {
sqlx::query!(
"INSERT INTO coin_indices_signatures(epoch_id, signatures) VALUES (?, ?)",
epoch_id,
coin_indices_sig
)
.execute(&self.connection_pool)
.await?;
Ok(())
}
/// Check if coin indices signatures are present for a given epoch
///
/// # Arguments
///
/// * `epoch_id`: Id of the epoch.
pub async fn is_coin_indices_sig_present(&self, epoch_id: String) -> Result<bool, sqlx::Error> {
sqlx::query!(
"SELECT epoch_id FROM coin_indices_signatures WHERE epoch_id = ?",
epoch_id
)
.fetch_optional(&self.connection_pool)
.await
.map(|r| r.is_some())
}
/// Get coin_indices_signatures of a given epoch.
///
/// # Arguments
///
/// * `epoch_id`: Id of the epoch.
pub async fn get_coin_indices_sig(
&self,
epoch_id: String,
) -> Result<Option<CoinIndicesSignature>, sqlx::Error> {
sqlx::query_as!(
CoinIndicesSignature,
"SELECT * FROM coin_indices_signatures WHERE epoch_id = ?",
epoch_id
)
.fetch_optional(&self.connection_pool)
.await
}
}
@@ -5,6 +5,7 @@ use std::fmt::{self, Debug, Formatter};
use crate::backends::memory::CoconutCredentialManager;
use crate::error::StorageError;
use crate::models::CoinIndicesSignature;
use crate::models::{StorableIssuedCredential, StoredIssuedCredential};
use crate::storage::Storage;
use async_trait::async_trait;
@@ -52,12 +53,11 @@ impl Storage for EphemeralStorage {
async fn get_next_unspent_credential(
&self,
gateway_id: &str,
) -> Result<Option<StoredIssuedCredential>, Self::StorageError> {
// first try to get a free pass if available, otherwise fallback to bandwidth voucher
let maybe_freepass = self
.coconut_credential_manager
.get_next_unspect_freepass(gateway_id)
.get_next_unspent_freepass()
.await;
if maybe_freepass.is_some() {
return Ok(maybe_freepass);
@@ -65,22 +65,58 @@ impl Storage for EphemeralStorage {
Ok(self
.coconut_credential_manager
.get_next_unspect_bandwidth_voucher()
.get_next_unspent_ticketbook()
.await)
}
async fn consume_coconut_credential(
&self,
id: i64,
gateway_id: &str,
) -> Result<(), StorageError> {
async fn consume_coconut_credential(&self, id: i64) -> Result<(), StorageError> {
self.coconut_credential_manager
.consume_coconut_credential(id, gateway_id)
.consume_coconut_credential(id)
.await;
Ok(())
}
async fn update_issued_credential<'a>(
&self,
bandwidth_credential: StorableIssuedCredential<'a>,
id: i64,
consumed: bool,
) -> Result<(), StorageError> {
self.coconut_credential_manager
.update_issued_credential(bandwidth_credential.credential_data, id, consumed)
.await;
Ok(())
}
async fn insert_coin_indices_sig(
&self,
epoch_id: String,
coin_indices_sig: String,
) -> Result<(), StorageError> {
self.coconut_credential_manager
.insert_coin_indices_sig(epoch_id, coin_indices_sig)
.await;
Ok(())
}
async fn is_coin_indices_sig_present(&self, epoch_id: String) -> Result<bool, StorageError> {
Ok(self
.coconut_credential_manager
.is_coin_indices_sig_present(epoch_id)
.await)
}
async fn get_coin_indices_sig(
&self,
epoch_id: String,
) -> Result<CoinIndicesSignature, StorageError> {
self.coconut_credential_manager
.get_coin_indices_sig(epoch_id)
.await
.ok_or(StorageError::NoSignatures)
}
async fn mark_expired(&self, id: i64) -> Result<(), Self::StorageError> {
self.coconut_credential_manager.mark_expired(id).await;
+3
View File
@@ -18,4 +18,7 @@ pub enum StorageError {
#[error("No unused credential in database. You need to buy at least one")]
NoCredential,
#[error("No signatures for that epoch in the database")]
NoSignatures,
}
+7
View File
@@ -27,6 +27,7 @@ pub struct StoredIssuedCredential {
pub epoch_id: u32,
pub expired: bool,
pub consumed: bool,
}
pub struct StorableIssuedCredential<'a> {
@@ -42,3 +43,9 @@ pub struct CredentialUsage {
pub credential_id: i64,
pub gateway_id_bs58: String,
}
#[derive(Clone)]
pub struct CoinIndicesSignature {
pub epoch_id: String,
pub signatures: String,
}
@@ -5,6 +5,7 @@ use crate::backends::sqlite::CoconutCredentialManager;
use crate::error::StorageError;
use crate::storage::Storage;
use crate::models::CoinIndicesSignature;
use crate::models::{StorableIssuedCredential, StoredIssuedCredential};
use async_trait::async_trait;
use log::{debug, error};
@@ -76,12 +77,11 @@ impl Storage for PersistentStorage {
async fn get_next_unspent_credential(
&self,
gateway_id: &str,
) -> Result<Option<StoredIssuedCredential>, Self::StorageError> {
// first try to get a free pass if available, otherwise fallback to bandwidth voucher
let maybe_freepass = self
.coconut_credential_manager
.get_next_unspect_freepass(gateway_id)
.get_next_unspent_freepass()
.await?;
if maybe_freepass.is_some() {
return Ok(maybe_freepass);
@@ -89,22 +89,62 @@ impl Storage for PersistentStorage {
Ok(self
.coconut_credential_manager
.get_next_unspect_bandwidth_voucher()
.get_next_unspent_ticketbook()
.await?)
}
async fn consume_coconut_credential(
&self,
id: i64,
gateway_id: &str,
) -> Result<(), StorageError> {
async fn consume_coconut_credential(&self, id: i64) -> Result<(), StorageError> {
self.coconut_credential_manager
.consume_coconut_credential(id, gateway_id)
.consume_coconut_credential(id)
.await?;
Ok(())
}
async fn update_issued_credential<'a>(
&self,
bandwidth_credential: StorableIssuedCredential<'a>,
id: i64,
consumed: bool,
) -> Result<(), Self::StorageError> {
self.coconut_credential_manager
.update_issued_credential(bandwidth_credential.credential_data, id, consumed)
.await?;
Ok(())
}
async fn insert_coin_indices_sig(
&self,
epoch_id: String,
coin_indices_sig: String,
) -> Result<(), StorageError> {
self.coconut_credential_manager
.insert_coin_indices_sig(epoch_id, coin_indices_sig)
.await?;
Ok(())
}
async fn is_coin_indices_sig_present(
&self,
epoch_id: String,
) -> Result<bool, Self::StorageError> {
Ok(self
.coconut_credential_manager
.is_coin_indices_sig_present(epoch_id)
.await?)
}
async fn get_coin_indices_sig(
&self,
epoch_id: String,
) -> Result<CoinIndicesSignature, StorageError> {
self.coconut_credential_manager
.get_coin_indices_sig(epoch_id)
.await?
.ok_or(StorageError::NoSignatures)
}
async fn mark_expired(&self, id: i64) -> Result<(), Self::StorageError> {
self.coconut_credential_manager.mark_expired(id).await?;
+45 -4
View File
@@ -1,6 +1,7 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::models::CoinIndicesSignature;
use crate::models::{StorableIssuedCredential, StoredIssuedCredential};
use async_trait::async_trait;
use std::error::Error;
@@ -18,7 +19,6 @@ pub trait Storage: Send + Sync {
/// that is also not marked as expired
async fn get_next_unspent_credential(
&self,
gateway_id: &str,
) -> Result<Option<StoredIssuedCredential>, Self::StorageError>;
/// Marks as consumed in the database the specified credential.
@@ -26,13 +26,54 @@ pub trait Storage: Send + Sync {
/// # Arguments
///
/// * `id`: Id of the credential to be consumed.
/// * `gateway_id`: id of the gateway that received the credential.
async fn consume_coconut_credential(
async fn consume_coconut_credential(&self, id: i64) -> Result<(), Self::StorageError>;
/// Update in the database the specified credential.
///
/// # Arguments
///
/// * `bandwidth_credential` : New credential
/// * `id`: Id of the credential to be updated.
/// * `consumed`: if the credential is consumed or not
///
async fn update_issued_credential<'a>(
&self,
bandwidth_credential: StorableIssuedCredential<'a>,
id: i64,
gateway_id: &str,
consumed: bool,
) -> Result<(), Self::StorageError>;
/// Inserts provided coin_indices_signatures into the database.
///
/// # Arguments
///
/// * `epoch_id`: Id of the epoch.
/// * `coin_indices_signatures` : The coin indices signatures for the epoch
async fn insert_coin_indices_sig(
&self,
epoch_id: String,
coin_indices_sig: String,
) -> Result<(), Self::StorageError>;
/// Check if coin indices signatures are present for a given epoch
///
/// # Arguments
///
/// * `epoch_id`: Id of the epoch.
async fn is_coin_indices_sig_present(
&self,
epoch_id: String,
) -> Result<bool, Self::StorageError>;
/// Get coin_indices_signatures of a given epoch.
///
/// # Arguments
///
/// * `epoch_id`: Id of the epoch.
async fn get_coin_indices_sig(
&self,
epoch_id: String,
) -> Result<CoinIndicesSignature, Self::StorageError>;
/// Marks the specified credential as expired
///
/// # Arguments
+1
View File
@@ -18,3 +18,4 @@ nym-credential-storage = { path = "../../common/credential-storage" }
nym-validator-client = { path = "../../common/client-libs/validator-client" }
nym-config = { path = "../../common/config" }
nym-client-core = { path = "../../common/client-core" }
nym-compact-ecash = { path = "../../common/nym_offline_compact_ecash" }
@@ -53,7 +53,7 @@ impl RecoveryStorage {
pub fn voucher_filename(voucher: &IssuanceBandwidthCredential) -> String {
let prefix = voucher.typ().to_string();
let suffix = voucher.blinded_serial_number_bs58();
let suffix = voucher.ecash_pubkey_bs58();
format!("{prefix}-{suffix}.{DUMPED_VOUCHER_EXTENSION}")
}
+18 -8
View File
@@ -7,9 +7,8 @@ use nym_config::DEFAULT_DATA_DIR;
use nym_credential_storage::persistent_storage::PersistentStorage;
use nym_credentials::coconut::bandwidth::CredentialType;
use nym_validator_client::nyxd::contract_traits::{
dkg_query_client::EpochState, CoconutBandwidthSigningClient, DkgQueryClient,
dkg_query_client::EpochState, DkgQueryClient, EcashSigningClient,
};
use nym_validator_client::nyxd::Coin;
use std::path::PathBuf;
use std::process::exit;
use std::time::{Duration, SystemTime};
@@ -18,12 +17,12 @@ const SAFETY_BUFFER_SECS: u64 = 60; // 1 minute
pub async fn issue_credential<C>(
client: &C,
amount: Coin,
client_id: &[u8],
persistent_storage: &PersistentStorage,
recovery_storage_path: PathBuf,
) -> Result<()>
where
C: DkgQueryClient + CoconutBandwidthSigningClient + Send + Sync,
C: DkgQueryClient + EcashSigningClient + Send + Sync,
{
let recovery_storage = setup_recovery_storage(recovery_storage_path).await;
@@ -42,7 +41,8 @@ where
}
};
let state = nym_bandwidth_controller::acquire::deposit(client, amount.clone()).await?;
let state = nym_bandwidth_controller::acquire::deposit(client, client_id).await?;
info!("Deposit done");
if nym_bandwidth_controller::acquire::get_bandwidth_voucher(&state, client, persistent_storage)
.await
@@ -63,7 +63,7 @@ where
));
}
info!("Succeeded adding a credential with amount {amount}");
info!("Succeeded adding a ticketbook credential");
Ok(())
}
@@ -130,15 +130,25 @@ where
let mut recovered_amount: u128 = 0;
for voucher in recovery_storage.unconsumed_vouchers()? {
let voucher_value = match voucher.typ() {
CredentialType::Voucher => voucher.get_bandwidth_attribute(),
CredentialType::TicketBook => voucher.value(),
CredentialType::Voucher => {
error!("Impossible to recover old coconut voucher");
continue;
}
CredentialType::FreePass => {
error!("unimplemented recovery of free pass credentials");
continue;
}
};
recovered_amount += voucher_value.parse::<u128>()?;
recovered_amount += voucher_value;
let voucher_name = RecoveryStorage::voucher_filename(&voucher);
if voucher.check_expiration_date() {
//We did change the expiration
warn!("Deposit {} was made with a different expiration date, it's validity will be shorter than the max one", voucher_name);
}
let state = State::new(voucher);
if let Err(e) =
+1
View File
@@ -15,4 +15,5 @@ bls12_381 = { workspace = true, default-features = false }
serde = { workspace = true, features = ["derive"] }
thiserror = { workspace = true }
nym-compact-ecash = { path = "../nym_offline_compact_ecash" }
nym-coconut = { path = "../nymcoconut" }
+182 -15
View File
@@ -1,21 +1,28 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use bls12_381::Scalar;
use serde::{Deserialize, Serialize};
use std::fmt::{Display, Formatter};
use std::str::FromStr;
use thiserror::Error;
pub use nym_coconut::{
aggregate_signature_shares, aggregate_verification_keys, blind_sign, hash_to_scalar, keygen,
prepare_blind_sign, prove_bandwidth_credential, verify_credential, Attribute, Base58,
BlindSignRequest, BlindedSerialNumber, BlindedSignature, Bytable, CoconutError, KeyPair,
Parameters, PrivateAttribute, PublicAttribute, SecretKey, Signature, SignatureShare,
VerificationKey, VerifyCredentialRequest,
pub use nym_compact_ecash::{
aggregate_verification_keys, aggregate_wallets, constants, error::CompactEcashError,
generate_keypair_user, generate_keypair_user_from_seed, issue_verify,
scheme::expiration_date_signatures::aggregate_expiration_signatures,
scheme::expiration_date_signatures::date_scalar,
scheme::expiration_date_signatures::ExpirationDateSignature,
scheme::expiration_date_signatures::PartialExpirationDateSignature,
scheme::keygen::KeyPairUser, scheme::setup::aggregate_indices_signatures,
scheme::setup::CoinIndexSignature, scheme::setup::PartialCoinIndexSignature,
scheme::withdrawal::RequestInfo, scheme::Payment, scheme::Wallet, setup::setup,
setup::Parameters, utils::BlindedSignature, withdrawal_request, Base58, Bytable,
GroupParameters, PartialWallet, PayInfo, PublicKeyUser, SecretKeyUser, VerificationKeyAuth,
WithdrawalRequest,
};
pub const VOUCHER_INFO_TYPE: &str = "BandwidthVoucher";
pub const ECASH_INFO_TYPE: &str = "TicketBook";
pub const FREE_PASS_INFO_TYPE: &str = "FreeBandwidthPass";
// pub trait NymCredential {
@@ -29,6 +36,7 @@ pub struct UnknownCredentialType(String);
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum CredentialType {
Voucher,
TicketBook,
FreePass,
}
@@ -36,10 +44,12 @@ impl FromStr for CredentialType {
type Err = UnknownCredentialType;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s == VOUCHER_INFO_TYPE {
Ok(CredentialType::Voucher)
if s == ECASH_INFO_TYPE {
Ok(CredentialType::TicketBook)
} else if s == FREE_PASS_INFO_TYPE {
Ok(CredentialType::FreePass)
} else if s == VOUCHER_INFO_TYPE {
Ok(CredentialType::Voucher)
} else {
Err(UnknownCredentialType(s.to_string()))
}
@@ -49,6 +59,7 @@ impl FromStr for CredentialType {
impl CredentialType {
pub fn validate(&self, type_plain: &str) -> bool {
match self {
CredentialType::TicketBook => type_plain == ECASH_INFO_TYPE,
CredentialType::Voucher => type_plain == VOUCHER_INFO_TYPE,
CredentialType::FreePass => type_plain == FREE_PASS_INFO_TYPE,
}
@@ -58,6 +69,10 @@ impl CredentialType {
matches!(self, CredentialType::FreePass)
}
pub fn is_ticketbook(&self) -> bool {
matches!(self, CredentialType::TicketBook)
}
pub fn is_voucher(&self) -> bool {
matches!(self, CredentialType::Voucher)
}
@@ -66,6 +81,7 @@ impl CredentialType {
impl Display for CredentialType {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
CredentialType::TicketBook => ECASH_INFO_TYPE.fmt(f),
CredentialType::Voucher => VOUCHER_INFO_TYPE.fmt(f),
CredentialType::FreePass => FREE_PASS_INFO_TYPE.fmt(f),
}
@@ -74,17 +90,168 @@ impl Display for CredentialType {
#[derive(Debug, Clone)]
pub struct CredentialSigningData {
pub pedersen_commitments_openings: Vec<Scalar>,
pub withdrawal_request: WithdrawalRequest,
pub blind_sign_request: BlindSignRequest,
pub request_info: RequestInfo,
pub public_attributes_plain: Vec<String>,
pub ecash_pub_key: PublicKeyUser,
pub expiration_date: u64,
pub typ: CredentialType,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
pub struct CredentialSpendingData {
pub payment: Payment,
pub pay_info: PayInfo,
pub spend_date: u64,
pub value: u64,
pub typ: CredentialType,
/// The (DKG) epoch id under which the credential has been issued so that the verifier could use correct verification key for validation.
pub epoch_id: u64,
}
impl CredentialSpendingData {
pub fn verify(
&self,
params: &Parameters,
verification_key: &VerificationKeyAuth,
) -> Result<bool, CompactEcashError> {
self.payment.spend_verify(
params,
verification_key,
&self.pay_info,
date_scalar(self.spend_date),
)
}
pub fn serial_number_b58(&self) -> String {
self.payment.serial_number_bs58()
}
pub fn to_bytes(&self) -> Vec<u8> {
// simple length prefixed serialization
// TODO: change it to a standard format instead
let mut bytes = Vec::new();
let payment_bytes = self.payment.to_bytes();
let typ = self.typ.to_string();
let typ_bytes = typ.as_bytes();
bytes.extend_from_slice(&(payment_bytes.len() as u32).to_be_bytes());
bytes.extend_from_slice(&payment_bytes);
bytes.extend_from_slice(&self.pay_info.pay_info_bytes); //this is 72 bytes long
bytes.extend_from_slice(&self.spend_date.to_be_bytes());
bytes.extend_from_slice(&self.value.to_be_bytes());
bytes.extend_from_slice(&(typ_bytes.len() as u32).to_be_bytes());
bytes.extend_from_slice(typ_bytes);
bytes.extend_from_slice(&self.epoch_id.to_be_bytes());
bytes
}
pub fn try_from_bytes(raw: &[u8]) -> Result<Self, CompactEcashError> {
if raw.len() < 72 + 8 + 8 + 8 + 4 + 4 {
return Err(CompactEcashError::Deserialization(
"Invalid byte array for EcashCredential deserialization".to_string(),
));
}
let mut index = 0;
//SAFETY : casting a slice of lenght 4 into an array of size 4
let payment_len = u32::from_be_bytes(raw[index..index + 4].try_into().unwrap()) as usize;
index += 4;
if raw[index..].len() < payment_len {
return Err(CompactEcashError::Deserialization(
"Invalid byte array for EcashCredential deserialization".to_string(),
));
}
let payment = Payment::try_from(&raw[index..index + payment_len])?;
index += payment_len;
if raw[index..].len() < 72 + 8 + 8 + 8 + 4 {
return Err(CompactEcashError::Deserialization(
"Invalid byte array for EcashCredential deserialization".to_string(),
));
}
let pay_info = PayInfo {
//SAFETY : casting a slice of lenght 72 into an array of size 72
pay_info_bytes: raw[index..index + 72].try_into().unwrap(),
};
index += 72;
//SAFETY : casting a slice of lenght 8 into an array of size 8
let spend_date = u64::from_be_bytes(raw[index..index + 8].try_into().unwrap());
index += 8;
//SAFETY : casting a slice of lenght 8 into an array of size 8
let value = u64::from_be_bytes(raw[index..index + 8].try_into().unwrap());
index += 8;
//SAFETY : casting a slice of lenght 4 into an array of size 4
let typ_len = u32::from_be_bytes(raw[index..index + 4].try_into().unwrap()) as usize;
index += 4;
if raw[index..].len() != typ_len + 8 {
return Err(CompactEcashError::Deserialization(
"Invalid byte array for EcashCredential deserialization".to_string(),
));
}
let raw_typ = String::from_utf8(raw[index..index + typ_len].to_vec()).map_err(|_| {
CompactEcashError::Deserialization("Failed to deserialize type".to_string())
})?;
let typ = raw_typ.parse().map_err(|_| {
CompactEcashError::Deserialization("Failed to deserialize type".to_string())
})?;
index += typ_len;
//SAFETY : casting a slice of lenght 8 into an array of size 8
let epoch_id = u64::from_be_bytes(raw[index..index + 8].try_into().unwrap());
Ok(CredentialSpendingData {
payment,
pay_info,
spend_date,
value,
typ,
epoch_id,
})
}
}
impl Bytable for CredentialSpendingData {
fn to_byte_vec(&self) -> Vec<u8> {
self.to_bytes()
}
fn try_from_byte_slice(slice: &[u8]) -> Result<Self, CompactEcashError> {
Self::try_from_bytes(slice)
}
}
impl Base58 for CredentialSpendingData {}
pub use nym_coconut::{
hash_to_scalar, keygen as coconut_keygen, prove_bandwidth_credential, verify_credential,
Attribute, Base58 as CoconutBase58, BlindedSerialNumber, CoconutError,
Parameters as CoconutParameters, Signature as CoconutSignature, VerificationKey,
VerifyCredentialRequest,
};
//SW NOTE: for coconut compatibility
pub fn to_coconut(verification_key: &VerificationKeyAuth) -> Result<VerificationKey, CoconutError> {
VerificationKey::from_bytes(&verification_key.to_bytes())
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
pub struct OldCredentialSpendingData {
pub embedded_private_attributes: usize,
pub verify_credential_request: VerifyCredentialRequest,
@@ -97,8 +264,8 @@ pub struct CredentialSpendingData {
pub epoch_id: u64,
}
impl CredentialSpendingData {
pub fn verify(&self, params: &Parameters, verification_key: &VerificationKey) -> bool {
impl OldCredentialSpendingData {
pub fn verify(&self, params: &CoconutParameters, verification_key: &VerificationKey) -> bool {
let hashed_public_attributes = self
.public_attributes_plain
.iter()
+3 -1
View File
@@ -18,9 +18,11 @@ zeroize = { workspace = true }
# I guess temporarily until we get serde support in coconut up and running
nym-credentials-interface = { path = "../credentials-interface" }
nym-crypto = { path = "../crypto", features = ["rand", "asymmetric", "serde"] }
chrono = "0.4.38"
nym-crypto = { path = "../crypto", features = ["rand", "asymmetric", "symmetric", "hashing"] }
nym-api-requests = { path = "../../nym-api/nym-api-requests" }
nym-validator-client = { path = "../client-libs/validator-client", default-features = false }
nym-ecash-contract-common = { path = "../cosmwasm-smart-contracts/ecash-contract" }
[dev-dependencies]
rand = "0.7.3"
@@ -1,97 +1,20 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::coconut::utils::scalar_serde_helper;
use crate::error::Error;
use nym_api_requests::coconut::FreePassRequest;
use nym_credentials_interface::{
hash_to_scalar, Attribute, BlindedSignature, CredentialSigningData, PublicAttribute,
};
use nym_credentials_interface::{BlindedSignature, CredentialSigningData};
use nym_validator_client::signing::AccountData;
use serde::{Deserialize, Serialize};
use time::{Duration, OffsetDateTime, Time};
use zeroize::{Zeroize, ZeroizeOnDrop};
pub const MAX_FREE_PASS_VALIDITY: Duration = Duration::WEEK; // 1 week
#[derive(Debug, Zeroize, ZeroizeOnDrop, Serialize, Deserialize)]
pub struct FreePassIssuedData {
/// the plain validity value of this credential expressed as unix timestamp
#[zeroize(skip)]
expiry_date: OffsetDateTime,
}
pub struct FreePassIssuedData {}
impl<'a> From<&'a FreePassIssuanceData> for FreePassIssuedData {
fn from(value: &'a FreePassIssuanceData) -> Self {
FreePassIssuedData {
expiry_date: value.expiry_date,
}
}
}
impl FreePassIssuedData {
pub fn expired(&self) -> bool {
self.expiry_date <= OffsetDateTime::now_utc()
}
pub fn expiry_date(&self) -> OffsetDateTime {
self.expiry_date
}
pub fn expiry_date_plain(&self) -> String {
self.expiry_date.unix_timestamp().to_string()
}
}
#[derive(Zeroize, Serialize, Deserialize)]
pub struct FreePassIssuanceData {
/// the plain validity value of this credential expressed as unix timestamp
#[zeroize(skip)]
expiry_date: OffsetDateTime,
// the expiry date, as unix timestamp, hashed into a scalar
#[serde(with = "scalar_serde_helper")]
expiry_date_prehashed: PublicAttribute,
}
pub struct FreePassIssuanceData {}
impl FreePassIssuanceData {
pub fn new(expiry_date: Option<OffsetDateTime>) -> Self {
// ideally we should have implemented a proper error handling here, sure.
// but given it's meant to only be used by nym, imo it's fine to just panic here in case of invalid arguments
let expiry_date = if let Some(provided) = expiry_date {
if provided - OffsetDateTime::now_utc() > MAX_FREE_PASS_VALIDITY {
panic!("the provided expiry date is bigger than the maximum value of {MAX_FREE_PASS_VALIDITY}");
}
provided
} else {
Self::default_expiry_date()
};
let expiry_date_prehashed = hash_to_scalar(expiry_date.unix_timestamp().to_string());
FreePassIssuanceData {
expiry_date,
expiry_date_prehashed,
}
}
pub fn default_expiry_date() -> OffsetDateTime {
// set it to furthest midnight in the future such as it's no more than a week away,
// i.e. if it's currently for example 9:43 on 2nd March 2024, it will set it to 0:00 on 9th March 2024
(OffsetDateTime::now_utc() + MAX_FREE_PASS_VALIDITY).replace_time(Time::MIDNIGHT)
}
pub fn expiry_date_attribute(&self) -> &Attribute {
&self.expiry_date_prehashed
}
pub fn expiry_date_plain(&self) -> String {
self.expiry_date.unix_timestamp().to_string()
}
pub async fn obtain_free_pass_nonce(
&self,
client: &nym_validator_client::client::NymApiClient,
) -> Result<[u8; 16], Error> {
let server_response = client.free_pass_nonce().await?;
@@ -99,7 +22,6 @@ impl FreePassIssuanceData {
}
pub fn create_free_pass_request(
&self,
signing_request: &CredentialSigningData,
account_data: &AccountData,
issuer_nonce: [u8; 16],
@@ -111,15 +33,15 @@ impl FreePassIssuanceData {
Ok(FreePassRequest {
cosmos_pubkey: account_data.public_key(),
inner_sign_request: signing_request.blind_sign_request.clone(),
inner_sign_request: signing_request.withdrawal_request.clone(),
used_nonce: issuer_nonce,
nonce_signature,
public_attributes_plain: signing_request.public_attributes_plain.clone(),
ecash_pubkey: signing_request.ecash_pub_key.clone(),
expiration_date: signing_request.expiration_date,
})
}
pub async fn obtain_blinded_credential(
&self,
client: &nym_validator_client::client::NymApiClient,
request: &FreePassRequest,
) -> Result<BlindedSignature, Error> {
@@ -128,14 +50,12 @@ impl FreePassIssuanceData {
}
pub async fn request_blinded_credential(
&self,
signing_request: &CredentialSigningData,
account_data: &AccountData,
client: &nym_validator_client::client::NymApiClient,
) -> Result<BlindedSignature, Error> {
let signing_nonce = self.obtain_free_pass_nonce(client).await?;
let request =
self.create_free_pass_request(signing_request, account_data, signing_nonce)?;
self.obtain_blinded_credential(client, &request).await
let signing_nonce = Self::obtain_free_pass_nonce(client).await?;
let request = Self::create_free_pass_request(signing_request, account_data, signing_nonce)?;
Self::obtain_blinded_credential(client, &request).await
}
}
@@ -7,70 +7,44 @@ use crate::coconut::bandwidth::voucher::BandwidthVoucherIssuanceData;
use crate::coconut::bandwidth::{
bandwidth_credential_params, CredentialSigningData, CredentialType,
};
use crate::coconut::utils::scalar_serde_helper;
use crate::coconut::utils::{cred_exp_date_timestamp, freepass_exp_date_timestamp};
use crate::error::Error;
use log::error;
use nym_credentials_interface::{
aggregate_signature_shares, hash_to_scalar, prepare_blind_sign, Attribute, BlindedSerialNumber,
BlindedSignature, Parameters, PrivateAttribute, PublicAttribute, Signature, SignatureShare,
VerificationKey,
aggregate_wallets, constants, generate_keypair_user, generate_keypair_user_from_seed,
issue_verify, setup, withdrawal_request, BlindedSignature, ExpirationDateSignature,
KeyPairUser, Parameters, PartialWallet, VerificationKeyAuth, Wallet,
};
use nym_crypto::asymmetric::{encryption, identity};
use nym_validator_client::nym_api::EpochId;
use nym_validator_client::nyxd::{Coin, Hash};
use nym_validator_client::nyxd::Hash;
use nym_validator_client::signing::AccountData;
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
use zeroize::{Zeroize, ZeroizeOnDrop};
#[derive(Zeroize, ZeroizeOnDrop, Serialize, Deserialize)]
pub enum BandwidthCredentialIssuanceDataVariant {
Voucher(BandwidthVoucherIssuanceData),
FreePass(FreePassIssuanceData),
}
impl From<FreePassIssuanceData> for BandwidthCredentialIssuanceDataVariant {
fn from(value: FreePassIssuanceData) -> Self {
BandwidthCredentialIssuanceDataVariant::FreePass(value)
}
TicketBook(BandwidthVoucherIssuanceData),
FreePass,
}
impl From<BandwidthVoucherIssuanceData> for BandwidthCredentialIssuanceDataVariant {
fn from(value: BandwidthVoucherIssuanceData) -> Self {
BandwidthCredentialIssuanceDataVariant::Voucher(value)
BandwidthCredentialIssuanceDataVariant::TicketBook(value)
}
}
impl BandwidthCredentialIssuanceDataVariant {
pub fn info(&self) -> CredentialType {
match self {
BandwidthCredentialIssuanceDataVariant::Voucher(..) => CredentialType::Voucher,
BandwidthCredentialIssuanceDataVariant::FreePass(..) => CredentialType::FreePass,
}
}
// currently this works under the assumption of there being a single unique public attribute for given variant
pub fn public_value(&self) -> &Attribute {
match self {
BandwidthCredentialIssuanceDataVariant::Voucher(voucher) => voucher.value_attribute(),
BandwidthCredentialIssuanceDataVariant::FreePass(freepass) => {
freepass.expiry_date_attribute()
}
}
}
// currently this works under the assumption of there being a single unique public attribute for given variant
pub fn public_value_plain(&self) -> String {
match self {
BandwidthCredentialIssuanceDataVariant::Voucher(voucher) => voucher.value_plain(),
BandwidthCredentialIssuanceDataVariant::FreePass(freepass) => {
freepass.expiry_date_plain()
}
BandwidthCredentialIssuanceDataVariant::TicketBook(..) => CredentialType::TicketBook,
BandwidthCredentialIssuanceDataVariant::FreePass => CredentialType::FreePass,
}
}
pub fn voucher_data(&self) -> Option<&BandwidthVoucherIssuanceData> {
match self {
BandwidthCredentialIssuanceDataVariant::Voucher(voucher) => Some(voucher),
BandwidthCredentialIssuanceDataVariant::TicketBook(voucher) => Some(voucher),
_ => None,
}
}
@@ -79,102 +53,103 @@ impl BandwidthCredentialIssuanceDataVariant {
// all types of bandwidth credentials contain serial number and binding number
#[derive(Zeroize, ZeroizeOnDrop, Serialize, Deserialize)]
pub struct IssuanceBandwidthCredential {
// private attributes
/// a random secret value generated by the client used for double-spending detection
#[serde(with = "scalar_serde_helper")]
serial_number: PrivateAttribute,
/// a random secret value generated by the client used to bind multiple credentials together
#[serde(with = "scalar_serde_helper")]
binding_number: PrivateAttribute,
/// data specific to given bandwidth credential, for example a value for bandwidth voucher and expiry date for the free pass
variant_data: BandwidthCredentialIssuanceDataVariant,
/// type of the bandwdith credential hashed onto a scalar
#[serde(with = "scalar_serde_helper")]
type_prehashed: PublicAttribute,
///ecash keypair related to the credential
ecash_keypair: KeyPairUser,
///expiration_date of that credential
expiration_date: u64,
}
impl IssuanceBandwidthCredential {
pub const PUBLIC_ATTRIBUTES: u32 = 2;
pub const PRIVATE_ATTRIBUTES: u32 = 2;
pub const ENCODED_ATTRIBUTES: u32 = Self::PUBLIC_ATTRIBUTES + Self::PRIVATE_ATTRIBUTES;
pub fn default_parameters() -> Parameters {
// safety: the unwrap is fine here as Self::ENCODED_ATTRIBUTES is non-zero
Parameters::new(Self::ENCODED_ATTRIBUTES).unwrap()
setup(constants::NB_TICKETS)
}
pub fn new<B: Into<BandwidthCredentialIssuanceDataVariant>>(variant_data: B) -> Self {
pub fn new<B: Into<BandwidthCredentialIssuanceDataVariant>>(
variant_data: B,
identifier: Option<&[u8]>,
expiration_date: u64,
) -> Self {
let variant_data = variant_data.into();
let type_prehashed = hash_to_scalar(variant_data.info().to_string());
let params = bandwidth_credential_params();
let serial_number = params.random_scalar();
let binding_number = params.random_scalar();
let params = bandwidth_credential_params().grp();
let ecash_keypair = if let Some(id) = identifier {
generate_keypair_user_from_seed(params, id)
} else {
generate_keypair_user(params)
};
IssuanceBandwidthCredential {
serial_number,
binding_number,
variant_data,
type_prehashed,
ecash_keypair,
expiration_date,
}
}
pub fn new_voucher(
value: impl Into<Coin>,
deposit_tx_hash: Hash,
identifier: &[u8],
signing_key: identity::PrivateKey,
unused_ed25519: encryption::PrivateKey,
) -> Self {
Self::new(BandwidthVoucherIssuanceData::new(
value,
deposit_tx_hash,
signing_key,
unused_ed25519,
))
Self::new(
BandwidthVoucherIssuanceData::new(deposit_tx_hash, signing_key, unused_ed25519),
Some(identifier),
cred_exp_date_timestamp(),
)
}
pub fn new_freepass(expiry_date: Option<OffsetDateTime>) -> Self {
Self::new(FreePassIssuanceData::new(expiry_date))
pub fn new_freepass(timestamp: u64) -> Self {
let exp_timestamp = if timestamp > freepass_exp_date_timestamp() {
error!(
"the provided free pass request has too long expiry, setting it to max possible"
);
freepass_exp_date_timestamp()
} else {
timestamp
};
Self::new(
BandwidthCredentialIssuanceDataVariant::FreePass,
None,
exp_timestamp,
)
}
pub fn blind_serial_number(&self) -> BlindedSerialNumber {
(bandwidth_credential_params().gen2() * self.serial_number).into()
}
pub fn blinded_serial_number_bs58(&self) -> String {
pub fn ecash_pubkey_bs58(&self) -> String {
use nym_credentials_interface::Base58;
self.blind_serial_number().to_bs58()
self.ecash_keypair.public_key().to_bs58()
}
pub fn typ(&self) -> CredentialType {
self.variant_data.info()
}
pub fn get_private_attributes(&self) -> Vec<&PrivateAttribute> {
vec![&self.serial_number, &self.binding_number]
}
pub fn get_public_attributes(&self) -> Vec<&PublicAttribute> {
vec![self.variant_data.public_value(), &self.type_prehashed]
}
pub fn get_plain_public_attributes(&self) -> Vec<String> {
vec![
self.variant_data.public_value_plain(),
self.typ().to_string(),
]
pub fn expiration_date(&self) -> u64 {
self.expiration_date
}
pub fn get_variant_data(&self) -> &BandwidthCredentialIssuanceDataVariant {
&self.variant_data
}
pub fn get_bandwidth_attribute(&self) -> String {
self.variant_data.public_value_plain()
pub fn value(&self) -> u128 {
if let BandwidthCredentialIssuanceDataVariant::TicketBook(data) = &self.variant_data {
data.value()
} else {
0_u128
}
}
pub fn check_expiration_date(&self) -> bool {
let old_expiration_date = self.expiration_date;
let new_expiration_date = match self.get_variant_data() {
BandwidthCredentialIssuanceDataVariant::TicketBook(_) => cred_exp_date_timestamp(),
BandwidthCredentialIssuanceDataVariant::FreePass => freepass_exp_date_timestamp(),
};
old_expiration_date != new_expiration_date
}
pub fn prepare_for_signing(&self) -> CredentialSigningData {
@@ -182,38 +157,37 @@ impl IssuanceBandwidthCredential {
// safety: the creation of the request can only fail if one provided invalid parameters
// and we created then specific to this type of the credential so the unwrap is fine
let (pedersen_commitments_openings, blind_sign_request) = prepare_blind_sign(
params,
&[&self.serial_number, &self.binding_number],
&self.get_public_attributes(),
let (withdrawal_request, request_info) = withdrawal_request(
params.grp(),
&self.ecash_keypair.secret_key(),
self.expiration_date,
)
.unwrap();
CredentialSigningData {
pedersen_commitments_openings,
blind_sign_request,
public_attributes_plain: self.get_plain_public_attributes(),
withdrawal_request,
request_info,
ecash_pub_key: self.ecash_keypair.public_key(),
typ: self.typ(),
expiration_date: self.expiration_date,
}
}
pub fn unblind_signature(
&self,
validator_vk: &VerificationKey,
validator_vk: &VerificationKeyAuth,
signing_data: &CredentialSigningData,
blinded_signature: BlindedSignature,
) -> Result<Signature, Error> {
let public_attributes = self.get_public_attributes();
let private_attributes = self.get_private_attributes();
let params = bandwidth_credential_params();
let unblinded_signature = blinded_signature.unblind_and_verify(
signer_index: u64,
) -> Result<PartialWallet, Error> {
let params = bandwidth_credential_params().grp();
let unblinded_signature = issue_verify(
params,
validator_vk,
&private_attributes,
&public_attributes,
&signing_data.blind_sign_request.get_commitment_hash(),
&signing_data.pedersen_commitments_openings,
&self.ecash_keypair.secret_key(),
&blinded_signature,
&signing_data.request_info,
signer_index,
)?;
Ok(unblinded_signature)
@@ -222,88 +196,88 @@ impl IssuanceBandwidthCredential {
pub async fn obtain_partial_freepass_credential(
&self,
client: &nym_validator_client::client::NymApiClient,
signer_index: u64,
account_data: &AccountData,
validator_vk: &VerificationKey,
signing_data: impl Into<Option<CredentialSigningData>>,
) -> Result<Signature, Error> {
// if we provided signing data, do use them, otherwise generate fresh data
let signing_data = signing_data
.into()
.unwrap_or_else(|| self.prepare_for_signing());
validator_vk: &VerificationKeyAuth,
signing_data: CredentialSigningData,
) -> Result<PartialWallet, Error> {
// We need signing data, because they will be use at the aggregation step
let blinded_signature = match &self.variant_data {
BandwidthCredentialIssuanceDataVariant::FreePass(freepass) => {
freepass
.request_blinded_credential(&signing_data, account_data, client)
.await?
BandwidthCredentialIssuanceDataVariant::FreePass => {
FreePassIssuanceData::request_blinded_credential(
&signing_data,
account_data,
client,
)
.await?
}
_ => return Err(Error::NotAFreePass),
};
self.unblind_signature(validator_vk, &signing_data, blinded_signature)
self.unblind_signature(validator_vk, &signing_data, blinded_signature, signer_index)
}
// ideally this would have been generic over credential type, but we really don't need secp256k1 keys for bandwidth vouchers
pub async fn obtain_partial_bandwidth_voucher_credential(
&self,
client: &nym_validator_client::client::NymApiClient,
validator_vk: &VerificationKey,
signing_data: impl Into<Option<CredentialSigningData>>,
) -> Result<Signature, Error> {
// if we provided signing data, do use them, otherwise generate fresh data
let signing_data = signing_data
.into()
.unwrap_or_else(|| self.prepare_for_signing());
signer_index: u64,
validator_vk: &VerificationKeyAuth,
signing_data: CredentialSigningData,
) -> Result<PartialWallet, Error> {
// We need signing data, because they will be use at the aggregation step
let blinded_signature = match &self.variant_data {
BandwidthCredentialIssuanceDataVariant::Voucher(voucher) => {
BandwidthCredentialIssuanceDataVariant::TicketBook(voucher) => {
// TODO: the request can be re-used between different apis
let request = voucher.create_blind_sign_request_body(&signing_data);
voucher.obtain_blinded_credential(client, &request).await?
}
_ => return Err(Error::NotABandwdithVoucher),
};
self.unblind_signature(validator_vk, &signing_data, blinded_signature)
self.unblind_signature(validator_vk, &signing_data, blinded_signature, signer_index)
}
pub fn aggregate_signature_shares(
&self,
verification_key: &VerificationKey,
shares: &[SignatureShare],
) -> Result<Signature, Error> {
let public_attributes = self.get_public_attributes();
let private_attributes = self.get_private_attributes();
let params = bandwidth_credential_params();
let mut attributes = Vec::with_capacity(private_attributes.len() + public_attributes.len());
attributes.extend_from_slice(&private_attributes);
attributes.extend_from_slice(&public_attributes);
aggregate_signature_shares(params, verification_key, &attributes, shares)
.map_err(Error::SignatureAggregationError)
verification_key: &VerificationKeyAuth,
shares: &[PartialWallet],
signing_data: CredentialSigningData,
) -> Result<Wallet, Error> {
let params = bandwidth_credential_params().grp();
aggregate_wallets(
params,
verification_key,
&self.ecash_keypair.secret_key(),
shares,
&signing_data.request_info,
)
.map_err(Error::SignatureAggregationError)
}
// also drops self after the conversion
pub fn into_issued_credential(
self,
aggregate_signature: Signature,
wallet: Wallet,
exp_date_signatures: Vec<ExpirationDateSignature>,
epoch_id: EpochId,
) -> IssuedBandwidthCredential {
self.to_issued_credential(aggregate_signature, epoch_id)
self.to_issued_credential(wallet, exp_date_signatures, epoch_id)
}
pub fn to_issued_credential(
&self,
aggregate_signature: Signature,
wallet: Wallet,
exp_date_signatures: Vec<ExpirationDateSignature>,
epoch_id: EpochId,
) -> IssuedBandwidthCredential {
IssuedBandwidthCredential::new(
self.serial_number,
self.binding_number,
aggregate_signature,
wallet,
(&self.variant_data).into(),
self.type_prehashed,
epoch_id,
self.ecash_keypair.secret_key(),
exp_date_signatures,
self.expiration_date,
)
}
@@ -2,70 +2,54 @@
// SPDX-License-Identifier: Apache-2.0
use crate::coconut::bandwidth::bandwidth_credential_params;
use crate::coconut::bandwidth::freepass::FreePassIssuedData;
use crate::coconut::bandwidth::issuance::{
BandwidthCredentialIssuanceDataVariant, IssuanceBandwidthCredential,
};
use crate::coconut::bandwidth::voucher::BandwidthVoucherIssuedData;
use crate::coconut::bandwidth::{CredentialSpendingData, CredentialType};
use crate::coconut::utils::scalar_serde_helper;
use crate::coconut::utils::today_timestamp;
use crate::error::Error;
use nym_credentials_interface::prove_bandwidth_credential;
use nym_credentials_interface::{
Parameters, PrivateAttribute, PublicAttribute, Signature, VerificationKey,
constants, date_scalar, CoinIndexSignature, ExpirationDateSignature, Parameters, PayInfo,
SecretKeyUser, VerificationKeyAuth, Wallet,
};
use nym_validator_client::nym_api::EpochId;
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
use zeroize::{Zeroize, ZeroizeOnDrop};
pub const CURRENT_SERIALIZATION_REVISION: u8 = 1;
#[derive(Debug, Zeroize, Serialize, Deserialize)]
pub enum BandwidthCredentialIssuedDataVariant {
Voucher(BandwidthVoucherIssuedData),
FreePass(FreePassIssuedData),
TicketBook(BandwidthVoucherIssuedData),
FreePass,
}
impl<'a> From<&'a BandwidthCredentialIssuanceDataVariant> for BandwidthCredentialIssuedDataVariant {
fn from(value: &'a BandwidthCredentialIssuanceDataVariant) -> Self {
match value {
BandwidthCredentialIssuanceDataVariant::Voucher(voucher) => {
BandwidthCredentialIssuedDataVariant::Voucher(voucher.into())
BandwidthCredentialIssuanceDataVariant::TicketBook(voucher) => {
BandwidthCredentialIssuedDataVariant::TicketBook(voucher.into())
}
BandwidthCredentialIssuanceDataVariant::FreePass(freepass) => {
BandwidthCredentialIssuedDataVariant::FreePass(freepass.into())
BandwidthCredentialIssuanceDataVariant::FreePass => {
BandwidthCredentialIssuedDataVariant::FreePass
}
}
}
}
impl From<FreePassIssuedData> for BandwidthCredentialIssuedDataVariant {
fn from(value: FreePassIssuedData) -> Self {
BandwidthCredentialIssuedDataVariant::FreePass(value)
}
}
impl From<BandwidthVoucherIssuedData> for BandwidthCredentialIssuedDataVariant {
fn from(value: BandwidthVoucherIssuedData) -> Self {
BandwidthCredentialIssuedDataVariant::Voucher(value)
BandwidthCredentialIssuedDataVariant::TicketBook(value)
}
}
impl BandwidthCredentialIssuedDataVariant {
pub fn info(&self) -> CredentialType {
match self {
BandwidthCredentialIssuedDataVariant::Voucher(..) => CredentialType::Voucher,
BandwidthCredentialIssuedDataVariant::FreePass(..) => CredentialType::FreePass,
}
}
// currently this works under the assumption of there being a single unique public attribute for given variant
pub fn public_value_plain(&self) -> String {
match self {
BandwidthCredentialIssuedDataVariant::Voucher(voucher) => voucher.value_plain(),
BandwidthCredentialIssuedDataVariant::FreePass(freepass) => {
freepass.expiry_date_plain()
}
BandwidthCredentialIssuedDataVariant::TicketBook(..) => CredentialType::TicketBook,
BandwidthCredentialIssuedDataVariant::FreePass => CredentialType::FreePass,
}
}
}
@@ -73,46 +57,42 @@ impl BandwidthCredentialIssuedDataVariant {
// the only important thing to zeroize here are the private attributes, the rest can be made fully public for what we're concerned
#[derive(Zeroize, ZeroizeOnDrop, Serialize, Deserialize)]
pub struct IssuedBandwidthCredential {
// private attributes
/// a random secret value generated by the client used for double-spending detection
#[serde(with = "scalar_serde_helper")]
serial_number: PrivateAttribute,
/// a random secret value generated by the client used to bind multiple credentials together
#[serde(with = "scalar_serde_helper")]
binding_number: PrivateAttribute,
/// the underlying aggregated signature on the attributes
#[zeroize(skip)]
signature: Signature,
/// the underlying wallet
wallet: Wallet,
/// data specific to given bandwidth credential, for example a value for bandwidth voucher and expiry date for the free pass
variant_data: BandwidthCredentialIssuedDataVariant,
/// type of the bandwdith credential hashed onto a scalar
#[serde(with = "scalar_serde_helper")]
type_prehashed: PublicAttribute,
variant_data: BandwidthCredentialIssuedDataVariant, //SW NOTE: freepass has no info, maybe put value directly here
/// Specifies the (DKG) epoch id when this credential has been issued
epoch_id: EpochId,
///secret ecash key used to generate this wallet
ecash_secret_key: SecretKeyUser,
///signatures on expiration dates used to spend tickets
#[zeroize(skip)]
exp_date_signatures: Vec<ExpirationDateSignature>,
///expiration_date for easier discarding
expiration_date: u64,
}
impl IssuedBandwidthCredential {
pub fn new(
serial_number: PrivateAttribute,
binding_number: PrivateAttribute,
signature: Signature,
wallet: Wallet,
variant_data: BandwidthCredentialIssuedDataVariant,
type_prehashed: PublicAttribute,
epoch_id: EpochId,
ecash_secret_key: SecretKeyUser,
exp_date_signatures: Vec<ExpirationDateSignature>,
expiration_date: u64,
) -> Self {
IssuedBandwidthCredential {
serial_number,
binding_number,
signature,
wallet,
variant_data,
type_prehashed,
epoch_id,
ecash_secret_key,
exp_date_signatures,
expiration_date,
}
}
@@ -137,6 +117,27 @@ impl IssuedBandwidthCredential {
CURRENT_SERIALIZATION_REVISION
}
pub fn expiration_date(&self) -> u64 {
self.expiration_date
}
pub fn expiration_date_formatted(&self) -> OffsetDateTime {
//SAFETY : expiration date is encoded as a u64 but it is a unix timestamp. The unwrap is guaranteed to succeed for at least 290 million more years
OffsetDateTime::from_unix_timestamp(self.expiration_date.try_into().unwrap()).unwrap()
}
pub fn expired(&self) -> bool {
self.expiration_date < today_timestamp()
}
pub fn exp_date_sigs(&self) -> Vec<ExpirationDateSignature> {
self.exp_date_signatures.clone()
}
pub fn wallet(&self) -> &Wallet {
&self.wallet
}
/// Pack (serialize) this credential data into a stream of bytes using v1 serializer.
pub fn pack_v1(&self) -> Vec<u8> {
use bincode::Options;
@@ -155,11 +156,6 @@ impl IssuedBandwidthCredential {
})
}
pub fn randomise_signature(&mut self) {
let signature_prime = self.signature.randomise(bandwidth_credential_params());
self.signature = signature_prime.0
}
pub fn default_parameters() -> Parameters {
IssuanceBandwidthCredential::default_parameters()
}
@@ -168,31 +164,38 @@ impl IssuedBandwidthCredential {
self.variant_data.info()
}
pub fn get_plain_public_attributes(&self) -> Vec<String> {
vec![
self.variant_data.public_value_plain(),
self.typ().to_string(),
]
}
pub fn prepare_for_spending(
&self,
verification_key: &VerificationKey,
verification_key: &VerificationKeyAuth,
pay_info: PayInfo,
coin_indices_signatures: Vec<CoinIndexSignature>,
) -> Result<CredentialSpendingData, Error> {
let params = bandwidth_credential_params();
let verify_credential_request = prove_bandwidth_credential(
let spend_date = today_timestamp();
let (payment, _) = self.wallet.spend(
params,
verification_key,
&self.signature,
&self.serial_number,
&self.binding_number,
&self.ecash_secret_key,
&pay_info,
false,
constants::SPEND_TICKETS,
self.exp_date_sigs(),
coin_indices_signatures,
date_scalar(spend_date),
)?;
let value = match &self.variant_data {
BandwidthCredentialIssuedDataVariant::FreePass => 0u64,
BandwidthCredentialIssuedDataVariant::TicketBook(voucher) => {
constants::SPEND_TICKETS * voucher.value() as u64 / params.get_total_coins()
}
};
Ok(CredentialSpendingData {
embedded_private_attributes: IssuanceBandwidthCredential::PRIVATE_ATTRIBUTES as usize,
verify_credential_request,
public_attributes_plain: self.get_plain_public_attributes(),
payment,
pay_info,
spend_date,
value,
typ: self.typ(),
epoch_id: self.epoch_id,
})
@@ -15,7 +15,6 @@ pub mod issuance;
pub mod issued;
pub mod voucher;
// works under the assumption of having 4 attributes in the underlying credential(s)
pub fn bandwidth_credential_params() -> &'static Parameters {
static BANDWIDTH_CREDENTIAL_PARAMS: OnceLock<Parameters> = OnceLock::new();
BANDWIDTH_CREDENTIAL_PARAMS.get_or_init(IssuanceBandwidthCredential::default_parameters)
@@ -2,14 +2,12 @@
// SPDX-License-Identifier: Apache-2.0
use crate::coconut::bandwidth::CredentialSigningData;
use crate::coconut::utils::scalar_serde_helper;
use crate::error::Error;
use nym_api_requests::coconut::BlindSignRequestBody;
use nym_credentials_interface::{
hash_to_scalar, Attribute, BlindSignRequest, BlindedSignature, PublicAttribute,
};
use nym_credentials_interface::{BlindedSignature, WithdrawalRequest};
use nym_crypto::asymmetric::{encryption, identity};
use nym_validator_client::nyxd::{Coin, Hash};
use nym_ecash_contract_common::events::TICKET_BOOK_VALUE;
use nym_validator_client::nyxd::Hash;
use serde::{Deserialize, Serialize};
use zeroize::{Zeroize, ZeroizeOnDrop};
@@ -18,24 +16,24 @@ pub struct BandwidthVoucherIssuedData {
/// the plain value (e.g., bandwidth) encoded in this voucher
// note: for legacy reasons we're only using the value of the coin and ignoring the denom
#[zeroize(skip)]
value: Coin,
value: u128,
}
impl<'a> From<&'a BandwidthVoucherIssuanceData> for BandwidthVoucherIssuedData {
fn from(value: &'a BandwidthVoucherIssuanceData) -> Self {
BandwidthVoucherIssuedData {
value: value.value.clone(),
value: value.value(),
}
}
}
impl BandwidthVoucherIssuedData {
pub fn value(&self) -> &Coin {
&self.value
pub fn value(&self) -> u128 {
self.value
}
pub fn value_plain(&self) -> String {
self.value.amount.to_string()
self.value.to_string()
}
}
@@ -44,11 +42,7 @@ pub struct BandwidthVoucherIssuanceData {
/// the plain value (e.g., bandwidth) encoded in this voucher
// note: for legacy reasons we're only using the value of the coin and ignoring the denom
#[zeroize(skip)]
value: Coin,
// note: as mentioned above, we're only hashing the value of the coin!
#[serde(with = "scalar_serde_helper")]
value_prehashed: PublicAttribute,
value: u128,
/// the hash of the deposit transaction
#[zeroize(skip)]
@@ -63,24 +57,21 @@ pub struct BandwidthVoucherIssuanceData {
impl BandwidthVoucherIssuanceData {
pub fn new(
value: impl Into<Coin>,
deposit_tx_hash: Hash,
signing_key: identity::PrivateKey,
unused_ed25519: encryption::PrivateKey,
) -> Self {
let value = value.into();
let value_prehashed = hash_to_scalar(value.amount.to_string());
let value = TICKET_BOOK_VALUE;
BandwidthVoucherIssuanceData {
value,
value_prehashed,
deposit_tx_hash,
signing_key,
unused_ed25519,
}
}
pub fn request_plaintext(request: &BlindSignRequest, tx_hash: Hash) -> Vec<u8> {
pub fn request_plaintext(request: &WithdrawalRequest, tx_hash: Hash) -> Vec<u8> {
let mut message = request.to_bytes();
message.extend_from_slice(tx_hash.as_bytes());
message
@@ -88,7 +79,7 @@ impl BandwidthVoucherIssuanceData {
fn request_signature(&self, signing_request: &CredentialSigningData) -> identity::Signature {
let message =
Self::request_plaintext(&signing_request.blind_sign_request, self.deposit_tx_hash);
Self::request_plaintext(&signing_request.withdrawal_request, self.deposit_tx_hash);
self.signing_key.sign(message)
}
@@ -99,10 +90,11 @@ impl BandwidthVoucherIssuanceData {
let request_signature = self.request_signature(signing_request);
BlindSignRequestBody::new(
signing_request.blind_sign_request.clone(),
signing_request.withdrawal_request.clone(),
self.deposit_tx_hash,
request_signature,
signing_request.public_attributes_plain.clone(),
signing_request.ecash_pub_key.clone(),
signing_request.expiration_date,
)
}
@@ -115,12 +107,8 @@ impl BandwidthVoucherIssuanceData {
Ok(server_response.blinded_signature)
}
pub fn value_plain(&self) -> String {
self.value.amount.to_string()
}
pub fn value_attribute(&self) -> &Attribute {
&self.value_prehashed
pub fn value(&self) -> u128 {
self.value
}
pub fn tx_hash(&self) -> Hash {
+148 -41
View File
@@ -3,15 +3,35 @@
use crate::coconut::bandwidth::IssuanceBandwidthCredential;
use crate::error::Error;
use chrono::{Duration, Timelike, Utc};
use log::{debug, warn};
use nym_credentials_interface::{
aggregate_verification_keys, Signature, SignatureShare, VerificationKey,
aggregate_expiration_signatures, aggregate_indices_signatures, aggregate_verification_keys,
constants, setup, Base58, CoinIndexSignature, ExpirationDateSignature,
PartialCoinIndexSignature, PartialExpirationDateSignature, VerificationKeyAuth, Wallet,
};
use nym_validator_client::client::CoconutApiClient;
pub fn today_timestamp() -> u64 {
let now_utc = Utc::now();
(now_utc.timestamp() - now_utc.num_seconds_from_midnight() as i64) as u64
}
pub fn cred_exp_date_timestamp() -> u64 {
today_timestamp()
+ Duration::days(constants::CRED_VALIDITY_PERIOD as i64 - 1).num_seconds() as u64
//count today as well
}
pub fn freepass_exp_date_timestamp() -> u64 {
today_timestamp()
+ Duration::days(constants::FREEPASS_VALIDITY_PERIOD as i64 - 1).num_seconds() as u64
//count today as well
}
pub fn obtain_aggregate_verification_key(
api_clients: &[CoconutApiClient],
) -> Result<VerificationKey, Error> {
) -> Result<VerificationKeyAuth, Error> {
if api_clients.is_empty() {
return Err(Error::NoValidatorsAvailable);
}
@@ -28,70 +48,157 @@ pub fn obtain_aggregate_verification_key(
Ok(aggregate_verification_keys(&shares, Some(&indices))?)
}
pub async fn obtain_aggregate_signature(
voucher: &IssuanceBandwidthCredential,
coconut_api_clients: &[CoconutApiClient],
pub async fn obtain_expiration_date_signatures(
ecash_api_clients: &[CoconutApiClient],
verification_key: &VerificationKeyAuth,
threshold: u64,
) -> Result<Signature, Error> {
if coconut_api_clients.is_empty() {
) -> Result<Vec<ExpirationDateSignature>, Error> {
if ecash_api_clients.is_empty() {
return Err(Error::NoValidatorsAvailable);
}
let mut shares = Vec::with_capacity(coconut_api_clients.len());
let verification_key = obtain_aggregate_verification_key(coconut_api_clients)?;
let mut signatures: Vec<(
u64,
VerificationKeyAuth,
Vec<PartialExpirationDateSignature>,
)> = Vec::with_capacity(ecash_api_clients.len());
let ecash_params = setup(constants::NB_TICKETS);
let expiration_date = cred_exp_date_timestamp();
for ecash_api_client in ecash_api_clients.iter() {
match ecash_api_client
.api_client
.expiration_date_signatures()
.await
{
Ok(signature) => {
let index = ecash_api_client.node_id;
let share = ecash_api_client.verification_key.clone();
signatures.push((index, share, signature.signs));
}
Err(err) => {
warn!(
"failed to obtain expiration date signature from {}: {err}",
ecash_api_client.api_client.api_url()
);
}
}
}
if signatures.len() < threshold as usize {
return Err(Error::NotEnoughShares);
}
//this already takes care of partial signatures validation
aggregate_expiration_signatures(
&ecash_params,
verification_key,
expiration_date,
&signatures,
)
.map_err(Error::CompactEcashError)
}
pub async fn obtain_coin_indices_signatures(
ecash_api_clients: &[CoconutApiClient],
verification_key: &VerificationKeyAuth,
threshold: u64,
) -> Result<Vec<CoinIndexSignature>, Error> {
if ecash_api_clients.is_empty() {
return Err(Error::NoValidatorsAvailable);
}
let mut signatures: Vec<(u64, VerificationKeyAuth, Vec<PartialCoinIndexSignature>)> =
Vec::with_capacity(ecash_api_clients.len());
let ecash_params = setup(constants::NB_TICKETS);
for ecash_api_client in ecash_api_clients.iter() {
match ecash_api_client.api_client.coin_indices_signatures().await {
Ok(signature) => {
let index = ecash_api_client.node_id;
let share = ecash_api_client.verification_key.clone();
signatures.push((index, share, signature.signs));
}
Err(err) => {
warn!(
"failed to obtain expiration date signature from {}: {err}",
ecash_api_client.api_client.api_url()
);
}
}
}
if signatures.len() < threshold as usize {
return Err(Error::NotEnoughShares);
}
//this takes care of validating partial signatures
aggregate_indices_signatures(&ecash_params, verification_key, &signatures)
.map_err(Error::CompactEcashError)
}
pub async fn obtain_aggregate_signature(
voucher: &IssuanceBandwidthCredential,
ecash_api_clients: &[CoconutApiClient],
threshold: u64,
) -> Result<Wallet, Error> {
if ecash_api_clients.is_empty() {
return Err(Error::NoValidatorsAvailable);
}
let verification_key = obtain_aggregate_verification_key(ecash_api_clients)?;
let request = voucher.prepare_for_signing();
for coconut_api_client in coconut_api_clients.iter() {
let mut wallets = Vec::with_capacity(ecash_api_clients.len());
for ecash_api_client in ecash_api_clients.iter() {
debug!(
"attempting to obtain partial credential from {}",
coconut_api_client.api_client.api_url()
ecash_api_client.api_client.api_url()
);
match voucher
.obtain_partial_bandwidth_voucher_credential(
&coconut_api_client.api_client,
&coconut_api_client.verification_key,
Some(request.clone()),
&ecash_api_client.api_client,
ecash_api_client.node_id,
&ecash_api_client.verification_key,
request.clone(),
)
.await
{
Ok(signature) => {
let share = SignatureShare::new(signature, coconut_api_client.node_id);
shares.push(share)
}
Ok(wallet) => wallets.push(wallet),
Err(err) => {
warn!(
"failed to obtain partial credential from {}: {err}",
coconut_api_client.api_client.api_url()
ecash_api_client.api_client.api_url()
);
}
};
}
if shares.len() < threshold as usize {
if wallets.len() < threshold as usize {
return Err(Error::NotEnoughShares);
}
voucher.aggregate_signature_shares(&verification_key, &shares)
voucher.aggregate_signature_shares(&verification_key, &wallets, request)
}
pub(crate) mod scalar_serde_helper {
use bls12_381::Scalar;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use zeroize::Zeroizing;
pub fn serialize<S: Serializer>(scalar: &Scalar, serializer: S) -> Result<S::Ok, S::Error> {
scalar.to_bytes().serialize(serializer)
}
pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Scalar, D::Error> {
let b = <[u8; 32]>::deserialize(deserializer)?;
// make sure the bytes get zeroed
let bytes = Zeroizing::new(b);
let maybe_scalar: Option<Scalar> = Scalar::from_bytes(&bytes).into();
maybe_scalar.ok_or(serde::de::Error::custom(
"did not construct a valid bls12-381 scalar out of the provided bytes",
))
}
pub fn signatures_to_string<B>(sigs: &[B]) -> String
where
B: Base58,
{
sigs.iter()
.map(|sig| sig.to_bs58())
.collect::<Vec<_>>()
.join(",")
}
pub fn signatures_from_string<B>(bs58_sigs: String) -> Result<Vec<B>, Error>
where
B: Base58,
{
bs58_sigs
.split(',')
.map(B::try_from_bs58)
.collect::<Result<Vec<_>, _>>()
.map_err(Error::CompactEcashError)
}
+4 -4
View File
@@ -1,7 +1,7 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_credentials_interface::CoconutError;
use nym_credentials_interface::CompactEcashError;
use nym_crypto::asymmetric::encryption::KeyRecoveryError;
use nym_validator_client::ValidatorClientError;
@@ -32,8 +32,8 @@ pub enum Error {
#[error("Could not contact any validator")]
NoValidatorsAvailable,
#[error("Ran into a coconut error - {0}")]
CoconutError(#[from] CoconutError),
#[error("Ran into a Compact ecash error - {0}")]
CompactEcashError(#[from] CompactEcashError),
#[error("Ran into a validator client error - {0}")]
ValidatorClientError(#[from] ValidatorClientError),
@@ -51,7 +51,7 @@ pub enum Error {
NotEnoughShares,
#[error("Could not aggregate signature shares - {0}. Try again using the recovery command")]
SignatureAggregationError(CoconutError),
SignatureAggregationError(CompactEcashError),
#[error("Could not deserialize bandwidth voucher - {0}")]
BandwidthVoucherDeserializationError(String),
+14 -1
View File
@@ -463,8 +463,11 @@ pub const ETH_ERC20_APPROVE_FUNCTION_NAME: &str = "approve";
/// How much bandwidth (in bytes) one token can buy
pub const BYTES_PER_UTOKEN: u64 = 1024;
/// How much bandwidth (in bytes) one ticket can buy
pub const TICKET_BANDWIDTH_VALUE: u64 = 100 * 1024 * 1024; // 100 MB
/// How much bandwidth (in bytes) one freepass provides
pub const BYTES_PER_FREEPASS: u64 = 1024 * 1024 * 1024; // 1GB
pub const BYTES_PER_FREEPASS: u64 = 10 * 1024 * 1024; // 10 MB
/// Threshold for claiming more bandwidth: 1 MB
pub const REMAINING_BANDWIDTH_THRESHOLD: i64 = 1024 * 1024;
@@ -475,6 +478,16 @@ pub const UTOKENS_TO_BURN: u64 = TOKENS_TO_BURN * 1000000;
/// Default bandwidth (in bytes) that we try to buy
pub const BANDWIDTH_VALUE: u64 = UTOKENS_TO_BURN * BYTES_PER_UTOKEN;
// Constants for bloom filter for double spending detection
//Chosen for FP of
//Calculator at https://hur.st/bloomfilter/
pub const BLOOM_NUM_HASHES: u32 = 13;
pub const BLOOM_BITMAP_SIZE: u64 = 250_000;
pub const BLOOM_SIP_KEYS: [(u64, u64); 2] = [
(12345678910111213141, 1415926535897932384),
(7182818284590452353, 3571113171923293137),
];
/// Defaults Cosmos Hub/ATOM path
pub const COSMOS_DERIVATION_PATH: &str = "m/44'/118'/0'/0/0";
// as set by validators in their configs
+10 -15
View File
@@ -4,7 +4,6 @@
use crate::NymIdError;
use nym_credential_storage::models::StorableIssuedCredential;
use nym_credential_storage::storage::Storage;
use nym_credentials::coconut::bandwidth::issued::BandwidthCredentialIssuedDataVariant;
use nym_credentials::IssuedBandwidthCredential;
use tracing::{debug, warn};
use zeroize::Zeroizing;
@@ -28,22 +27,18 @@ where
"attempting to import credential of type {}",
credential.typ()
);
debug!(
"with expiration date at {}",
credential.expiration_date_formatted()
);
match credential.variant_data() {
BandwidthCredentialIssuedDataVariant::Voucher(voucher_info) => {
debug!("with value of {}", voucher_info.value())
}
BandwidthCredentialIssuedDataVariant::FreePass(freepass_info) => {
debug!("with expiry at {}", freepass_info.expiry_date());
if freepass_info.expired() {
warn!("the free pass has already expired!");
if credential.expired() {
warn!("the credential has already expired!");
// technically we can import it, but the gateway will just reject it so what's the point
return Err(NymIdError::ExpiredCredentialImport {
expiration: freepass_info.expiry_date(),
});
}
}
// technically we can import it, but the gateway will just reject it so what's the point
return Err(NymIdError::ExpiredCredentialImport {
expiration: credential.expiration_date_formatted(),
});
}
// SAFETY:
@@ -0,0 +1,56 @@
[package]
name = "nym-compact-ecash"
version = "0.1.0"
authors = ["Ania Piotrowska <ania@nymtech.net>"]
edition = "2021"
license = { workspace = true }
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
chrono = "0.4.38"
bls12_381 = { workspace = true , features = ["alloc", "pairings", "experimental", "zeroize"]}
itertools = "0.12.1"
digest = "0.9"
rand = { workspace = true }
thiserror = { workspace = true }
sha2 = "0.9"
bs58 = { workspace = true }
serde = { workspace = true }
getset = "0.1.1"
rayon = "1.5.0"
zeroize = { workspace = true , features = ["zeroize_derive"]}
nym-pemstore = { path = "../pemstore" }
[dev-dependencies]
criterion = { version = "0.3", features = ["html_reports"] }
[dependencies.ff]
version = "0.13"
default-features = false
[dependencies.group]
version = "0.13"
default-features = false
[[bench]]
name = "benchmarks_group_operations"
path = "benches/benchmarks_group_operations.rs"
harness = false
[[bench]]
name = "benchmarks_expiration_date_signatures"
path = "benches/benchmarks_expiration_date_signatures.rs"
harness = false
[[bench]]
name = "benchmarks_coin_indices_signatures"
path = "benches/benchmarks_coin_indices_signatures.rs"
harness = false
[[bench]]
name = "benchmarks_ecash_e2e"
path = "benches/benchmarks_ecash_e2e.rs"
harness = false
@@ -0,0 +1,120 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use criterion::{criterion_group, criterion_main, Criterion};
use nym_compact_ecash::scheme::keygen::SecretKeyAuth;
use nym_compact_ecash::setup::{
aggregate_indices_signatures, setup, sign_coin_indices, verify_coin_indices_signatures,
PartialCoinIndexSignature,
};
use nym_compact_ecash::{aggregate_verification_keys, ttp_keygen, VerificationKeyAuth};
fn bench_coin_signing(c: &mut Criterion) {
let mut group = c.benchmark_group("benchmark-sign-verify-coin-signing");
let ll = 32;
let params = setup(ll);
let authorities_keypairs = ttp_keygen(params.grp(), 2, 3).unwrap();
let indices: [u64; 3] = [1, 2, 3];
// Pick one authority to do the signing
let sk_i_auth = authorities_keypairs[0].secret_key();
let vk_i_auth = authorities_keypairs[0].verification_key();
// list of verification keys of each authority
let verification_keys_auth: Vec<VerificationKeyAuth> = authorities_keypairs
.iter()
.map(|keypair| keypair.verification_key())
.collect();
// the global master verification key
let verification_key =
aggregate_verification_keys(&verification_keys_auth, Some(&indices)).unwrap();
let partial_signatures = sign_coin_indices(&params, &verification_key, &sk_i_auth);
// ISSUING AUTHORITY BENCHMARK: issue a set of (partial) signatures for coin indices
group.bench_function(
&format!(
"[IssuingAuthority] sign_coin_indices_L_{}",
params.get_total_coins()
),
|b| b.iter(|| sign_coin_indices(&params, &verification_key, &sk_i_auth)),
);
// CLIENT: verify the correctness of the (partial)) signatures for coin indices
assert!(verify_coin_indices_signatures(
&params,
&verification_key,
&vk_i_auth,
&partial_signatures
)
.is_ok());
group.bench_function(
&format!(
"[Client] verify_coin_indices_signatures_L_{}",
params.get_total_coins()
),
|b| {
b.iter(|| {
verify_coin_indices_signatures(
&params,
&verification_key,
&vk_i_auth,
&partial_signatures,
)
})
},
);
}
fn bench_aggregate_coin_indices_signatures(c: &mut Criterion) {
let mut group = c.benchmark_group("benchmark-aggregate-coin-signing");
let ll = 32;
let params = setup(ll);
let authorities_keypairs = ttp_keygen(params.grp(), 7, 10).unwrap();
let indices: [u64; 10] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// list of secret keys of each authority
let secret_keys_authorities: Vec<SecretKeyAuth> = authorities_keypairs
.iter()
.map(|keypair| keypair.secret_key())
.collect();
// list of verification keys of each authority
let verification_keys_auth: Vec<VerificationKeyAuth> = authorities_keypairs
.iter()
.map(|keypair| keypair.verification_key())
.collect();
// the global master verification key
let verification_key =
aggregate_verification_keys(&verification_keys_auth, Some(&indices)).unwrap();
// create the partial signatures from each authority
let partial_signatures: Vec<Vec<PartialCoinIndexSignature>> = secret_keys_authorities
.iter()
.map(|sk_auth| sign_coin_indices(&params, &verification_key, sk_auth))
.collect();
let combined_data: Vec<(u64, VerificationKeyAuth, Vec<PartialCoinIndexSignature>)> = indices
.iter()
.zip(verification_keys_auth.iter().zip(partial_signatures.iter()))
.map(|(i, (vk, sigs))| (*i, vk.clone(), sigs.clone()))
.collect();
// CLIENT: verify all the partial signature vectors and aggregate into a single vector of signed coin indices
group.bench_function(
&format!(
"[Client] aggregate_coin_indices_signatures_from_{}_issuing_authorities_L_{}",
authorities_keypairs.len(),
params.get_total_coins(),
),
|b| b.iter(|| aggregate_indices_signatures(&params, &verification_key, &combined_data)),
);
}
criterion_group!(
benches,
bench_coin_signing,
bench_aggregate_coin_indices_signatures
);
criterion_main!(benches);
@@ -0,0 +1,373 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use bls12_381::Scalar;
use criterion::{criterion_group, criterion_main, Criterion};
use itertools::izip;
use rand::seq::SliceRandom;
use nym_compact_ecash::constants;
use nym_compact_ecash::identify::{identify, IdentifyResult};
use nym_compact_ecash::error::Result;
use nym_compact_ecash::scheme::expiration_date_signatures::{
aggregate_expiration_signatures, sign_expiration_date, ExpirationDateSignature,
PartialExpirationDateSignature,
};
use nym_compact_ecash::scheme::keygen::SecretKeyAuth;
use nym_compact_ecash::scheme::setup::{
aggregate_indices_signatures, setup, sign_coin_indices, CoinIndexSignature, Parameters,
PartialCoinIndexSignature,
};
use nym_compact_ecash::{
aggregate_verification_keys, aggregate_wallets, generate_keypair_user, issue, issue_verify,
ttp_keygen, withdrawal_request, PartialWallet, PayInfo, PublicKeyUser, SecretKeyUser,
VerificationKeyAuth,
};
pub fn generate_expiration_date_signatures(
params: &Parameters,
expiration_date: u64,
secret_keys_authorities: &[SecretKeyAuth],
verification_keys_auth: &[VerificationKeyAuth],
verification_key: &VerificationKeyAuth,
indices: &[u64],
) -> Result<Vec<ExpirationDateSignature>> {
let mut edt_partial_signatures: Vec<Vec<PartialExpirationDateSignature>> =
Vec::with_capacity(constants::CRED_VALIDITY_PERIOD as usize);
for sk_auth in secret_keys_authorities.iter() {
let sign = sign_expiration_date(sk_auth, expiration_date);
edt_partial_signatures.push(sign);
}
let combined_data: Vec<(
u64,
VerificationKeyAuth,
Vec<PartialExpirationDateSignature>,
)> = indices
.iter()
.zip(
verification_keys_auth
.iter()
.zip(edt_partial_signatures.iter()),
)
.map(|(i, (vk, sigs))| (*i, vk.clone(), sigs.clone()))
.collect();
aggregate_expiration_signatures(params, verification_key, expiration_date, &combined_data)
}
pub fn generate_coin_indices_signatures(
params: &Parameters,
secret_keys_authorities: &[SecretKeyAuth],
verification_keys_auth: &[VerificationKeyAuth],
verification_key: &VerificationKeyAuth,
indices: &[u64],
) -> Result<Vec<CoinIndexSignature>> {
// create the partial signatures from each authority
let partial_signatures: Vec<Vec<PartialCoinIndexSignature>> = secret_keys_authorities
.iter()
.map(|sk_auth| sign_coin_indices(params, verification_key, sk_auth))
.collect();
let combined_data: Vec<(u64, VerificationKeyAuth, Vec<PartialCoinIndexSignature>)> = indices
.iter()
.zip(verification_keys_auth.iter().zip(partial_signatures.iter()))
.map(|(i, (vk, sigs))| (*i, vk.clone(), sigs.clone()))
.collect();
aggregate_indices_signatures(params, verification_key, &combined_data)
}
struct BenchCase {
num_authorities: u64,
threshold_p: f32,
ll: u64,
spend_vv: u64,
case_nr_pub_keys: u64,
}
fn bench_compact_ecash(c: &mut Criterion) {
let mut group = c.benchmark_group("benchmark-compact-ecash");
// group.sample_size(300);
// group.measurement_time(Duration::from_secs(1500));
let expiration_date = 1703721600; // Dec 28 2023
let spend_date = Scalar::from(1701960386); // Dec 07 2023
let case = BenchCase {
num_authorities: 100,
threshold_p: 0.7,
ll: 1000,
spend_vv: 1,
case_nr_pub_keys: 99,
};
// SETUP PHASE and KEY GENERATION
let params = setup(case.ll);
let grp = params.grp();
let user_keypair = generate_keypair_user(grp);
let threshold = (case.threshold_p * case.num_authorities as f32).round() as u64;
let authorities_keypairs = ttp_keygen(grp, threshold, case.num_authorities).unwrap();
let secret_keys_authorities: Vec<SecretKeyAuth> = authorities_keypairs
.iter()
.map(|keypair| keypair.secret_key())
.collect();
let verification_keys_auth: Vec<VerificationKeyAuth> = authorities_keypairs
.iter()
.map(|keypair| keypair.verification_key())
.collect();
let indices: Vec<u64> = (1..case.num_authorities + 1).collect();
let verification_key =
aggregate_verification_keys(&verification_keys_auth, Some(&indices)).unwrap();
// PRE-GENERATION OF THE EXPORATION DATE SIGNATURES AND THE COIN INDICES SIGNATURES
// generate valid dates signatures
let dates_signatures = generate_expiration_date_signatures(
&params,
expiration_date,
&secret_keys_authorities,
&verification_keys_auth,
&verification_key,
&indices,
)
.unwrap();
// generate coin indices signatures
let coin_indices_signatures = generate_coin_indices_signatures(
&params,
&secret_keys_authorities,
&verification_keys_auth,
&verification_key,
&indices,
)
.unwrap();
// ISSUANCE PHASE
let (req, req_info) =
withdrawal_request(grp, &user_keypair.secret_key(), expiration_date).unwrap();
// CLIENT BENCHMARK: prepare a single withdrawal request
group.bench_function(
&format!(
"[Client] withdrawal_request_{}_authorities_{}_L_{}_threshold",
case.num_authorities, case.ll, case.threshold_p,
),
|b| {
b.iter(|| withdrawal_request(grp, &user_keypair.secret_key(), expiration_date).unwrap())
},
);
// ISSUING AUTHRORITY BENCHMARK: Benchmark the issue function
// called by an authority to issue a blind signature on a partial wallet
let mut rng = rand::thread_rng();
let keypair = authorities_keypairs.choose(&mut rng).unwrap();
group.bench_function(
&format!(
"[Issuing Authority] issue_partial_wallet_with_L_{}",
case.ll,
),
|b| {
b.iter(|| {
issue(
grp,
keypair.secret_key(),
user_keypair.public_key(),
&req,
expiration_date,
)
})
},
);
let mut wallet_blinded_signatures = Vec::new();
for auth_keypair in &authorities_keypairs {
let blind_signature = issue(
grp,
auth_keypair.secret_key(),
user_keypair.public_key(),
&req,
expiration_date,
);
wallet_blinded_signatures.push(blind_signature.unwrap());
}
// CLIENT BENCHMARK: verify the issued partial wallet
let w = wallet_blinded_signatures.first().unwrap();
let vk = verification_keys_auth.first().unwrap();
group.bench_function(
&format!("[Client] issue_verify_a_partial_wallet_with_L_{}", case.ll,),
|b| b.iter(|| issue_verify(grp, vk, &user_keypair.secret_key(), w, &req_info, 1).unwrap()),
);
let unblinded_wallet_shares: Vec<PartialWallet> = izip!(
wallet_blinded_signatures.iter(),
verification_keys_auth.iter()
)
.enumerate()
.map(|(idx, (w, vk))| {
issue_verify(
grp,
vk,
&user_keypair.secret_key(),
w,
&req_info,
idx as u64 + 1,
)
.unwrap()
})
.collect();
// CLIENT BENCHMARK: aggregating all partial wallets
group.bench_function(
&format!(
"[Client] aggregate_wallets_with_L_{}_threshold_{}",
case.ll, case.threshold_p,
),
|b| {
b.iter(|| {
aggregate_wallets(
grp,
&verification_key,
&user_keypair.secret_key(),
&unblinded_wallet_shares,
&req_info,
)
.unwrap()
})
},
);
// Aggregate partial wallets
let aggr_wallet = aggregate_wallets(
grp,
&verification_key,
&user_keypair.secret_key(),
&unblinded_wallet_shares,
&req_info,
)
.unwrap();
// SPENDING PHASE
let pay_info = PayInfo {
pay_info_bytes: [6u8; 72],
};
// CLIENT BENCHMARK: spend a single coin from the wallet
group.bench_function(
&format!(
"[Client] spend_a_single_coin_L_{}_threshold_{}",
case.ll, case.threshold_p,
),
|b| {
b.iter(|| {
aggr_wallet
.spend(
&params,
&verification_key,
&user_keypair.secret_key(),
&pay_info,
false,
case.spend_vv,
dates_signatures.clone(),
coin_indices_signatures.clone(),
spend_date,
)
.unwrap()
})
},
);
let (payment, _) = aggr_wallet
.spend(
&params,
&verification_key,
&user_keypair.secret_key(),
&pay_info,
false,
case.spend_vv,
dates_signatures.clone(),
coin_indices_signatures.clone(),
spend_date,
)
.unwrap();
// MERCHANT BENCHMARK: verify whether the submitted payment is legit
group.bench_function(
&format!(
"[Merchant] spend_verify_of_a_single_payment_L_{}_threshold_{}",
case.ll, case.threshold_p,
),
|b| {
b.iter(|| {
payment
.spend_verify(&params, &verification_key, &pay_info, spend_date)
.unwrap()
})
},
);
// BENCHMARK IDENTIFICATION
// Let's generate a double spending payment
// let's reverse the spending counter in the wallet to create a double spending payment
let current_l = aggr_wallet.l.get();
aggr_wallet.l.set(current_l - case.spend_vv);
let pay_info2 = PayInfo {
pay_info_bytes: [7u8; 72],
};
let (payment2, _) = aggr_wallet
.spend(
&params,
&verification_key,
&user_keypair.secret_key(),
&pay_info2,
true,
case.spend_vv,
dates_signatures.clone(),
coin_indices_signatures.clone(),
spend_date,
)
.unwrap();
// GENERATE KEYS FOR OTHER USERS
let mut public_keys: Vec<PublicKeyUser> = Default::default();
for _ in 0..case.case_nr_pub_keys {
let sk = grp.random_scalar();
let sk_user = SecretKeyUser { sk };
let pk_user = sk_user.public_key(grp);
public_keys.push(pk_user);
}
public_keys.push(user_keypair.public_key());
// MERCHANT BENCHMARK: identify double spending
group.bench_function(
&format!(
"[Merchant] identify_L_{}_threshold_{}_spend_vv_{}_pks_{}",
case.ll,
case.threshold_p,
case.spend_vv,
public_keys.len()
),
|b| {
b.iter(|| {
identify(
payment.clone(),
payment2.clone(),
pay_info.clone(),
pay_info2.clone(),
)
})
},
);
let identify_result = identify(payment, payment2, pay_info.clone(), pay_info2.clone());
assert_eq!(
identify_result,
IdentifyResult::DoubleSpendingPublicKeys(user_keypair.public_key())
);
}
criterion_group!(benches, bench_compact_ecash);
criterion_main!(benches);
@@ -0,0 +1,121 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_compact_ecash::scheme::expiration_date_signatures::{
aggregate_expiration_signatures, sign_expiration_date, verify_valid_dates_signatures,
PartialExpirationDateSignature,
};
use criterion::{criterion_group, criterion_main, Criterion};
use nym_compact_ecash::constants;
use nym_compact_ecash::scheme::keygen::SecretKeyAuth;
use nym_compact_ecash::setup::setup;
use nym_compact_ecash::{aggregate_verification_keys, ttp_keygen, VerificationKeyAuth};
fn bench_partial_sign_expiration_date(c: &mut Criterion) {
let mut group = c.benchmark_group("benchmark-sign-verify-expiration-date");
let ll = 32;
let params = setup(ll);
let expiration_date = 1703183958;
let authorities_keys = ttp_keygen(params.grp(), 2, 3).unwrap();
let sk_i_auth = authorities_keys[0].secret_key();
let vk_i_auth = authorities_keys[0].verification_key();
let partial_exp_sig = sign_expiration_date(&sk_i_auth, expiration_date);
// ISSUING AUTHORITY BENCHMARK: issue a set of (partial) signatures for a given expiration date
group.bench_function(
&format!(
"[IssuingAuthority] sign_expiration_date_{}_validity_period",
constants::CRED_VALIDITY_PERIOD,
),
|b| b.iter(|| sign_expiration_date(&sk_i_auth, expiration_date)),
);
// CLIENT: verify the correctness of the set of (partial) signatures for a given expiration date
assert!(
verify_valid_dates_signatures(&params, &vk_i_auth, &partial_exp_sig, expiration_date)
.is_ok()
);
group.bench_function(
&format!(
"[Client] verify_valid_dates_signatures_{}_validity_period",
constants::CRED_VALIDITY_PERIOD,
),
|b| {
b.iter(|| {
verify_valid_dates_signatures(
&params,
&vk_i_auth,
&partial_exp_sig,
expiration_date,
)
})
},
);
}
fn bench_aggregate_expiration_date_signatures(c: &mut Criterion) {
let mut group = c.benchmark_group("benchmark-aggregate-verify-expiration-date-signatures");
let ll = 32;
let params = setup(ll);
let expiration_date = 1703183958;
let authorities_keypairs = ttp_keygen(params.grp(), 7, 10).unwrap();
let indices: [u64; 10] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// list of secret keys of each authority
let secret_keys_authorities: Vec<SecretKeyAuth> = authorities_keypairs
.iter()
.map(|keypair| keypair.secret_key())
.collect();
// list of verification keys of each authority
let verification_keys_auth: Vec<VerificationKeyAuth> = authorities_keypairs
.iter()
.map(|keypair| keypair.verification_key())
.collect();
// the global master verification key
let verification_key =
aggregate_verification_keys(&verification_keys_auth, Some(&indices)).unwrap();
let mut partial_signatures: Vec<Vec<PartialExpirationDateSignature>> =
Vec::with_capacity(constants::CRED_VALIDITY_PERIOD as usize);
for sk in secret_keys_authorities.iter() {
let sign = sign_expiration_date(sk, expiration_date);
partial_signatures.push(sign);
}
let combined_data: Vec<(
u64,
VerificationKeyAuth,
Vec<PartialExpirationDateSignature>,
)> = indices
.iter()
.zip(verification_keys_auth.iter().zip(partial_signatures.iter()))
.map(|(i, (vk, sigs))| (*i, vk.clone(), sigs.clone()))
.collect();
// CLIENT: verify all the partial signature vectors and aggregate into a single vector of signed valid dates
group.bench_function(
&format!(
"[Client] aggregate_expiration_signatures_from_{}_issuing_authorities_{}_validity_period",
constants::CRED_VALIDITY_PERIOD, authorities_keypairs.len(),
),
|b| {
b.iter(|| {
aggregate_expiration_signatures(
&params,
&verification_key,
expiration_date,
&combined_data,
)
})
},
);
}
criterion_group!(
benches,
bench_partial_sign_expiration_date,
bench_aggregate_expiration_date_signatures
);
criterion_main!(benches);
@@ -0,0 +1,136 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::ops::Neg;
use std::time::Duration;
use bls12_381::{
multi_miller_loop, G1Affine, G1Projective, G2Affine, G2Prepared, G2Projective, Gt, Scalar,
};
use criterion::{criterion_group, criterion_main, Criterion};
use ff::Field;
use group::{Curve, Group};
#[allow(unused)]
fn double_pairing(g11: &G1Affine, g21: &G2Affine, g12: &G1Affine, g22: &G2Affine) {
let gt1 = bls12_381::pairing(g11, g21);
let gt2 = bls12_381::pairing(g12, g22);
assert_eq!(gt1, gt2)
}
#[allow(unused)]
fn single_pairing(g11: &G1Affine, g21: &G2Affine) {
let gt1 = bls12_381::pairing(g11, g21);
}
#[allow(unused)]
fn exponent_in_g1(g1: G1Projective, r: Scalar) {
let g11 = (g1 * r);
}
#[allow(unused)]
fn exponent_in_g2(g2: G2Projective, r: Scalar) {
let g22 = (g2 * r);
}
#[allow(unused)]
fn exponent_in_gt(gt: Gt, r: Scalar) {
let gtt = (gt * r);
}
#[allow(unused)]
fn multi_miller_pairing_affine(g11: &G1Affine, g21: &G2Affine, g12: &G1Affine, g22: &G2Affine) {
let miller_loop_result = multi_miller_loop(&[
(g11, &G2Prepared::from(*g21)),
(&g12.neg(), &G2Prepared::from(*g22)),
]);
assert!(bool::from(
miller_loop_result.final_exponentiation().is_identity()
))
}
#[allow(unused)]
fn multi_miller_pairing_with_prepared(
g11: &G1Affine,
g21: &G2Prepared,
g12: &G1Affine,
g22: &G2Prepared,
) {
let miller_loop_result = multi_miller_loop(&[(g11, g21), (&g12.neg(), g22)]);
assert!(bool::from(
miller_loop_result.final_exponentiation().is_identity()
))
}
// the case of being able to prepare G2 generator
#[allow(unused)]
fn multi_miller_pairing_with_semi_prepared(
g11: &G1Affine,
g21: &G2Affine,
g12: &G1Affine,
g22: &G2Prepared,
) {
let miller_loop_result =
multi_miller_loop(&[(g11, &G2Prepared::from(*g21)), (&g12.neg(), g22)]);
assert!(bool::from(
miller_loop_result.final_exponentiation().is_identity()
))
}
#[allow(unused)]
fn bench_group_operations(c: &mut Criterion) {
let mut group = c.benchmark_group("bench_group_operations");
group.measurement_time(Duration::from_secs(200));
let mut rng = rand::thread_rng();
let g1 = G1Affine::generator();
let g2 = G2Affine::generator();
let r = Scalar::random(&mut rng);
let s = Scalar::random(&mut rng);
let g11 = (g1 * r).to_affine();
let g21 = (g2 * s).to_affine();
let g21_prep = G2Prepared::from(g21);
let g12 = (g1 * s).to_affine();
let g22 = (g2 * r).to_affine();
let g22_prep = G2Prepared::from(g22);
let gt = bls12_381::pairing(&g11, &g21);
let gen1 = G1Projective::generator();
let gen2 = G2Projective::generator();
group.bench_function("exponent operation in G1", |b| {
b.iter(|| exponent_in_g1(gen1, r))
});
group.bench_function("exponent operation in G2", |b| {
b.iter(|| exponent_in_g2(gen2, r))
});
group.bench_function("exponent operation in Gt", |b| {
b.iter(|| exponent_in_gt(gt, r))
});
group.bench_function("single pairing", |b| b.iter(|| single_pairing(&g11, &g21)));
group.bench_function("double pairing", |b| {
b.iter(|| double_pairing(&g11, &g21, &g12, &g22))
});
group.bench_function("multi miller in affine", |b| {
b.iter(|| multi_miller_pairing_affine(&g11, &g21, &g12, &g22))
});
group.bench_function("multi miller with prepared g2", |b| {
b.iter(|| multi_miller_pairing_with_prepared(&g11, &g21_prep, &g12, &g22_prep))
});
group.bench_function("multi miller with semi-prepared g2", |b| {
b.iter(|| multi_miller_pairing_with_semi_prepared(&g11, &g21, &g12, &g22_prep))
});
}
criterion_group!(benches, bench_group_operations);
criterion_main!(benches);
@@ -0,0 +1,9 @@
pub const PUBLIC_ATTRIBUTES_LEN: usize = 1;
pub const PRIVATE_ATTRIBUTES_LEN: usize = 2;
pub const ATTRIBUTES_LEN: usize = 3; // number of attributes encoded in a single zk-nym credential
pub const CRED_VALIDITY_PERIOD: u64 = 30;
pub const FREEPASS_VALIDITY_PERIOD: u64 = 7;
pub const NB_TICKETS: u64 = 1000;
pub const SPEND_TICKETS: u64 = 1;
pub const TYPE_EXP: [u8; 32] = *b"ZKNYMEXPIRATIONDATE4llCBMEypAxr3";
pub const TYPE_IDX: [u8; 32] = *b"ZKNYMSINDICESh^7gTYbhnap*12n5GG6";
@@ -0,0 +1,77 @@
use thiserror::Error;
pub type Result<T> = std::result::Result<T, CompactEcashError>;
#[derive(Error, Debug)]
pub enum CompactEcashError {
#[error("Setup error: {0}")]
Setup(String),
#[error("Aggregation error: {0}")]
Aggregation(String),
#[error("Withdrawal Request Verification related error: {0}")]
WithdrawalRequestVerification(String),
#[error("Deserialization error: {0}")]
Deserialization(String),
#[error("Interpolation error: {0}")]
Interpolation(String),
#[error("Issuance related error: {0}")]
Issuance(String),
#[error("Issuance Verification related error: {0}")]
IssuanceVfy(String),
#[error("Spend Verification related error: {0}")]
Spend(String),
#[error("ZKP Proof related error: {0}")]
RangeProofOutOfBound(String),
#[error("Identify Verification related error: {0}")]
Identify(String),
#[error("Could not decode base 58 string - {0}")]
MalformedString(#[from] bs58::decode::Error),
#[error("Payment did not verify")]
PaymentVerification,
#[error("Expiration Date related error: {0}")]
ExpirationDate(String),
#[error("Coin Indices related error: {0}")]
CoinIndices(String),
#[error(
"Deserailization error, expected at least {} bytes, got {}",
min,
actual
)]
DeserializationMinLength { min: usize, actual: usize },
#[error("Tried to deserialize {object} with bytes of invalid length. Expected {actual} < {target} or {modulus_target} % {modulus} == 0")]
DeserializationInvalidLength {
actual: usize,
target: usize,
modulus_target: usize,
modulus: usize,
object: String,
},
#[error("received an array of unexpected size for deserialization of {typ}. got {received} but expected {expected}")]
UnexpectedArrayLength {
typ: String,
received: usize,
expected: usize,
},
#[error("failed to deserialize scalar from the received bytes - it might not have been canonically encoded")]
ScalarDeserializationFailure,
#[error("failed to deserialize G1Projective point from the received bytes - it might not have been canonically encoded")]
G1ProjectiveDeserializationFailure,
}
@@ -0,0 +1,16 @@
use crate::scheme::withdrawal::WithdrawalRequest;
use crate::traits::Bytable;
use crate::utils::BlindedSignature;
macro_rules! impl_clone {
($struct:ident) => {
impl Clone for $struct {
fn clone(&self) -> Self {
Self::try_from_byte_slice(&self.to_byte_vec()).unwrap()
}
}
};
}
impl_clone!(WithdrawalRequest);
impl_clone!(BlindedSignature);
@@ -0,0 +1,2 @@
mod clone;
mod serde;
@@ -0,0 +1,61 @@
use crate::scheme::expiration_date_signatures::ExpirationDateSignature;
use crate::scheme::{Payment, Wallet};
use crate::setup::PartialCoinIndexSignature;
use crate::traits::Base58;
use crate::utils::BlindedSignature;
use crate::{PayInfo, PublicKeyUser, SecretKeyUser, VerificationKeyAuth};
use serde::de::Unexpected;
use serde::{de::Error, de::Visitor, Deserialize, Deserializer, Serialize, Serializer};
use std::fmt;
use crate::scheme::withdrawal::WithdrawalRequest;
macro_rules! impl_serde {
($struct:ident, $visitor:ident) => {
pub struct $visitor {}
impl Serialize for $struct {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_bs58())
}
}
impl<'de> Visitor<'de> for $visitor {
type Value = $struct;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "A base58 encoded struct")
}
fn visit_str<E: Error>(self, s: &str) -> Result<Self::Value, E> {
match $struct::try_from_bs58(s) {
Ok(x) => Ok(x),
Err(_) => Err(Error::invalid_value(Unexpected::Str(s), &self)),
}
}
}
impl<'de> Deserialize<'de> for $struct {
fn deserialize<D>(deserializer: D) -> Result<$struct, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_str($visitor {})
}
}
};
}
impl_serde!(WithdrawalRequest, V1);
impl_serde!(Payment, V2);
impl_serde!(PayInfo, V3);
impl_serde!(VerificationKeyAuth, V4);
impl_serde!(ExpirationDateSignature, V5);
impl_serde!(PartialCoinIndexSignature, V6);
impl_serde!(BlindedSignature, V7);
impl_serde!(PublicKeyUser, V8);
impl_serde!(SecretKeyUser, V9);
impl_serde!(Wallet, V10);
@@ -0,0 +1,44 @@
use std::convert::TryInto;
pub use bls12_381::G1Projective;
use bls12_381::Scalar;
pub use scheme::aggregation::aggregate_verification_keys;
pub use scheme::aggregation::aggregate_wallets;
pub use scheme::identify;
pub use scheme::keygen::ttp_keygen;
pub use scheme::keygen::{generate_keypair_user, generate_keypair_user_from_seed};
pub use scheme::keygen::{KeyPairAuth, PublicKeyUser, SecretKeyUser, VerificationKeyAuth};
pub use scheme::setup;
pub use scheme::withdrawal::issue;
pub use scheme::withdrawal::issue_verify;
pub use scheme::withdrawal::withdrawal_request;
pub use scheme::withdrawal::WithdrawalRequest;
pub use scheme::PartialWallet;
pub use scheme::PayInfo;
pub use setup::GroupParameters;
pub use traits::Base58;
pub use crate::error::CompactEcashError;
pub use crate::traits::Bytable;
pub mod constants;
pub mod error;
mod impls;
mod proofs;
pub mod scheme;
pub mod tests;
mod traits;
pub mod utils;
pub type Attribute = Scalar;
impl Bytable for Attribute {
fn to_byte_vec(&self) -> Vec<u8> {
self.to_bytes().to_vec()
}
fn try_from_byte_slice(slice: &[u8]) -> Result<Self, CompactEcashError> {
Ok(Attribute::from_bytes(slice.try_into().unwrap()).unwrap())
}
}
@@ -0,0 +1,56 @@
use std::borrow::Borrow;
use bls12_381::Scalar;
use digest::generic_array::typenum::Unsigned;
use digest::Digest;
use sha2::Sha256;
pub mod proof_spend;
pub mod proof_withdrawal;
type ChallengeDigest = Sha256;
/// Generates a Scalar [or Fp] challenge by hashing a number of elliptic curve points.
fn compute_challenge<D, I, B>(iter: I) -> Scalar
where
D: Digest,
I: Iterator<Item = B>,
B: AsRef<[u8]>,
{
let mut h = D::new();
for point_representation in iter {
h.update(point_representation);
}
let digest = h.finalize();
// TODO: I don't like the 0 padding here (though it's what we've been using before,
// but we never had a security audit anyway...)
// instead we could maybe use the `from_bytes` variant and adding some suffix
// when computing the digest until we produce a valid scalar.
let mut bytes = [0u8; 64];
let pad_size = 64usize
.checked_sub(D::OutputSize::to_usize())
.unwrap_or_default();
bytes[pad_size..].copy_from_slice(&digest);
Scalar::from_bytes_wide(&bytes)
}
fn produce_response(witness_replacement: &Scalar, challenge: &Scalar, secret: &Scalar) -> Scalar {
witness_replacement - challenge * secret
}
// note: it's caller's responsibility to ensure witnesses.len() = secrets.len()
fn produce_responses<S>(witnesses: &[Scalar], challenge: &Scalar, secrets: &[S]) -> Vec<Scalar>
where
S: Borrow<Scalar>,
{
debug_assert_eq!(witnesses.len(), secrets.len());
witnesses
.iter()
.zip(secrets.iter())
.map(|(w, x)| produce_response(w, challenge, x.borrow()))
.collect()
}
@@ -0,0 +1,788 @@
use std::convert::{TryFrom, TryInto};
use bls12_381::{G1Projective, G2Projective, Scalar};
use group::{Curve, GroupEncoding};
use crate::error::{CompactEcashError, Result};
use crate::proofs::{compute_challenge, produce_response, produce_responses, ChallengeDigest};
use crate::scheme::keygen::VerificationKeyAuth;
use crate::scheme::setup::Parameters;
use crate::scheme::PayInfo;
use crate::utils::{
try_deserialize_g1_projective, try_deserialize_g2_projective, try_deserialize_scalar,
try_deserialize_scalar_vec,
};
#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq))]
pub struct SpendInstance {
pub kappa: G2Projective,
pub cc: G1Projective,
pub aa: Vec<G1Projective>,
pub ss: Vec<G1Projective>,
pub tt: Vec<G1Projective>,
pub kappa_k: Vec<G2Projective>,
pub kappa_e: G2Projective,
}
impl TryFrom<&[u8]> for SpendInstance {
type Error = CompactEcashError;
fn try_from(bytes: &[u8]) -> Result<SpendInstance> {
if bytes.len() < 48 * 5 + 3 * 96 || (bytes.len()) % 48 != 0 {
return Err(CompactEcashError::DeserializationInvalidLength {
actual: bytes.len(),
modulus_target: bytes.len(),
target: 48 * 5 + 3 * 96,
modulus: 48,
object: "spend instance".to_string(),
});
}
let mut j = 0;
let kappa_bytes = bytes[j..j + 96].try_into().unwrap();
let kappa = try_deserialize_g2_projective(
&kappa_bytes,
CompactEcashError::Deserialization("Failed to deserialize kappa".to_string()),
)?;
j += 96;
let kappa_e_bytes = bytes[j..j + 96].try_into().unwrap();
let kappa_e = try_deserialize_g2_projective(
&kappa_e_bytes,
CompactEcashError::Deserialization("Failed to deserialize kappa_e".to_string()),
)?;
j += 96;
let a_len = u64::from_le_bytes(bytes[j..j + 8].try_into().unwrap());
j += 8;
if bytes[j..].len() < a_len as usize * 48 {
return Err(CompactEcashError::DeserializationMinLength {
min: a_len as usize * 48,
actual: bytes[j..].len(),
});
}
let mut aa = Vec::with_capacity(a_len as usize);
for i in 0..a_len as usize {
let start = j + i * 48;
let end = start + 48;
let aa_elem_bytes = bytes[start..end].try_into().unwrap();
let aa_elem = try_deserialize_g1_projective(
&aa_elem_bytes,
CompactEcashError::Deserialization(
"Failed to deserialize compressed A values".to_string(),
),
)?;
aa.push(aa_elem)
}
j += a_len as usize * 48;
let cc_bytes = bytes[j..j + 48].try_into().unwrap();
let cc = try_deserialize_g1_projective(
&cc_bytes,
CompactEcashError::Deserialization("Failed to deserialize C".to_string()),
)?;
j += 48;
let s_len = u64::from_le_bytes(bytes[j..j + 8].try_into().unwrap());
j += 8;
if bytes[j..].len() < s_len as usize * 48 {
return Err(CompactEcashError::DeserializationMinLength {
min: s_len as usize * 48,
actual: bytes[j..].len(),
});
}
let mut ss = Vec::with_capacity(s_len as usize);
for i in 0..s_len as usize {
let start = j + i * 48;
let end = start + 48;
let ss_elem_bytes = bytes[start..end].try_into().unwrap();
let ss_elem = try_deserialize_g1_projective(
&ss_elem_bytes,
CompactEcashError::Deserialization(
"Failed to deserialize compressed S values".to_string(),
),
)?;
ss.push(ss_elem)
}
j += s_len as usize * 48;
let t_len = u64::from_le_bytes(bytes[j..j + 8].try_into().unwrap());
j += 8;
if bytes[j..].len() < t_len as usize * 48 {
return Err(CompactEcashError::DeserializationMinLength {
min: t_len as usize * 48,
actual: bytes[j..].len(),
});
}
let mut tt = Vec::with_capacity(t_len as usize);
for i in 0..t_len as usize {
let start = j + i * 48;
let end = start + 48;
let tt_elem_bytes = bytes[start..end].try_into().unwrap();
let tt_elem = try_deserialize_g1_projective(
&tt_elem_bytes,
CompactEcashError::Deserialization(
"Failed to deserialize compressed T values".to_string(),
),
)?;
tt.push(tt_elem)
}
j += t_len as usize * 48;
let kappa_k_len = u64::from_le_bytes(bytes[j..j + 8].try_into().unwrap());
j += 8;
if bytes[j..].len() < kappa_k_len as usize * 96 {
return Err(CompactEcashError::DeserializationMinLength {
min: kappa_k_len as usize * 96,
actual: bytes[j..].len(),
});
}
let mut kappa_k = Vec::with_capacity(kappa_k_len as usize);
for i in 0..kappa_k_len as usize {
let start = j + i * 48;
let end = start + 48;
let kappa_k_elem_bytes = bytes[start..end].try_into().unwrap();
let kappa_k_elem = try_deserialize_g2_projective(
&kappa_k_elem_bytes,
CompactEcashError::Deserialization(
"Failed to deserialize compressed kappa_k values".to_string(),
),
)?;
kappa_k.push(kappa_k_elem)
}
Ok(SpendInstance {
kappa,
aa,
cc,
ss,
tt,
kappa_k,
kappa_e,
})
}
}
impl SpendInstance {
pub(crate) fn to_bytes(&self) -> Vec<u8> {
let mut bytes: Vec<u8> = Default::default();
bytes.extend_from_slice(self.kappa.to_bytes().as_ref());
bytes.extend_from_slice(self.kappa_e.to_bytes().as_ref());
bytes.extend_from_slice(&self.aa.len().to_le_bytes());
for a in &self.aa {
bytes.extend_from_slice(&a.to_affine().to_compressed());
}
bytes.extend_from_slice(self.cc.to_bytes().as_ref());
bytes.extend_from_slice(&self.ss.len().to_le_bytes());
for s in &self.ss {
bytes.extend_from_slice(&s.to_affine().to_compressed());
}
bytes.extend_from_slice(&self.tt.len().to_le_bytes());
for t in &self.tt {
bytes.extend_from_slice(&t.to_affine().to_compressed());
}
bytes.extend_from_slice(&self.kappa_k.len().to_le_bytes());
for k in &self.kappa_k {
bytes.extend_from_slice(&k.to_affine().to_compressed());
}
bytes
}
}
pub struct SpendWitness {
// includes skUser, v, t
pub attributes: Vec<Scalar>,
// signature randomizing element
pub r: Scalar,
pub o_c: Scalar,
pub lk: Vec<Scalar>,
pub o_a: Vec<Scalar>,
pub mu: Vec<Scalar>,
pub o_mu: Vec<Scalar>,
pub r_k: Vec<Scalar>,
pub r_e: Scalar,
pub expiration_date: Scalar,
}
pub struct WitnessReplacement {
pub r_attributes: Vec<Scalar>,
pub r_r: Scalar,
pub r_r_e: Scalar,
pub r_o_c: Scalar,
pub r_r_lk: Vec<Scalar>,
pub r_lk: Vec<Scalar>,
pub r_o_a: Vec<Scalar>,
pub r_mu: Vec<Scalar>,
pub r_o_mu: Vec<Scalar>,
}
pub struct InstanceCommitments {
pub tt_kappa: G2Projective,
pub tt_kappa_e: G2Projective,
pub tt_cc: G1Projective,
pub tt_aa: Vec<G1Projective>,
pub tt_ss: Vec<G1Projective>,
pub tt_tt: Vec<G1Projective>,
pub tt_gamma1: Vec<G1Projective>,
pub tt_kappa_k: Vec<G2Projective>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct SpendProof {
challenge: Scalar,
response_r: Scalar,
response_r_e: Scalar,
responses_r_k: Vec<Scalar>,
responses_l: Vec<Scalar>,
responses_o_a: Vec<Scalar>,
response_o_c: Scalar,
responses_mu: Vec<Scalar>,
responses_o_mu: Vec<Scalar>,
responses_attributes: Vec<Scalar>,
}
pub fn generate_witness_replacement(
params: &Parameters,
witness: &SpendWitness,
) -> WitnessReplacement {
let grp_params = params.grp();
let r_attributes = grp_params.n_random_scalars(witness.attributes.len());
let r_r = grp_params.random_scalar();
let r_r_e = grp_params.random_scalar();
let r_o_c = grp_params.random_scalar();
let r_r_lk = grp_params.n_random_scalars(witness.r_k.len());
let r_lk = grp_params.n_random_scalars(witness.lk.len());
let r_o_a = grp_params.n_random_scalars(witness.o_a.len());
let r_mu = grp_params.n_random_scalars(witness.mu.len());
let r_o_mu = grp_params.n_random_scalars(witness.o_mu.len());
WitnessReplacement {
r_attributes,
r_r,
r_r_e,
r_o_c,
r_r_lk,
r_lk,
r_o_a,
r_mu,
r_o_mu,
}
}
pub fn compute_instance_commitments(
params: &Parameters,
witness_replacement: &WitnessReplacement,
instance: &SpendInstance,
verification_key: &VerificationKeyAuth,
rr: &[Scalar],
) -> InstanceCommitments {
let grp_params = params.grp();
let g1 = *grp_params.gen1();
let gamma0 = grp_params.gamma_idx(0).unwrap();
let gamma1 = grp_params.gamma_idx(1).unwrap();
let tt_kappa = grp_params.gen2() * witness_replacement.r_r
+ verification_key.alpha
+ witness_replacement
.r_attributes
.iter()
.zip(verification_key.beta_g2.iter())
.map(|(attr, beta_i)| beta_i * attr)
.sum::<G2Projective>();
let tt_cc = g1 * witness_replacement.r_o_c + gamma0 * witness_replacement.r_attributes[1];
let tt_kappa_e = grp_params.gen2() * witness_replacement.r_r_e
+ verification_key.alpha
+ verification_key.beta_g2[0] * witness_replacement.r_attributes[2];
let tt_aa: Vec<G1Projective> = witness_replacement
.r_o_a
.iter()
.zip(witness_replacement.r_lk.iter())
.map(|(r_o_a_k, r_l_k)| g1 * r_o_a_k + gamma0 * r_l_k)
.collect::<Vec<_>>();
let tt_kappa_k = witness_replacement
.r_lk
.iter()
.zip(witness_replacement.r_r_lk.iter())
.map(|(r_l_k, r_r_k)| {
verification_key.alpha + verification_key.beta_g2[0] * r_l_k + grp_params.gen2() * r_r_k
})
.collect::<Vec<_>>();
let tt_ss = witness_replacement
.r_mu
.iter()
.map(|r_mu_k| grp_params.delta() * r_mu_k)
.collect::<Vec<_>>();
let tt_tt = rr
.iter()
.zip(witness_replacement.r_mu.iter())
.map(|(rr_k, r_mu_k)| g1 * witness_replacement.r_attributes[0] + (g1 * rr_k) * r_mu_k)
.collect::<Vec<_>>();
let tt_gamma1 = instance
.aa
.iter()
.zip(witness_replacement.r_mu.iter())
.zip(witness_replacement.r_o_mu.iter())
.map(|((aa_k, r_mu_k), r_o_mu_k)| (aa_k + instance.cc + gamma1) * r_mu_k + g1 * r_o_mu_k)
.collect::<Vec<_>>();
InstanceCommitments {
tt_kappa,
tt_kappa_e,
tt_cc,
tt_aa,
tt_ss,
tt_tt,
tt_gamma1,
tt_kappa_k,
}
}
impl SpendProof {
pub fn construct(
params: &Parameters,
instance: &SpendInstance,
witness: &SpendWitness,
verification_key: &VerificationKeyAuth,
rr: &[Scalar],
pay_info: &PayInfo,
spend_value: u64,
) -> Self {
let grp_params = params.grp();
// generate random values to replace each witness
let witness_replacement = generate_witness_replacement(params, witness);
// compute zkp commitment for each instance
let instance_commitments = compute_instance_commitments(
params,
&witness_replacement,
instance,
verification_key,
rr,
);
let tt_aa_bytes = instance_commitments
.tt_aa
.iter()
.map(|x| x.to_bytes())
.collect::<Vec<_>>();
let tt_ss_bytes = instance_commitments
.tt_ss
.iter()
.map(|x| x.to_bytes())
.collect::<Vec<_>>();
let tt_tt_bytes = instance_commitments
.tt_tt
.iter()
.map(|x| x.to_bytes())
.collect::<Vec<_>>();
let tt_kappa_k_bytes = instance_commitments
.tt_kappa_k
.iter()
.map(|x| x.to_bytes())
.collect::<Vec<_>>();
// compute the challenge
let challenge = compute_challenge::<ChallengeDigest, _, _>(
std::iter::once(grp_params.gen1().to_bytes().as_ref())
.chain(std::iter::once(grp_params.gen2().to_bytes().as_ref()))
.chain(std::iter::once(grp_params.gammas_to_bytes().as_ref()))
.chain(std::iter::once(verification_key.to_bytes().as_ref()))
.chain(std::iter::once(instance.to_bytes().as_ref()))
.chain(std::iter::once(
instance_commitments.tt_kappa.to_bytes().as_ref(),
))
.chain(std::iter::once(
instance_commitments.tt_kappa_e.to_bytes().as_ref(),
))
.chain(std::iter::once(
instance_commitments.tt_cc.to_bytes().as_ref(),
))
.chain(tt_aa_bytes.iter().map(|x| x.as_ref()))
.chain(tt_ss_bytes.iter().map(|x| x.as_ref()))
.chain(tt_kappa_k_bytes.iter().map(|x| x.as_ref()))
.chain(tt_tt_bytes.iter().map(|x| x.as_ref()))
.chain(std::iter::once(pay_info.pay_info_bytes.as_ref()))
.chain(std::iter::once(spend_value.to_le_bytes().as_ref())),
);
// compute response for each witness
let responses_attributes = produce_responses(
&witness_replacement.r_attributes,
&challenge,
&witness.attributes.iter().collect::<Vec<_>>(),
);
let response_r = produce_response(&witness_replacement.r_r, &challenge, &witness.r);
let response_r_e = produce_response(&witness_replacement.r_r_e, &challenge, &witness.r_e);
let response_o_c = produce_response(&witness_replacement.r_o_c, &challenge, &witness.o_c);
let responses_r_k =
produce_responses(&witness_replacement.r_r_lk, &challenge, &witness.r_k);
let responses_l = produce_responses(&witness_replacement.r_lk, &challenge, &witness.lk);
let responses_o_a = produce_responses(&witness_replacement.r_o_a, &challenge, &witness.o_a);
let responses_mu = produce_responses(&witness_replacement.r_mu, &challenge, &witness.mu);
let responses_o_mu =
produce_responses(&witness_replacement.r_o_mu, &challenge, &witness.o_mu);
SpendProof {
challenge,
response_r,
response_r_e,
responses_r_k,
responses_l,
responses_o_a,
response_o_c,
responses_mu,
responses_o_mu,
responses_attributes,
}
}
pub fn verify(
&self,
params: &Parameters,
instance: &SpendInstance,
verification_key: &VerificationKeyAuth,
rr: &[Scalar],
pay_info: &PayInfo,
spend_value: u64,
) -> bool {
let grp_params = params.grp();
let g1 = *grp_params.gen1();
let gamma0 = *grp_params.gamma_idx(0).unwrap();
// re-compute each zkp commitment
let tt_kappa = instance.kappa * self.challenge
+ verification_key.alpha * (self.challenge.neg())
+ verification_key.alpha
+ grp_params.gen2() * self.response_r
+ self
.responses_attributes
.iter()
.zip(verification_key.beta_g2.iter())
.map(|(attr, beta_i)| beta_i * attr)
.sum::<G2Projective>();
let tt_cc = g1 * self.response_o_c
+ gamma0 * self.responses_attributes[1]
+ instance.cc * self.challenge;
let tt_kappa_e = instance.kappa_e * self.challenge
+ verification_key.alpha * (self.challenge.neg())
+ verification_key.alpha
+ verification_key.beta_g2[0] * self.responses_attributes[2]
+ grp_params.gen2() * self.response_r_e;
let tt_aa = self
.responses_o_a
.iter()
.zip(self.responses_l.iter())
.zip(instance.aa.iter())
.map(|((resp_o_a_k, resp_l_k), aa_k)| {
g1 * resp_o_a_k + gamma0 * resp_l_k + aa_k * self.challenge
})
.collect::<Vec<_>>();
let tt_aa_bytes = tt_aa.iter().map(|x| x.to_bytes()).collect::<Vec<_>>();
let tt_ss = self
.responses_mu
.iter()
.zip(instance.ss.iter())
.map(|(resp_mu_k, ss_k)| grp_params.delta() * resp_mu_k + ss_k * self.challenge)
.collect::<Vec<_>>();
let tt_ss_bytes = tt_ss.iter().map(|x| x.to_bytes()).collect::<Vec<_>>();
let tt_tt = self
.responses_mu
.iter()
.zip(rr.iter())
.zip(instance.tt.iter())
.map(|((resp_mu_k, rr_k), tt_k)| {
g1 * self.responses_attributes[0] + (g1 * rr_k) * resp_mu_k + tt_k * self.challenge
})
.collect::<Vec<_>>();
let tt_tt_bytes = tt_tt.iter().map(|x| x.to_bytes()).collect::<Vec<_>>();
let tt_kappa_k = instance
.kappa_k
.iter()
.zip(self.responses_r_k.iter())
.zip(self.responses_l.iter())
.map(|((kappa_k, resp_r_k), resp_r_l_k)| {
kappa_k * self.challenge
+ grp_params.gen2() * resp_r_k
+ verification_key.alpha * (Scalar::one() - self.challenge)
+ verification_key.beta_g2[0] * resp_r_l_k
})
.collect::<Vec<_>>();
let tt_kappa_k_bytes = tt_kappa_k.iter().map(|x| x.to_bytes()).collect::<Vec<_>>();
// re-compute the challenge
let challenge = compute_challenge::<ChallengeDigest, _, _>(
std::iter::once(grp_params.gen1().to_bytes().as_ref())
.chain(std::iter::once(grp_params.gen2().to_bytes().as_ref()))
.chain(std::iter::once(grp_params.gammas_to_bytes().as_ref()))
.chain(std::iter::once(verification_key.to_bytes().as_ref()))
.chain(std::iter::once(instance.to_bytes().as_ref()))
.chain(std::iter::once(tt_kappa.to_bytes().as_ref()))
.chain(std::iter::once(tt_kappa_e.to_bytes().as_ref()))
.chain(std::iter::once(tt_cc.to_bytes().as_ref()))
.chain(tt_aa_bytes.iter().map(|x| x.as_ref()))
.chain(tt_ss_bytes.iter().map(|x| x.as_ref()))
.chain(tt_kappa_k_bytes.iter().map(|x| x.as_ref()))
.chain(tt_tt_bytes.iter().map(|x| x.as_ref()))
.chain(std::iter::once(pay_info.pay_info_bytes.as_ref()))
.chain(std::iter::once(spend_value.to_le_bytes().as_ref())),
);
challenge == self.challenge
}
pub fn to_bytes(&self) -> Vec<u8> {
let challenge_bytes = self.challenge.to_bytes();
let response_r_bytes = self.response_r.to_bytes();
let response_r_e_bytes = self.response_r_e.to_bytes();
let rrk_len = self.responses_r_k.len();
let rrk_len_bytes = rrk_len.to_le_bytes();
let rl_len = self.responses_l.len();
let rl_len_bytes = rl_len.to_le_bytes();
let roa_len = self.responses_o_a.len();
let roa_len_bytes = roa_len.to_le_bytes();
let roc_bytes = self.response_o_c.to_bytes();
let rmu_len = self.responses_mu.len();
let rmu_len_bytes = rmu_len.to_le_bytes();
let romu_len = self.responses_o_mu.len();
let romu_len_bytes = romu_len.to_le_bytes();
let rattributes_len = self.responses_attributes.len();
let rattributes_len_bytes = rattributes_len.to_le_bytes();
let mut bytes: Vec<u8> = Vec::with_capacity(
128 + (rrk_len + rl_len + roa_len + rmu_len + romu_len + rattributes_len) * 8
+ (rrk_len + rl_len + roa_len + rmu_len + romu_len + rattributes_len) * 32,
);
bytes.extend_from_slice(&challenge_bytes);
bytes.extend_from_slice(&response_r_bytes);
bytes.extend_from_slice(&response_r_e_bytes);
bytes.extend_from_slice(&roc_bytes);
bytes.extend_from_slice(&rrk_len_bytes);
for rrk in &self.responses_r_k {
bytes.extend_from_slice(&rrk.to_bytes());
}
bytes.extend_from_slice(&rl_len_bytes);
for rl in &self.responses_l {
bytes.extend_from_slice(&rl.to_bytes());
}
bytes.extend_from_slice(&roa_len_bytes);
for roa in &self.responses_o_a {
bytes.extend_from_slice(&roa.to_bytes());
}
bytes.extend_from_slice(&rmu_len_bytes);
for rmu in &self.responses_mu {
bytes.extend_from_slice(&rmu.to_bytes());
}
bytes.extend_from_slice(&romu_len_bytes);
for romu in &self.responses_o_mu {
bytes.extend_from_slice(&romu.to_bytes());
}
bytes.extend_from_slice(&rattributes_len_bytes);
for rattr in &self.responses_attributes {
bytes.extend_from_slice(&rattr.to_bytes());
}
bytes
}
}
impl TryFrom<&[u8]> for SpendProof {
type Error = CompactEcashError;
fn try_from(bytes: &[u8]) -> Result<SpendProof> {
if bytes.len() < 368 || (bytes.len() - 128 - 48) % 32 != 0 {
return Err(CompactEcashError::Deserialization(
"tried to deserialize proof of spending with bytes of invalid length".to_string(),
));
}
let mut idx = 0;
let challenge_bytes = bytes[idx..idx + 32].try_into().unwrap();
idx += 32;
let response_r_bytes = bytes[idx..idx + 32].try_into().unwrap();
idx += 32;
let response_r_e_bytes = bytes[idx..idx + 32].try_into().unwrap();
idx += 32;
let response_o_c_bytes = bytes[idx..idx + 32].try_into().unwrap();
idx += 32;
let challenge = try_deserialize_scalar(
&challenge_bytes,
CompactEcashError::Deserialization("Failed to deserialize challenge".to_string()),
)?;
let response_r = try_deserialize_scalar(
&response_r_bytes,
CompactEcashError::Deserialization("Failed to deserialize response_r".to_string()),
)?;
let response_r_e = try_deserialize_scalar(
&response_r_e_bytes,
CompactEcashError::Deserialization("Failed to deserialize response_r_e".to_string()),
)?;
let response_o_c = try_deserialize_scalar(
&response_o_c_bytes,
CompactEcashError::Deserialization("Failed to deserialize response_o_c".to_string()),
)?;
let rrl_len = u64::from_le_bytes(bytes[idx..idx + 8].try_into().unwrap());
idx += 8;
if bytes[idx..].len() < rrl_len as usize * 32 {
return Err(CompactEcashError::Deserialization(
"tried to deserialize response_r_l".to_string(),
));
}
let rrl_end = idx + rrl_len as usize * 32;
let responses_r_k = try_deserialize_scalar_vec(
rrl_len,
&bytes[idx..rrl_end],
CompactEcashError::Deserialization("Failed to deserialize response_r_l".to_string()),
)?;
let rl_len = u64::from_le_bytes(bytes[rrl_end..rrl_end + 8].try_into().unwrap());
let response_l_start = rrl_end + 8;
if bytes[response_l_start..].len() < rl_len as usize * 32 {
return Err(CompactEcashError::Deserialization(
"tried to deserialize response_l".to_string(),
));
}
let rl_end = response_l_start + rl_len as usize * 32;
let responses_l = try_deserialize_scalar_vec(
rl_len,
&bytes[response_l_start..rl_end],
CompactEcashError::Deserialization("Failed to deserialize response_l".to_string()),
)?;
let roa_len = u64::from_le_bytes(bytes[rl_end..rl_end + 8].try_into().unwrap());
let roa_end = rl_end + 8;
if bytes[roa_end..].len() < roa_len as usize * 32 {
return Err(CompactEcashError::Deserialization(
"tried to deserialize response_o_a".to_string(),
));
}
let roa_end = roa_end + roa_len as usize * 32;
let responses_o_a = try_deserialize_scalar_vec(
roa_len,
&bytes[rl_end + 8..roa_end],
CompactEcashError::Deserialization("Failed to deserialize response_o_a".to_string()),
)?;
let response_mu_len = u64::from_le_bytes(bytes[roa_end..roa_end + 8].try_into().unwrap());
let response_mu_end = roa_end + 8;
if bytes[response_mu_end..].len() < response_mu_len as usize * 32 {
return Err(CompactEcashError::Deserialization(
"tried to deserialize response_mu".to_string(),
));
}
let response_mu_end = response_mu_end + response_mu_len as usize * 32;
let responses_mu = try_deserialize_scalar_vec(
response_mu_len,
&bytes[roa_end + 8..response_mu_end],
CompactEcashError::Deserialization("Failed to deserialize response_mu".to_string()),
)?;
let response_o_mu_len = u64::from_le_bytes(
bytes[response_mu_end..response_mu_end + 8]
.try_into()
.unwrap(),
);
let response_o_mu_end = response_mu_end + 8;
if bytes[response_o_mu_end..].len() < response_o_mu_len as usize * 32 {
return Err(CompactEcashError::Deserialization(
"tried to deserialize response_o_mu".to_string(),
));
}
let response_o_mu_end = response_o_mu_end + response_o_mu_len as usize * 32;
let responses_o_mu = try_deserialize_scalar_vec(
response_o_mu_len,
&bytes[response_mu_end + 8..response_o_mu_end],
CompactEcashError::Deserialization("Failed to deserialize response_o_mu".to_string()),
)?;
let response_attributes_len = u64::from_le_bytes(
bytes[response_o_mu_end..response_o_mu_end + 8]
.try_into()
.unwrap(),
);
let response_attributes_end = response_o_mu_end + 8;
if bytes[response_attributes_end..].len() < response_attributes_len as usize * 32 {
return Err(CompactEcashError::Deserialization(
"tried to deserialize response_attributes".to_string(),
));
}
let response_attributes_end =
response_attributes_end + response_attributes_len as usize * 32;
let responses_attributes = try_deserialize_scalar_vec(
response_attributes_len,
&bytes[response_o_mu_end + 8..response_attributes_end],
CompactEcashError::Deserialization(
"Failed to deserialize response_attributes".to_string(),
),
)?;
// Construct the SpendProof struct from the deserialized data
let spend_proof = SpendProof {
challenge,
response_r,
response_r_e,
response_o_c,
responses_r_k,
responses_l,
responses_o_a,
responses_mu,
responses_o_mu,
responses_attributes,
};
Ok(spend_proof)
}
}
@@ -0,0 +1,419 @@
use std::convert::{TryFrom, TryInto};
use bls12_381::{G1Projective, Scalar};
use group::GroupEncoding;
use itertools::izip;
use crate::error::{CompactEcashError, Result};
use crate::proofs::{compute_challenge, produce_response, produce_responses, ChallengeDigest};
use crate::scheme::keygen::PublicKeyUser;
use crate::scheme::setup::GroupParameters;
use crate::utils::{
try_deserialize_g1_projective, try_deserialize_scalar, try_deserialize_scalar_vec,
};
#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq))]
// instance: g, gamma1, gamma2, gamma3, com, h, com1, com2, com3, pkUser
pub struct WithdrawalReqInstance {
// Joined commitment to all attributes
pub joined_commitment: G1Projective,
// Hash of the joined commitment com
pub joined_commitment_hash: G1Projective,
// Pedersen commitments to each attribute
pub private_attributes_commitments: Vec<G1Projective>,
// Public key of a user
pub pk_user: PublicKeyUser,
}
impl TryFrom<&[u8]> for WithdrawalReqInstance {
type Error = CompactEcashError;
fn try_from(bytes: &[u8]) -> Result<WithdrawalReqInstance> {
if bytes.len() < 48 * 4 + 8 || (bytes.len() - 8) % 48 != 0 {
return Err(CompactEcashError::DeserializationInvalidLength {
actual: bytes.len(),
modulus_target: bytes.len() - 8,
target: 48 * 4 + 8,
modulus: 48,
object: "withdrawal request zkp instance".to_string(),
});
}
let com_bytes: [u8; 48] = bytes[..48].try_into().unwrap();
let joined_commitment = try_deserialize_g1_projective(
&com_bytes,
CompactEcashError::Deserialization("Failed to deserialize com".to_string()),
)?;
let h_bytes: [u8; 48] = bytes[48..96].try_into().unwrap();
let joined_commitment_hash = try_deserialize_g1_projective(
&h_bytes,
CompactEcashError::Deserialization("Failed to deserialize h".to_string()),
)?;
let pc_coms_len = u64::from_le_bytes(bytes[96..104].try_into().unwrap());
let actual_pc_coms_len = (bytes.len() - 152) / 48;
if pc_coms_len as usize != actual_pc_coms_len {
return Err(CompactEcashError::Deserialization(format!(
"Tried to deserialize pedersen commitments with inconsistent pc_coms_len (expected {}, got {})",
pc_coms_len, actual_pc_coms_len
)));
}
let mut private_attributes_commitments = Vec::new();
let mut pc_coms_end: usize = 0;
for i in 0..pc_coms_len {
let start = (104 + i * 48) as usize;
let end = start + 48;
let pc_i_bytes = bytes[start..end].try_into().unwrap();
let pc_i = try_deserialize_g1_projective(
&pc_i_bytes,
CompactEcashError::Deserialization(
"Failed to deserialize pedersen commitment".to_string(),
),
)?;
pc_coms_end = end;
private_attributes_commitments.push(pc_i);
}
let pk_bytes = bytes[pc_coms_end..].try_into().unwrap();
let pk = try_deserialize_g1_projective(
&pk_bytes,
CompactEcashError::Deserialization(
"Failed to deserialize user's public key".to_string(),
),
)?;
Ok(WithdrawalReqInstance {
joined_commitment,
joined_commitment_hash,
private_attributes_commitments,
pk_user: PublicKeyUser { pk },
})
}
}
impl WithdrawalReqInstance {
pub(crate) fn to_bytes(&self) -> Vec<u8> {
let pc_coms_len = self.private_attributes_commitments.len();
let mut bytes = Vec::with_capacity(8 + (pc_coms_len + 3) * 48);
bytes.extend_from_slice(self.joined_commitment.to_bytes().as_ref());
bytes.extend_from_slice(self.joined_commitment_hash.to_bytes().as_ref());
bytes.extend_from_slice(&pc_coms_len.to_le_bytes());
for pc in self.private_attributes_commitments.iter() {
bytes.extend_from_slice((pc.to_bytes()).as_ref());
}
bytes.extend_from_slice(self.pk_user.pk.to_bytes().as_ref());
bytes
}
#[allow(dead_code)]
pub fn from_bytes(bytes: &[u8]) -> Result<WithdrawalReqInstance> {
WithdrawalReqInstance::try_from(bytes)
}
}
// witness: m1, m2, m3, o, o1, o2, o3,
pub struct WithdrawalReqWitness {
pub private_attributes: Vec<Scalar>,
// Opening for the joined commitment com
pub joined_commitment_opening: Scalar,
// Openings for the pedersen commitments of private attributes
pub private_attributes_openings: Vec<Scalar>,
}
#[derive(Debug, PartialEq, Clone)]
pub struct WithdrawalReqProof {
challenge: Scalar,
response_opening: Scalar,
response_openings: Vec<Scalar>,
response_attributes: Vec<Scalar>,
}
impl WithdrawalReqProof {
pub(crate) fn construct(
params: &GroupParameters,
instance: &WithdrawalReqInstance,
witness: &WithdrawalReqWitness,
) -> Self {
// generate random values to replace the witnesses
let r_com_opening = params.random_scalar();
let r_pedcom_openings = params.n_random_scalars(witness.private_attributes_openings.len());
let r_attributes = params.n_random_scalars(witness.private_attributes.len());
// compute zkp commitments for each instance
let zkcm_com = params.gen1() * r_com_opening
+ r_attributes
.iter()
.zip(params.gammas().iter())
.map(|(rm_i, gamma_i)| gamma_i * rm_i)
.sum::<G1Projective>();
let zkcm_pedcom = r_pedcom_openings
.iter()
.zip(r_attributes.iter())
.map(|(o_j, m_j)| params.gen1() * o_j + instance.joined_commitment_hash * m_j)
.collect::<Vec<_>>();
let zkcm_user_sk = params.gen1() * r_attributes[0];
// covert to bytes
let gammas_bytes = params
.gammas()
.iter()
.map(|gamma| gamma.to_bytes())
.collect::<Vec<_>>();
let zkcm_pedcom_bytes = zkcm_pedcom
.iter()
.map(|cm| cm.to_bytes())
.collect::<Vec<_>>();
// compute zkp challenge using g1, gammas, c, h, c1, c2, c3, zk commitments
let challenge = compute_challenge::<ChallengeDigest, _, _>(
std::iter::once(params.gen1().to_bytes().as_ref())
.chain(gammas_bytes.iter().map(|gamma| gamma.as_ref()))
.chain(std::iter::once(instance.to_bytes().as_ref()))
.chain(std::iter::once(zkcm_com.to_bytes().as_ref()))
.chain(zkcm_pedcom_bytes.iter().map(|c| c.as_ref()))
.chain(std::iter::once(zkcm_user_sk.to_bytes().as_ref())),
);
// compute response
let response_opening = produce_response(
&r_com_opening,
&challenge,
&witness.joined_commitment_opening,
);
let response_openings = produce_responses(
&r_pedcom_openings,
&challenge,
&witness
.private_attributes_openings
.iter()
.collect::<Vec<_>>(),
);
let response_attributes = produce_responses(
&r_attributes,
&challenge,
&witness.private_attributes.iter().collect::<Vec<_>>(),
);
WithdrawalReqProof {
challenge,
response_opening,
response_openings,
response_attributes,
}
}
pub(crate) fn verify(
&self,
params: &GroupParameters,
instance: &WithdrawalReqInstance,
) -> bool {
// recompute zk commitments for each instance
let zkcm_com = instance.joined_commitment * self.challenge
+ params.gen1() * self.response_opening
+ self
.response_attributes
.iter()
.zip(params.gammas().iter())
.map(|(m_i, gamma_i)| gamma_i * m_i)
.sum::<G1Projective>();
let zkcm_pedcom = izip!(
instance.private_attributes_commitments.iter(),
self.response_openings.iter(),
self.response_attributes.iter()
)
.map(|(cm_j, resp_o_j, resp_m_j)| {
cm_j * self.challenge
+ params.gen1() * resp_o_j
+ instance.joined_commitment_hash * resp_m_j
})
.collect::<Vec<_>>();
let zk_commitment_user_sk =
instance.pk_user.pk * self.challenge + params.gen1() * self.response_attributes[0];
// covert to bytes
let gammas_bytes = params
.gammas()
.iter()
.map(|gamma| gamma.to_bytes())
.collect::<Vec<_>>();
let zkcm_pedcom_bytes = zkcm_pedcom
.iter()
.map(|cm| cm.to_bytes())
.collect::<Vec<_>>();
// recompute zkp challenge
let challenge = compute_challenge::<ChallengeDigest, _, _>(
std::iter::once(params.gen1().to_bytes().as_ref())
.chain(gammas_bytes.iter().map(|hs| hs.as_ref()))
.chain(std::iter::once(instance.to_bytes().as_ref()))
.chain(std::iter::once(zkcm_com.to_bytes().as_ref()))
.chain(zkcm_pedcom_bytes.iter().map(|c| c.as_ref()))
.chain(std::iter::once(zk_commitment_user_sk.to_bytes().as_ref())),
);
challenge == self.challenge
}
pub fn to_bytes(&self) -> Vec<u8> {
let challenge_bytes = self.challenge.to_bytes();
let response_opening_bytes = self.response_opening.to_bytes();
let ro_len = self.response_openings.len() as u64;
let ra_len = self.response_attributes.len() as u64;
let mut bytes =
Vec::with_capacity(32 + 32 + 8 + ro_len as usize * 32 + 8 + ra_len as usize * 32);
bytes.extend_from_slice(&challenge_bytes);
bytes.extend_from_slice(&response_opening_bytes);
bytes.extend_from_slice(&ro_len.to_le_bytes());
for ro in &self.response_openings {
bytes.extend_from_slice(&ro.to_bytes());
}
bytes.extend_from_slice(&ra_len.to_le_bytes());
for ra in &self.response_attributes {
bytes.extend_from_slice(&ra.to_bytes());
}
bytes
}
}
impl TryFrom<&[u8]> for WithdrawalReqProof {
type Error = CompactEcashError;
fn try_from(bytes: &[u8]) -> Result<WithdrawalReqProof> {
if bytes.len() < 32 + 32 + 16 + 32 + 32 || (bytes.len() - 16) % 32 != 0 {
return Err(CompactEcashError::Deserialization(
"tried to deserialize proof of withdrawal with bytes of invalid length".to_string(),
));
}
let mut idx = 0;
let challenge_bytes = bytes[idx..idx + 32].try_into().unwrap();
idx += 32;
let response_opening_bytes = bytes[idx..idx + 32].try_into().unwrap();
idx += 32;
let challenge = try_deserialize_scalar(
&challenge_bytes,
CompactEcashError::Deserialization("Failed to deserialize challenge".to_string()),
)?;
let response_opening = try_deserialize_scalar(
&response_opening_bytes,
CompactEcashError::Deserialization(
"Failed to deserialize the response to the random".to_string(),
),
)?;
let ro_len = u64::from_le_bytes(bytes[idx..idx + 8].try_into().unwrap());
idx += 8;
if bytes[idx..].len() < ro_len as usize * 32 + 8 {
return Err(CompactEcashError::Deserialization(
"tried to deserialize response openings".to_string(),
));
}
let ro_end = idx + ro_len as usize * 32;
let response_openings = try_deserialize_scalar_vec(
ro_len,
&bytes[idx..ro_end],
CompactEcashError::Deserialization(
"Failed to deserialize openings response".to_string(),
),
)?;
let ra_len = u64::from_le_bytes(bytes[ro_end..ro_end + 8].try_into().unwrap());
let response_attributes = try_deserialize_scalar_vec(
ra_len,
&bytes[ro_end + 8..],
CompactEcashError::Deserialization(
"Failed to deserialize attributes response".to_string(),
),
)?;
Ok(WithdrawalReqProof {
challenge,
response_opening,
response_openings,
response_attributes,
})
}
}
#[cfg(test)]
mod tests {
use group::Group;
use rand::thread_rng;
use crate::utils::hash_g1;
use super::*;
#[test]
fn withdrawal_request_instance_roundtrip() {
let mut rng = thread_rng();
let params = GroupParameters::new();
let instance = WithdrawalReqInstance {
joined_commitment: G1Projective::random(&mut rng),
joined_commitment_hash: G1Projective::random(&mut rng),
private_attributes_commitments: vec![
G1Projective::random(&mut rng),
G1Projective::random(&mut rng),
G1Projective::random(&mut rng),
],
pk_user: PublicKeyUser {
pk: params.gen1() * params.random_scalar(),
},
};
let instance_bytes = instance.to_bytes();
let instance_p = WithdrawalReqInstance::from_bytes(&instance_bytes).unwrap();
assert_eq!(instance, instance_p)
}
#[test]
fn withdrawal_proof_construct_and_verify() {
let _rng = thread_rng();
let params = GroupParameters::new();
let sk = params.random_scalar();
let pk_user = PublicKeyUser {
pk: params.gen1() * sk,
};
let v = params.random_scalar();
let t = params.random_scalar();
let private_attributes = vec![sk, v, t];
let joined_commitment_opening = params.random_scalar();
let joined_commitment = params.gen1() * joined_commitment_opening
+ private_attributes
.iter()
.zip(params.gammas())
.map(|(&m, gamma)| gamma * m)
.sum::<G1Projective>();
let joined_commitment_hash = hash_g1(joined_commitment.to_bytes());
let private_attributes_openings = params.n_random_scalars(private_attributes.len());
let private_attributes_commitments = private_attributes_openings
.iter()
.zip(private_attributes.iter())
.map(|(o_j, m_j)| params.gen1() * o_j + joined_commitment_hash * m_j)
.collect::<Vec<_>>();
let instance = WithdrawalReqInstance {
joined_commitment,
joined_commitment_hash,
private_attributes_commitments,
pk_user,
};
let witness = WithdrawalReqWitness {
private_attributes,
joined_commitment_opening,
private_attributes_openings,
};
let zk_proof = WithdrawalReqProof::construct(&params, &instance, &witness);
assert!(zk_proof.verify(&params, &instance))
}
}
@@ -0,0 +1,170 @@
use core::iter::Sum;
use core::ops::Mul;
use std::cell::Cell;
use bls12_381::{G2Prepared, G2Projective, Scalar};
use group::Curve;
use itertools::Itertools;
use crate::error::{CompactEcashError, Result};
use crate::scheme::keygen::{SecretKeyUser, VerificationKeyAuth};
use crate::scheme::setup::GroupParameters;
use crate::scheme::withdrawal::RequestInfo;
use crate::scheme::{PartialWallet, Wallet};
use crate::utils::{
check_bilinear_pairing, perform_lagrangian_interpolation_at_origin, PartialSignature,
Signature, SignatureShare, SignerIndex,
};
use crate::Attribute;
pub(crate) trait Aggregatable: Sized {
fn aggregate(aggregatable: &[Self], indices: Option<&[SignerIndex]>) -> Result<Self>;
fn check_unique_indices(indices: &[SignerIndex]) -> bool {
// if aggregation is a threshold one, all indices should be unique
indices.iter().unique_by(|&index| index).count() == indices.len()
}
}
impl<T> Aggregatable for T
where
T: Sum,
for<'a> T: Sum<&'a T>,
for<'a> &'a T: Mul<Scalar, Output = T>,
{
fn aggregate(aggregatable: &[T], indices: Option<&[u64]>) -> Result<T> {
if aggregatable.is_empty() {
return Err(CompactEcashError::Aggregation(
"Empty set of values".to_string(),
));
}
if let Some(indices) = indices {
if !Self::check_unique_indices(indices) {
return Err(CompactEcashError::Aggregation(
"Non-unique indices".to_string(),
));
}
perform_lagrangian_interpolation_at_origin(indices, aggregatable)
} else {
// non-threshold
Ok(aggregatable.iter().sum())
}
}
}
impl Aggregatable for PartialSignature {
fn aggregate(sigs: &[PartialSignature], indices: Option<&[u64]>) -> Result<Signature> {
let h = sigs
.first()
.ok_or_else(|| CompactEcashError::Aggregation("Empty set of signatures".to_string()))?
.sig1();
// TODO: is it possible to avoid this allocation?
let sigmas = sigs.iter().map(|sig| *sig.sig2()).collect::<Vec<_>>();
let aggr_sigma = Aggregatable::aggregate(&sigmas, indices)?;
Ok(Signature(*h, aggr_sigma))
}
}
/// Ensures all provided verification keys were generated to verify the same number of attributes.
fn check_same_key_size(keys: &[VerificationKeyAuth]) -> bool {
keys.iter().map(|vk| vk.beta_g1.len()).all_equal()
&& keys.iter().map(|vk| vk.beta_g2.len()).all_equal()
}
pub fn aggregate_verification_keys(
keys: &[VerificationKeyAuth],
indices: Option<&[SignerIndex]>,
) -> Result<VerificationKeyAuth> {
if !check_same_key_size(keys) {
return Err(CompactEcashError::Aggregation(
"Verification keys are of different sizes".to_string(),
));
}
Aggregatable::aggregate(keys, indices)
}
pub fn aggregate_signature_shares(
params: &GroupParameters,
verification_key: &VerificationKeyAuth,
attributes: &[Attribute],
shares: &[SignatureShare],
) -> Result<Signature> {
let (signatures, indices): (Vec<_>, Vec<_>) = shares
.iter()
.map(|share| (*share.signature(), share.index()))
.unzip();
aggregate_signatures(
params,
verification_key,
attributes,
&signatures,
Some(&indices),
)
}
pub fn aggregate_signatures(
params: &GroupParameters,
verification_key: &VerificationKeyAuth,
attributes: &[Attribute],
signatures: &[PartialSignature],
indices: Option<&[SignerIndex]>,
) -> Result<Signature> {
// aggregate the signature
let signature = match Aggregatable::aggregate(signatures, indices) {
Ok(res) => res,
Err(err) => return Err(err),
};
// Verify the signature
let tmp = attributes
.iter()
.zip(verification_key.beta_g2.iter())
.map(|(attr, beta_i)| beta_i * attr)
.sum::<G2Projective>();
if !check_bilinear_pairing(
&signature.0.to_affine(),
&G2Prepared::from((verification_key.alpha + tmp).to_affine()),
&signature.1.to_affine(),
params.prepared_miller_g2(),
) {
return Err(CompactEcashError::Aggregation(
"Verification of the aggregated signature failed".to_string(),
));
}
Ok(signature)
}
pub fn aggregate_wallets(
params: &GroupParameters,
verification_key: &VerificationKeyAuth,
sk_user: &SecretKeyUser,
wallets: &[PartialWallet],
req_info: &RequestInfo,
) -> Result<Wallet> {
// Aggregate partial wallets
let signature_shares: Vec<SignatureShare> = wallets
.iter()
.map(|wallet| SignatureShare::new(*wallet.signature(), wallet.index()))
.collect();
let attributes = vec![
sk_user.sk,
*req_info.get_v(),
*req_info.get_expiration_date(),
];
let aggregated_signature =
aggregate_signature_shares(params, verification_key, &attributes, &signature_shares)?;
Ok(Wallet {
sig: aggregated_signature,
v: *req_info.get_v(),
expiration_date: *req_info.get_expiration_date(),
l: Cell::new(0),
})
}
@@ -0,0 +1,467 @@
use crate::error::{CompactEcashError, Result};
use crate::scheme::keygen::{SecretKeyAuth, VerificationKeyAuth};
use crate::scheme::setup::{GroupParameters, Parameters};
use crate::traits::Bytable;
use crate::utils::hash_g1;
use crate::utils::{
check_bilinear_pairing, generate_lagrangian_coefficients_at_origin,
try_deserialize_g1_projective,
};
use crate::{constants, Base58};
use bls12_381::{G1Projective, G2Prepared, G2Projective, Scalar};
use chrono::{DateTime, Duration};
use group::Curve;
use itertools::Itertools;
use rayon::prelude::*;
/// A structure representing an expiration date signature.
#[derive(Debug, PartialEq, Clone)]
pub struct ExpirationDateSignature {
pub(crate) h: G1Projective,
pub(crate) s: G1Projective,
}
pub type PartialExpirationDateSignature = ExpirationDateSignature;
impl ExpirationDateSignature {
/// Function randomises the expiration date signature.
///
/// # Arguments
///
/// * `params` - A reference to group parameters used for the signature generation.
///
/// # Returns
///
/// A tuple containing the randomized expiration date signature and the blinding scalar.
pub fn randomise(&self, params: &GroupParameters) -> (ExpirationDateSignature, Scalar) {
// Generate random blinding scalars
let r = params.random_scalar();
let r_prime = params.random_scalar();
// Calculate h_prime and s_prime using the random scalars
let h_prime = self.h * r_prime;
let s_prime = (self.s * r_prime) + (h_prime * r);
(
ExpirationDateSignature {
h: h_prime,
s: s_prime,
},
r,
)
}
/// Converts the expiration date signature to a byte vector.
///
/// # Returns
///
/// A vector of bytes representing the expiration date signature.
pub fn to_bytes(&self) -> Vec<u8> {
let mut bytes: Vec<u8> = Vec::with_capacity(48 + 48);
bytes.extend(self.h.to_affine().to_compressed());
bytes.extend(self.s.to_affine().to_compressed());
bytes
}
}
impl TryFrom<&[u8]> for ExpirationDateSignature {
type Error = CompactEcashError;
fn try_from(bytes: &[u8]) -> Result<ExpirationDateSignature> {
if bytes.len() != 96 {
return Err(CompactEcashError::Deserialization(format!(
"ExpirationDateSignature must be exactly 96 bytes, got {}",
bytes.len()
)));
}
let h_bytes: &[u8; 48] = &bytes[..48].try_into().expect("Slice size != 48");
let s_bytes: &[u8; 48] = &bytes[48..].try_into().expect("Slice size != 48");
let h = try_deserialize_g1_projective(
h_bytes,
CompactEcashError::Deserialization(
"Failed to deserialize compressed h of the ExpirationDateSignature".to_string(),
),
)?;
let s = try_deserialize_g1_projective(
s_bytes,
CompactEcashError::Deserialization(
"Failed to deserialize compressed s of the ExpirationDateSignature".to_string(),
),
)?;
Ok(ExpirationDateSignature { h, s })
}
}
impl Bytable for ExpirationDateSignature {
fn to_byte_vec(&self) -> Vec<u8> {
self.to_bytes().to_vec()
}
fn try_from_byte_slice(slice: &[u8]) -> std::result::Result<Self, CompactEcashError> {
Self::try_from(slice)
}
}
impl Base58 for ExpirationDateSignature {}
/// Signs given expiration date for a specified validity period using the given secret key of a single authority.
///
/// # Arguments
///
/// * `params` - The cryptographic parameters used in the signing process.
/// * `sk_auth` - The secret key of the signing authority.
/// * `expiration_date` - The expiration date for which signatures will be generated (as unix timestamp).
///
/// # Returns
///
/// A vector containing partial signatures for each date within the validity period (i.e.,
/// from expiration_date - CRED_VALIDITY_PERIOD till expiration_date.
///
/// # Note
///
/// This function is executed by a single singing authority and generates partial expiration date
/// signatures for a specified validity period. Each signature is created by combining cryptographic
/// attributes derived from the expiration date, and the resulting vector contains signatures for
/// each date within the defined validity period till expiration date.
/// The validity period is determined by the constant `CRED_VALIDITY_PERIOD` in the `constants` module.
pub fn sign_expiration_date(
sk_auth: &SecretKeyAuth,
expiration_date: u64,
) -> Vec<PartialExpirationDateSignature> {
let m0: Scalar = Scalar::from(expiration_date);
let m2: Scalar = Scalar::from_bytes(&constants::TYPE_EXP).unwrap();
(0..constants::CRED_VALIDITY_PERIOD)
.into_par_iter()
.fold(Vec::new, |mut exp_signs, l| {
let expiration_date = DateTime::from_timestamp(expiration_date as i64, 0).unwrap();
let valid_date = expiration_date
- Duration::days(constants::CRED_VALIDITY_PERIOD as i64)
+ Duration::days(l as i64)
+ Duration::days(1i64);
let m1: Scalar = Scalar::from(valid_date.timestamp() as u64);
// Compute the hash
let h = hash_g1([m0.to_bytes(), m1.to_bytes()].concat());
// Sign the attributes by performing scalar-point multiplications and accumulating the result
let mut s_exponent = sk_auth.x;
s_exponent += sk_auth.ys[0] * m0;
s_exponent += sk_auth.ys[1] * m1;
s_exponent += sk_auth.ys[2] * m2;
// Create the signature struct on the expiration date
let exp_sign = PartialExpirationDateSignature {
h,
s: h * s_exponent,
};
exp_signs.push(exp_sign);
exp_signs
})
.reduce(Vec::new, |mut v1, mut v2| {
v1.append(&mut v2);
v1
})
}
/// Verifies the expiration date signatures against the given verification key.
///
/// This function iterates over the provided valid date signatures and verifies each one
/// against the provided verification key. It computes the hash and checks the correctness of the
/// signature using bilinear pairings.
///
/// # Arguments
///
/// * `params` - The cryptographic parameters used in the signing process.
/// * `vkey` - The verification key of the signing authority.
/// * `signatures` - The list of date signatures to be verified.
/// * `expiration_date` - The expiration date for which signatures are being issued (as unix timestamp).
///
/// # Returns
///
/// Returns `Ok(true)` if all signatures are verified successfully, otherwise returns an
/// `Err(CompactEcashError::ExpirationDate)` with an error message.
///
pub fn verify_valid_dates_signatures(
params: &Parameters,
vk: &VerificationKeyAuth,
signatures: &[ExpirationDateSignature],
expiration_date: u64,
) -> Result<()> {
let m0: Scalar = Scalar::from(expiration_date);
let m2: Scalar = Scalar::from_bytes(&constants::TYPE_EXP).unwrap();
signatures.par_iter().enumerate().try_for_each(|(l, sig)| {
let expiration_date = DateTime::from_timestamp(expiration_date as i64, 0).unwrap();
let valid_date = expiration_date - Duration::days(constants::CRED_VALIDITY_PERIOD as i64)
+ Duration::days(l as i64)
+ Duration::days(1i64);
let m1: Scalar = Scalar::from(valid_date.timestamp() as u64);
// Compute the hash
let h = hash_g1([m0.to_bytes(), m1.to_bytes()].concat());
// Verify the signature correctness
if sig.h != h {
return Err(CompactEcashError::ExpirationDate(
"Failed to verify the commitment hash".to_string(),
));
}
let partially_signed_attributes = [m0, m1, m2]
.iter()
.zip(vk.beta_g2.iter())
.map(|(m, beta_i)| beta_i * m)
.sum::<G2Projective>();
if !check_bilinear_pairing(
&sig.h.to_affine(),
&G2Prepared::from((vk.alpha + partially_signed_attributes).to_affine()),
&sig.s.to_affine(),
params.grp().prepared_miller_g2(),
) {
return Err(CompactEcashError::ExpirationDate(
"Verification of the date signature failed".to_string(),
));
}
Ok(())
})
}
/// Aggregates partial expiration date signatures into a list of aggregated expiration date signatures.
///
/// # Arguments
///
/// * `params` - The cryptographic parameters used in the signing process.
/// * `vk_auth` - The global verification key.
/// * `expiration_date` - The expiration date for which the signatures are being aggregated (as unix timestamp).
/// * `signatures` - A list of tuples containing unique indices, verification keys, and partial expiration date signatures corresponding to the signing authorities.
///
/// # Returns
///
/// A `Result` containing a vector of `ExpirationDateSignature` if the aggregation is successful,
/// or an `Err` variant with a description of the encountered error.
///
/// # Errors
///
/// This function returns an error if there is a mismatch in the lengths of `signatures`. This occurs
/// when the number of tuples in `signatures` is not equal to the expected number of signing authorities.
/// Each tuple should contain a unique index, a verification key, and a list of partial signatures.
///
/// It also returns an error if there are not enough unique indices. This happens when the number
/// of unique indices in the tuples is less than the total number of signing authorities.
///
/// Additionally, an error is returned if the verification of the partial or aggregated signatures fails.
/// This can occur if the cryptographic verification process fails for any of the provided signatures.
///
pub fn aggregate_expiration_signatures(
params: &Parameters,
vk: &VerificationKeyAuth,
expiration_date: u64,
signatures: &[(
u64,
VerificationKeyAuth,
Vec<PartialExpirationDateSignature>,
)],
) -> Result<Vec<ExpirationDateSignature>> {
// Check if all indices are unique
if signatures
.iter()
.map(|(index, _, _)| index)
.unique()
.count()
!= signatures.len()
{
return Err(CompactEcashError::ExpirationDate(
"Not enough unique indices shares".to_string(),
));
}
// Evaluate at 0 the Lagrange basis polynomials k_i
let coefficients = generate_lagrangian_coefficients_at_origin(
&signatures
.iter()
.map(|(index, _, _)| *index)
.collect::<Vec<_>>(),
);
// Verify that all signatures are valid
signatures
.par_iter()
.try_for_each(|(_, vk_auth, partial_signatures)| {
verify_valid_dates_signatures(params, vk_auth, partial_signatures, expiration_date)
})?;
// Pre-allocate vectors
let mut aggregated_date_signatures: Vec<ExpirationDateSignature> =
Vec::with_capacity(constants::CRED_VALIDITY_PERIOD as usize);
let m0: Scalar = Scalar::from(expiration_date);
for l in 0..constants::CRED_VALIDITY_PERIOD {
let expiration_date = DateTime::from_timestamp(expiration_date as i64, 0).unwrap();
let valid_date = expiration_date - Duration::days(constants::CRED_VALIDITY_PERIOD as i64)
+ Duration::days(l as i64)
+ Duration::days(1i64);
let m1: Scalar = Scalar::from(valid_date.timestamp() as u64);
// Compute the hash
let h = hash_g1([m0.to_bytes(), m1.to_bytes()].concat());
// Collect the partial signatures for the same valid date
let collected_at_l: Vec<_> = signatures
.iter()
.filter_map(|(_, _, inner_vec)| inner_vec.get(l as usize))
.cloned()
.collect();
// Aggregate partial signatures for each validity date
let aggr_s: G1Projective = coefficients
.iter()
.zip(collected_at_l.iter())
.map(|(coeff, sig)| sig.s * coeff)
.sum();
let aggr_sig = ExpirationDateSignature { h, s: aggr_s };
aggregated_date_signatures.push(aggr_sig);
}
verify_valid_dates_signatures(params, vk, &aggregated_date_signatures, expiration_date)?;
Ok(aggregated_date_signatures)
}
/// Finds the index corresponding to the given spend date based on the expiration date.
///
/// This function calculates the index such that the following equality holds:
/// `spend_date = expiration_date - 30 + index`
/// This index is used to retrieve a corresponding signature.
///
/// # Arguments
///
/// * `spend_date` - The spend date for which to find the index.
/// * `expiration_date` - The expiration date used in the calculation.
///
/// # Returns
///
/// If a valid index is found, returns `Ok(index)`. If no valid index is found
/// (i.e., `spend_date` is earlier than `expiration_date - 30`), returns `Err(InvalidDateError)`.
///
pub fn find_index(spend_date: Scalar, expiration_date: Scalar) -> Result<usize> {
let expiration_date_bytes = expiration_date.to_bytes();
let expiration_date_u64 = u64::from_le_bytes(expiration_date_bytes[..8].try_into().unwrap());
let spend_date_bytes = spend_date.to_bytes();
let spend_date_u64 = u64::from_le_bytes(spend_date_bytes[..8].try_into().unwrap());
let start_date = DateTime::from_timestamp(expiration_date_u64 as i64, 0).unwrap()
- Duration::days(constants::CRED_VALIDITY_PERIOD as i64)
+ Duration::days(1i64);
if DateTime::from_timestamp(spend_date_u64 as i64, 0).unwrap() >= start_date {
let index_a = (DateTime::from_timestamp(spend_date_u64 as i64, 0).unwrap() - start_date)
.num_days() as usize;
if index_a as u64 >= constants::CRED_VALIDITY_PERIOD {
Err(CompactEcashError::ExpirationDate(
"Spend_date is too late, no valid index".to_string(),
))
} else {
Ok(index_a)
}
} else {
Err(CompactEcashError::ExpirationDate(
"Spend_date is too early, no valid index".to_string(),
))
}
}
pub fn date_scalar(date: u64) -> Scalar {
Scalar::from(date)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::scheme::aggregation::aggregate_verification_keys;
use crate::scheme::keygen::ttp_keygen;
use crate::scheme::setup::setup;
#[test]
fn test_find_index() {
let expiration_date = 1701993600; // Dec 8 2023
let expiration_date_scalar = Scalar::from(expiration_date);
for i in 0..constants::CRED_VALIDITY_PERIOD {
let current_spend_date = expiration_date - i * 86400;
assert_eq!(
find_index(Scalar::from(current_spend_date), expiration_date_scalar).unwrap(),
(constants::CRED_VALIDITY_PERIOD - 1 - i) as usize
)
}
let late_spend_date = expiration_date + 86400;
assert!(find_index(Scalar::from(late_spend_date), expiration_date_scalar).is_err());
let early_spend_date = expiration_date - (constants::CRED_VALIDITY_PERIOD) * 86400;
assert!(find_index(Scalar::from(early_spend_date), expiration_date_scalar).is_err());
}
#[test]
fn test_sign_expiration_date() {
let total_coins = 32;
let params = setup(total_coins);
let expiration_date = 1702050209; // Dec 8 2023
let authorities_keys = ttp_keygen(params.grp(), 2, 3).unwrap();
let sk_i_auth = authorities_keys[0].secret_key();
let vk_i_auth = authorities_keys[0].verification_key();
let partial_exp_sig = sign_expiration_date(&sk_i_auth, expiration_date);
assert!(verify_valid_dates_signatures(
&params,
&vk_i_auth,
&partial_exp_sig,
expiration_date
)
.is_ok());
}
#[test]
fn test_aggregate_expiration_signatures() {
let total_coins = 32;
let params = setup(total_coins);
let expiration_date = 1702050209; // Dec 8 2023
let authorities_keypairs = ttp_keygen(params.grp(), 2, 3).unwrap();
let indices: [u64; 3] = [1, 2, 3];
// list of secret keys of each authority
let secret_keys_authorities: Vec<SecretKeyAuth> = authorities_keypairs
.iter()
.map(|keypair| keypair.secret_key())
.collect();
// list of verification keys of each authority
let verification_keys_auth: Vec<VerificationKeyAuth> = authorities_keypairs
.iter()
.map(|keypair| keypair.verification_key())
.collect();
// the global master verification key
let verification_key =
aggregate_verification_keys(&verification_keys_auth, Some(&indices)).unwrap();
let mut edt_partial_signatures: Vec<Vec<PartialExpirationDateSignature>> =
Vec::with_capacity(constants::CRED_VALIDITY_PERIOD as usize);
for sk_auth in secret_keys_authorities.iter() {
let sign = sign_expiration_date(sk_auth, expiration_date);
edt_partial_signatures.push(sign);
}
let combined_data: Vec<(
u64,
VerificationKeyAuth,
Vec<PartialExpirationDateSignature>,
)> = indices
.iter()
.zip(
verification_keys_auth
.iter()
.zip(edt_partial_signatures.iter()),
)
.map(|(i, (vk, sigs))| (*i, vk.clone(), sigs.clone()))
.collect();
assert!(aggregate_expiration_signatures(
&params,
&verification_key,
expiration_date,
&combined_data,
)
.is_ok());
}
}
@@ -0,0 +1,690 @@
use crate::constants;
use crate::error::Result;
use crate::scheme::expiration_date_signatures::{
aggregate_expiration_signatures, sign_expiration_date, ExpirationDateSignature,
PartialExpirationDateSignature,
};
use crate::scheme::keygen::{PublicKeyUser, SecretKeyAuth, VerificationKeyAuth};
use crate::scheme::setup::{
aggregate_indices_signatures, sign_coin_indices, CoinIndexSignature, Parameters,
PartialCoinIndexSignature,
};
use crate::scheme::{compute_pay_info_hash, Payment};
use crate::PayInfo;
#[derive(Debug, Eq, PartialEq)]
pub enum IdentifyResult {
NotADuplicatePayment,
DuplicatePayInfo(PayInfo),
DoubleSpendingPublicKeys(PublicKeyUser),
}
pub fn identify(
payment1: Payment,
payment2: Payment,
pay_info1: PayInfo,
pay_info2: PayInfo,
) -> IdentifyResult {
let mut k = 0;
let mut j = 0;
for (id1, pay1_ss) in payment1.ss.iter().enumerate() {
for (id2, pay2_ss) in payment2.ss.iter().enumerate() {
if pay1_ss == pay2_ss {
k = id1;
j = id2;
break;
}
}
}
if payment1
.ss
.iter()
.any(|pay1_ss| payment2.ss.contains(pay1_ss))
{
if pay_info1 == pay_info2 {
IdentifyResult::DuplicatePayInfo(pay_info1)
} else {
let rr_k_payment1 = compute_pay_info_hash(&pay_info1, k as u64);
let rr_j_payment2 = compute_pay_info_hash(&pay_info2, j as u64);
let rr_diff = rr_k_payment1 - rr_j_payment2;
let pk = (payment2.tt[j] * rr_k_payment1 - payment1.tt[k] * rr_j_payment2)
* rr_diff.invert().unwrap();
let pk_user = PublicKeyUser { pk };
IdentifyResult::DoubleSpendingPublicKeys(pk_user)
}
} else {
IdentifyResult::NotADuplicatePayment
}
}
pub fn generate_expiration_date_signatures(
params: &Parameters,
expiration_date: u64,
secret_keys_authorities: &[SecretKeyAuth],
verification_keys_auth: &[VerificationKeyAuth],
verification_key: &VerificationKeyAuth,
indices: &[u64],
) -> Result<Vec<ExpirationDateSignature>> {
let mut edt_partial_signatures: Vec<Vec<PartialExpirationDateSignature>> =
Vec::with_capacity(constants::CRED_VALIDITY_PERIOD as usize);
for sk_auth in secret_keys_authorities.iter() {
let sign = sign_expiration_date(sk_auth, expiration_date);
edt_partial_signatures.push(sign);
}
let combined_data: Vec<(
u64,
VerificationKeyAuth,
Vec<PartialExpirationDateSignature>,
)> = indices
.iter()
.zip(
verification_keys_auth
.iter()
.zip(edt_partial_signatures.iter()),
)
.map(|(i, (vk, sigs))| (*i, vk.clone(), sigs.clone()))
.collect();
aggregate_expiration_signatures(params, verification_key, expiration_date, &combined_data)
}
pub fn generate_coin_indices_signatures(
params: &Parameters,
secret_keys_authorities: &[SecretKeyAuth],
verification_keys_auth: &[VerificationKeyAuth],
verification_key: &VerificationKeyAuth,
indices: &[u64],
) -> Result<Vec<CoinIndexSignature>> {
// create the partial signatures from each authority
let partial_signatures: Vec<Vec<PartialCoinIndexSignature>> = secret_keys_authorities
.iter()
.map(|sk_auth| sign_coin_indices(params, verification_key, sk_auth))
.collect();
let combined_data: Vec<(u64, VerificationKeyAuth, Vec<PartialCoinIndexSignature>)> = indices
.iter()
.zip(verification_keys_auth.iter().zip(partial_signatures.iter()))
.map(|(i, (vk, sigs))| (*i, vk.clone(), sigs.clone()))
.collect();
aggregate_indices_signatures(params, verification_key, &combined_data)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::scheme::identify::{identify, IdentifyResult};
use crate::scheme::keygen::{PublicKeyUser, SecretKeyUser};
use crate::scheme::setup::setup;
use crate::{
aggregate_verification_keys, aggregate_wallets, generate_keypair_user, issue, issue_verify,
ttp_keygen, withdrawal_request, PartialWallet, PayInfo, VerificationKeyAuth,
};
use bls12_381::Scalar;
use itertools::izip;
#[test]
fn duplicate_payments_with_the_same_pay_info() {
let total_coins = 32;
let params = setup(total_coins);
// NOTE: Make sure that the date timestamp are calculated at 00:00:00!!
let expiration_date = 1703721600; // Dec 28 2023 00:00:00
let spend_date = Scalar::from(1701907200); // Dec 07 2023 00:00:00
let grp = params.grp();
let user_keypair = generate_keypair_user(grp);
let (req, req_info) =
withdrawal_request(grp, &user_keypair.secret_key(), expiration_date).unwrap();
let authorities_keypairs = ttp_keygen(grp, 2, 3).unwrap();
let indices: [u64; 3] = [1, 2, 3];
let secret_keys_authorities: Vec<SecretKeyAuth> = authorities_keypairs
.iter()
.map(|keypair| keypair.secret_key())
.collect();
let verification_keys_auth: Vec<VerificationKeyAuth> = authorities_keypairs
.iter()
.map(|keypair| keypair.verification_key())
.collect();
let verification_key =
aggregate_verification_keys(&verification_keys_auth, Some(&[1, 2, 3])).unwrap();
// generate valid dates signatures
let dates_signatures = generate_expiration_date_signatures(
&params,
expiration_date,
&secret_keys_authorities,
&verification_keys_auth,
&verification_key,
&indices,
)
.unwrap();
// generate coin indices signatures
let coin_indices_signatures = generate_coin_indices_signatures(
&params,
&secret_keys_authorities,
&verification_keys_auth,
&verification_key,
&indices,
)
.unwrap();
let mut wallet_blinded_signatures = Vec::new();
for auth_keypair in authorities_keypairs {
let blind_signature = issue(
grp,
auth_keypair.secret_key(),
user_keypair.public_key(),
&req,
expiration_date,
);
wallet_blinded_signatures.push(blind_signature.unwrap());
}
let unblinded_wallet_shares: Vec<PartialWallet> = izip!(
wallet_blinded_signatures.iter(),
verification_keys_auth.iter()
)
.enumerate()
.map(|(idx, (w, vk))| {
issue_verify(
grp,
vk,
&user_keypair.secret_key(),
w,
&req_info,
idx as u64 + 1,
)
.unwrap()
})
.collect();
// Aggregate partial wallets
let aggr_wallet = aggregate_wallets(
grp,
&verification_key,
&user_keypair.secret_key(),
&unblinded_wallet_shares,
&req_info,
)
.unwrap();
// Let's try to spend some coins
let pay_info1 = PayInfo {
pay_info_bytes: [6u8; 72],
};
let spend_vv = 1;
let (payment1, _upd_wallet) = aggr_wallet
.spend(
&params,
&verification_key,
&user_keypair.secret_key(),
&pay_info1,
false,
spend_vv,
dates_signatures,
coin_indices_signatures,
spend_date,
)
.unwrap();
assert!(payment1
.spend_verify(&params, &verification_key, &pay_info1, spend_date)
.unwrap());
let payment2 = payment1.clone();
assert!(payment2
.spend_verify(&params, &verification_key, &pay_info1, spend_date)
.unwrap());
let pay_info2 = pay_info1.clone();
let identify_result = identify(payment1, payment2, pay_info1.clone(), pay_info2.clone());
assert_eq!(
identify_result,
IdentifyResult::DuplicatePayInfo(pay_info1.clone())
);
}
#[test]
fn ok_if_two_different_payments() {
let total_coins = 32;
let params = setup(total_coins);
let grp = params.grp();
// NOTE: Make sure that the date timestamp are calculated at 00:00:00!!
let expiration_date = 1703721600; // Dec 28 2023 00:00:00
let spend_date = Scalar::from(1701907200); // Dec 07 2023 00:00:00
let user_keypair = generate_keypair_user(grp);
let (req, req_info) =
withdrawal_request(grp, &user_keypair.secret_key(), expiration_date).unwrap();
let authorities_keypairs = ttp_keygen(grp, 2, 3).unwrap();
let indices: [u64; 3] = [1, 2, 3];
let secret_keys_authorities: Vec<SecretKeyAuth> = authorities_keypairs
.iter()
.map(|keypair| keypair.secret_key())
.collect();
let verification_keys_auth: Vec<VerificationKeyAuth> = authorities_keypairs
.iter()
.map(|keypair| keypair.verification_key())
.collect();
let verification_key =
aggregate_verification_keys(&verification_keys_auth, Some(&[1, 2, 3])).unwrap();
// generate valid dates signatures
let dates_signatures = generate_expiration_date_signatures(
&params,
expiration_date,
&secret_keys_authorities,
&verification_keys_auth,
&verification_key,
&indices,
)
.unwrap();
// generate coin indices signatures
let coin_indices_signatures = generate_coin_indices_signatures(
&params,
&secret_keys_authorities,
&verification_keys_auth,
&verification_key,
&indices,
)
.unwrap();
let mut wallet_blinded_signatures = Vec::new();
for auth_keypair in authorities_keypairs {
let blind_signature = issue(
grp,
auth_keypair.secret_key(),
user_keypair.public_key(),
&req,
expiration_date,
);
wallet_blinded_signatures.push(blind_signature.unwrap());
}
let unblinded_wallet_shares: Vec<PartialWallet> = izip!(
wallet_blinded_signatures.iter(),
verification_keys_auth.iter()
)
.enumerate()
.map(|(idx, (w, vk))| {
issue_verify(
grp,
vk,
&user_keypair.secret_key(),
w,
&req_info,
idx as u64 + 1,
)
.unwrap()
})
.collect();
// Aggregate partial wallets
let aggr_wallet = aggregate_wallets(
grp,
&verification_key,
&user_keypair.secret_key(),
&unblinded_wallet_shares,
&req_info,
)
.unwrap();
// Let's try to spend some coins
let pay_info1 = PayInfo {
pay_info_bytes: [6u8; 72],
};
let spend_vv = 1;
let (payment1, upd_wallet) = aggr_wallet
.spend(
&params,
&verification_key,
&user_keypair.secret_key(),
&pay_info1,
false,
spend_vv,
dates_signatures.clone(),
coin_indices_signatures.clone(),
spend_date,
)
.unwrap();
assert!(payment1
.spend_verify(&params, &verification_key, &pay_info1, spend_date)
.unwrap());
let pay_info2 = PayInfo {
pay_info_bytes: [7u8; 72],
};
let (payment2, _) = upd_wallet
.spend(
&params,
&verification_key,
&user_keypair.secret_key(),
&pay_info2,
false,
spend_vv,
dates_signatures,
coin_indices_signatures,
spend_date,
)
.unwrap();
assert!(payment2
.spend_verify(&params, &verification_key, &pay_info2, spend_date)
.unwrap());
let identify_result = identify(payment1, payment2, pay_info1.clone(), pay_info2.clone());
assert_eq!(identify_result, IdentifyResult::NotADuplicatePayment);
}
#[test]
fn two_payments_with_one_repeating_serial_number_but_different_pay_info() {
let total_coins = 32;
let params = setup(total_coins);
let grp = params.grp();
// NOTE: Make sure that the date timestamp are calculated at 00:00:00!!
let expiration_date = 1703721600; // Dec 28 2023 00:00:00
let spend_date = Scalar::from(1701907200); // Dec 07 2023 00:00:00
let user_keypair = generate_keypair_user(grp);
// GENERATE KEYS FOR OTHER USERS
let mut public_keys: Vec<PublicKeyUser> = Default::default();
for _i in 0..50 {
let sk = grp.random_scalar();
let sk_user = SecretKeyUser { sk };
let pk_user = sk_user.public_key(grp);
public_keys.push(pk_user.clone());
}
public_keys.push(user_keypair.public_key().clone());
let (req, req_info) =
withdrawal_request(grp, &user_keypair.secret_key(), expiration_date).unwrap();
let authorities_keypairs = ttp_keygen(grp, 2, 3).unwrap();
let indices: [u64; 3] = [1, 2, 3];
let secret_keys_authorities: Vec<SecretKeyAuth> = authorities_keypairs
.iter()
.map(|keypair| keypair.secret_key())
.collect();
let verification_keys_auth: Vec<VerificationKeyAuth> = authorities_keypairs
.iter()
.map(|keypair| keypair.verification_key())
.collect();
let verification_key =
aggregate_verification_keys(&verification_keys_auth, Some(&[1, 2, 3])).unwrap();
// generate valid dates signatures
let dates_signatures = generate_expiration_date_signatures(
&params,
expiration_date,
&secret_keys_authorities,
&verification_keys_auth,
&verification_key,
&indices,
)
.unwrap();
// generate coin indices signatures
let coin_indices_signatures = generate_coin_indices_signatures(
&params,
&secret_keys_authorities,
&verification_keys_auth,
&verification_key,
&indices,
)
.unwrap();
let mut wallet_blinded_signatures = Vec::new();
for auth_keypair in authorities_keypairs {
let blind_signature = issue(
grp,
auth_keypair.secret_key(),
user_keypair.public_key(),
&req,
expiration_date,
);
wallet_blinded_signatures.push(blind_signature.unwrap());
}
let unblinded_wallet_shares: Vec<PartialWallet> = izip!(
wallet_blinded_signatures.iter(),
verification_keys_auth.iter()
)
.enumerate()
.map(|(idx, (w, vk))| {
issue_verify(
grp,
vk,
&user_keypair.secret_key(),
w,
&req_info,
idx as u64 + 1,
)
.unwrap()
})
.collect();
// Aggregate partial wallets
let aggr_wallet = aggregate_wallets(
grp,
&verification_key,
&user_keypair.secret_key(),
&unblinded_wallet_shares,
&req_info,
)
.unwrap();
// Let's try to spend some coins
let pay_info1 = PayInfo {
pay_info_bytes: [6u8; 72],
};
let spend_vv = 1;
let (payment1, _upd_wallet) = aggr_wallet
.spend(
&params,
&verification_key,
&user_keypair.secret_key(),
&pay_info1,
false,
spend_vv,
dates_signatures.clone(),
coin_indices_signatures.clone(),
spend_date,
)
.unwrap();
assert!(payment1
.spend_verify(&params, &verification_key, &pay_info1, spend_date)
.unwrap());
// let's reverse the spending counter in the wallet to create a double spending payment
let current_l = aggr_wallet.l.get();
aggr_wallet.l.set(current_l - 1);
let pay_info2 = PayInfo {
pay_info_bytes: [7u8; 72],
};
let (payment2, _) = aggr_wallet
.spend(
&params,
&verification_key,
&user_keypair.secret_key(),
&pay_info2,
false,
spend_vv,
dates_signatures.clone(),
coin_indices_signatures.clone(),
spend_date,
)
.unwrap();
assert!(payment2
.spend_verify(&params, &verification_key, &pay_info2, spend_date)
.unwrap());
let identify_result = identify(payment1, payment2, pay_info1.clone(), pay_info2.clone());
assert_eq!(
identify_result,
IdentifyResult::DoubleSpendingPublicKeys(user_keypair.public_key())
);
}
#[test]
fn two_payments_with_multiple_repeating_serial_numbers_but_different_pay_info() {
let total_coins = 32;
let params = setup(total_coins);
let grp = params.grp();
// NOTE: Make sure that the date timestamp are calculated at 00:00:00!!
let expiration_date = 1703721600; // Dec 28 2023 00:00:00
let spend_date = Scalar::from(1701907200); // Dec 07 2023 00:00:00
let user_keypair = generate_keypair_user(grp);
// GENERATE KEYS FOR OTHER USERS
let mut public_keys: Vec<PublicKeyUser> = Default::default();
for _ in 0..50 {
let sk = grp.random_scalar();
let sk_user = SecretKeyUser { sk };
let pk_user = sk_user.public_key(grp);
public_keys.push(pk_user.clone());
}
public_keys.push(user_keypair.public_key().clone());
let (req, req_info) =
withdrawal_request(grp, &user_keypair.secret_key(), expiration_date).unwrap();
let authorities_keypairs = ttp_keygen(grp, 2, 3).unwrap();
let indices: [u64; 3] = [1, 2, 3];
let secret_keys_authorities: Vec<SecretKeyAuth> = authorities_keypairs
.iter()
.map(|keypair| keypair.secret_key())
.collect();
let verification_keys_auth: Vec<VerificationKeyAuth> = authorities_keypairs
.iter()
.map(|keypair| keypair.verification_key())
.collect();
let verification_key =
aggregate_verification_keys(&verification_keys_auth, Some(&[1, 2, 3])).unwrap();
// generate valid dates signatures
let dates_signatures = generate_expiration_date_signatures(
&params,
expiration_date,
&secret_keys_authorities,
&verification_keys_auth,
&verification_key,
&indices,
)
.unwrap();
// generate coin indices signatures
let coin_indices_signatures = generate_coin_indices_signatures(
&params,
&secret_keys_authorities,
&verification_keys_auth,
&verification_key,
&indices,
)
.unwrap();
let mut wallet_blinded_signatures = Vec::new();
for auth_keypair in authorities_keypairs {
let blind_signature = issue(
grp,
auth_keypair.secret_key(),
user_keypair.public_key(),
&req,
expiration_date,
);
wallet_blinded_signatures.push(blind_signature.unwrap());
}
let unblinded_wallet_shares: Vec<PartialWallet> = izip!(
wallet_blinded_signatures.iter(),
verification_keys_auth.iter()
)
.enumerate()
.map(|(idx, (w, vk))| {
issue_verify(
grp,
vk,
&user_keypair.secret_key(),
w,
&req_info,
idx as u64 + 1,
)
.unwrap()
})
.collect();
// Aggregate partial wallets
let aggr_wallet = aggregate_wallets(
grp,
&verification_key,
&user_keypair.secret_key(),
&unblinded_wallet_shares,
&req_info,
)
.unwrap();
// Let's try to spend some coins
let pay_info1 = PayInfo {
pay_info_bytes: [6u8; 72],
};
let spend_vv = 10;
let (payment1, _) = aggr_wallet
.spend(
&params,
&verification_key,
&user_keypair.secret_key(),
&pay_info1,
false,
spend_vv,
dates_signatures.clone(),
coin_indices_signatures.clone(),
spend_date,
)
.unwrap();
assert!(payment1
.spend_verify(&params, &verification_key, &pay_info1, spend_date)
.unwrap());
// let's reverse the spending counter in the wallet to create a double spending payment
let current_l = aggr_wallet.l.get();
aggr_wallet.l.set(current_l - 10);
let pay_info2 = PayInfo {
pay_info_bytes: [7u8; 72],
};
let (payment2, _) = aggr_wallet
.spend(
&params,
&verification_key,
&user_keypair.secret_key(),
&pay_info2,
false,
spend_vv,
dates_signatures.clone(),
coin_indices_signatures.clone(),
spend_date,
)
.unwrap();
let identify_result = identify(payment1, payment2, pay_info1.clone(), pay_info2.clone());
assert_eq!(
identify_result,
IdentifyResult::DoubleSpendingPublicKeys(user_keypair.public_key())
);
}
}
@@ -0,0 +1,652 @@
use core::borrow::Borrow;
use core::iter::Sum;
use core::ops::{Add, Mul};
use std::convert::TryFrom;
use std::convert::TryInto;
use bls12_381::{G1Projective, G2Projective, Scalar};
use group::{Curve, GroupEncoding};
use nym_pemstore::traits::{PemStorableKey, PemStorableKeyPair};
use serde::{Deserialize, Serialize};
use crate::error::{CompactEcashError, Result};
use crate::scheme::aggregation::aggregate_verification_keys;
use crate::scheme::setup::GroupParameters;
use crate::scheme::SignerIndex;
use crate::traits::Bytable;
use crate::utils::{hash_to_scalar, Polynomial};
use crate::utils::{
try_deserialize_g1_projective, try_deserialize_g2_projective, try_deserialize_scalar,
try_deserialize_scalar_vec,
};
use crate::Base58;
use zeroize::{Zeroize, ZeroizeOnDrop};
#[derive(Debug, PartialEq, Clone, Zeroize, ZeroizeOnDrop)]
pub struct SecretKeyAuth {
pub(crate) x: Scalar,
pub(crate) ys: Vec<Scalar>,
}
impl PemStorableKey for SecretKeyAuth {
type Error = CompactEcashError;
fn pem_type() -> &'static str {
"ECASH SECRET KEY"
}
fn to_bytes(&self) -> Vec<u8> {
self.to_bytes()
}
fn from_bytes(bytes: &[u8]) -> std::result::Result<Self, Self::Error> {
Self::from_bytes(bytes)
}
}
impl TryFrom<&[u8]> for SecretKeyAuth {
type Error = CompactEcashError;
fn try_from(bytes: &[u8]) -> Result<SecretKeyAuth> {
// There should be x and at least one y
if bytes.len() < 32 * 2 + 8 || (bytes.len() - 8) % 32 != 0 {
return Err(CompactEcashError::DeserializationInvalidLength {
actual: bytes.len(),
modulus_target: bytes.len() - 8,
target: 32 * 2 + 8,
modulus: 32,
object: "secret key".to_string(),
});
}
// this conversion will not fail as we are taking the same length of data
let x_bytes: [u8; 32] = bytes[..32].try_into().unwrap();
let ys_len = u64::from_le_bytes(bytes[32..40].try_into().unwrap());
let actual_ys_len = (bytes.len() - 40) / 32;
if ys_len as usize != actual_ys_len {
return Err(CompactEcashError::Deserialization(format!(
"Tried to deserialize secret key with inconsistent ys len (expected {}, got {})",
ys_len, actual_ys_len
)));
}
let x = try_deserialize_scalar(
&x_bytes,
CompactEcashError::Deserialization(
"Failed to deserialize secret key scalar".to_string(),
),
)?;
let ys = try_deserialize_scalar_vec(
ys_len,
&bytes[40..],
CompactEcashError::Deserialization(
"Failed to deserialize secret key scalars".to_string(),
),
)?;
Ok(SecretKeyAuth { x, ys })
}
}
impl SecretKeyAuth {
/// Following a (distributed) key generation process, scalar values can be obtained
/// outside of the normal key generation process.
pub fn create_from_raw(x: Scalar, ys: Vec<Scalar>) -> Self {
Self { x, ys }
}
/// Extract the Scalar copy of the underlying secrets.
/// The caller of this function must exercise extreme care to not misuse the data and ensuring it gets zeroized
pub fn hazmat_to_raw(&self) -> (Scalar, Vec<Scalar>) {
(self.x, self.ys.clone())
}
pub fn size(&self) -> usize {
self.ys.len()
}
pub fn get_ys(&self) -> Vec<Scalar> {
self.ys.clone()
}
pub(crate) fn get_y_by_idx(&self, i: usize) -> Option<&Scalar> {
self.ys.get(i)
}
pub fn verification_key(&self, params: &GroupParameters) -> VerificationKeyAuth {
let g1 = params.gen1();
let g2 = params.gen2();
VerificationKeyAuth {
alpha: g2 * self.x,
beta_g1: self.ys.iter().map(|y| g1 * y).collect(),
beta_g2: self.ys.iter().map(|y| g2 * y).collect(),
}
}
pub fn to_bytes(&self) -> Vec<u8> {
let ys_len = self.ys.len();
let mut bytes = Vec::with_capacity(8 + (ys_len + 1) * 32);
bytes.extend_from_slice(&self.x.to_bytes());
bytes.extend_from_slice(&ys_len.to_le_bytes());
for y in self.ys.iter() {
bytes.extend_from_slice(&y.to_bytes())
}
bytes
}
pub fn from_bytes(bytes: &[u8]) -> Result<SecretKeyAuth> {
SecretKeyAuth::try_from(bytes)
}
}
#[derive(Debug, PartialEq, Clone, Zeroize, ZeroizeOnDrop)]
pub struct VerificationKeyAuth {
pub(crate) alpha: G2Projective,
pub(crate) beta_g1: Vec<G1Projective>,
pub(crate) beta_g2: Vec<G2Projective>,
}
impl PemStorableKey for VerificationKeyAuth {
type Error = CompactEcashError;
fn pem_type() -> &'static str {
"ECASH VERIFICATION KEY"
}
fn to_bytes(&self) -> Vec<u8> {
self.to_bytes()
}
fn from_bytes(bytes: &[u8]) -> std::result::Result<Self, Self::Error> {
Self::from_bytes(bytes)
}
}
impl TryFrom<&[u8]> for VerificationKeyAuth {
type Error = CompactEcashError;
fn try_from(bytes: &[u8]) -> Result<VerificationKeyAuth> {
// There should be at least alpha, one betaG1 and one betaG2 and their length
if bytes.len() < 96 * 2 + 48 + 8 || (bytes.len() - 8 - 96) % (96 + 48) != 0 {
return Err(CompactEcashError::DeserializationInvalidLength {
actual: bytes.len(),
modulus_target: bytes.len() - 8 - 96,
target: 96 * 2 + 48 + 8,
modulus: 96 + 48,
object: "verification key".to_string(),
});
}
// this conversion will not fail as we are taking the same length of data
let alpha_bytes: [u8; 96] = bytes[..96].try_into().unwrap();
let betas_len = u64::from_le_bytes(bytes[96..104].try_into().unwrap());
let actual_betas_len = (bytes.len() - 104) / (96 + 48);
if betas_len as usize != actual_betas_len {
return Err(
CompactEcashError::Deserialization(
format!("Tried to deserialize verification key with inconsistent betas len (expected {}, got {})",
betas_len, actual_betas_len
)));
}
let alpha = try_deserialize_g2_projective(
&alpha_bytes,
CompactEcashError::Deserialization(
"Failed to deserialize verification key G2 point (alpha)".to_string(),
),
)?;
let mut beta_g1 = Vec::with_capacity(betas_len as usize);
let mut beta_g1_end: u64 = 0;
for i in 0..betas_len {
let start = (104 + i * 48) as usize;
let end = start + 48;
let beta_i_bytes = bytes[start..end].try_into().unwrap();
let beta_i = try_deserialize_g1_projective(
&beta_i_bytes,
CompactEcashError::Deserialization(
"Failed to deserialize verification key G1 point (beta)".to_string(),
),
)?;
beta_g1_end = end as u64;
beta_g1.push(beta_i)
}
let mut beta_g2 = Vec::with_capacity(betas_len as usize);
for i in 0..betas_len {
let start = (beta_g1_end + i * 96) as usize;
let end = start + 96;
let beta_i_bytes = bytes[start..end].try_into().unwrap();
let beta_i = try_deserialize_g2_projective(
&beta_i_bytes,
CompactEcashError::Deserialization(
"Failed to deserialize verification key G2 point (beta)".to_string(),
),
)?;
beta_g2.push(beta_i)
}
Ok(VerificationKeyAuth {
alpha,
beta_g1,
beta_g2,
})
}
}
impl<'b> Add<&'b VerificationKeyAuth> for VerificationKeyAuth {
type Output = VerificationKeyAuth;
#[inline]
fn add(self, rhs: &'b VerificationKeyAuth) -> VerificationKeyAuth {
// If you're trying to add two keys together that were created
// for different number of attributes, just panic as it's a
// nonsense operation.
assert_eq!(
self.beta_g1.len(),
rhs.beta_g1.len(),
"trying to add verification keys generated for different number of attributes [G1]"
);
assert_eq!(
self.beta_g2.len(),
rhs.beta_g2.len(),
"trying to add verification keys generated for different number of attributes [G2]"
);
assert_eq!(
self.beta_g1.len(),
self.beta_g2.len(),
"this key is incorrect - the number of elements G1 and G2 does not match"
);
assert_eq!(
rhs.beta_g1.len(),
rhs.beta_g2.len(),
"they key you want to add is incorrect - the number of elements G1 and G2 does not match"
);
VerificationKeyAuth {
alpha: self.alpha + rhs.alpha,
beta_g1: self
.beta_g1
.iter()
.zip(rhs.beta_g1.iter())
.map(|(self_beta_g1, rhs_beta_g1)| self_beta_g1 + rhs_beta_g1)
.collect(),
beta_g2: self
.beta_g2
.iter()
.zip(rhs.beta_g2.iter())
.map(|(self_beta_g2, rhs_beta_g2)| self_beta_g2 + rhs_beta_g2)
.collect(),
}
}
}
impl<'a> Mul<Scalar> for &'a VerificationKeyAuth {
type Output = VerificationKeyAuth;
#[inline]
fn mul(self, rhs: Scalar) -> Self::Output {
VerificationKeyAuth {
alpha: self.alpha * rhs,
beta_g1: self.beta_g1.iter().map(|b_i| b_i * rhs).collect(),
beta_g2: self.beta_g2.iter().map(|b_i| b_i * rhs).collect(),
}
}
}
impl<T> Sum<T> for VerificationKeyAuth
where
T: Borrow<VerificationKeyAuth>,
{
#[inline]
fn sum<I>(iter: I) -> Self
where
I: Iterator<Item = T>,
{
let mut peekable = iter.peekable();
let head_attributes = match peekable.peek() {
Some(head) => head.borrow().beta_g2.len(),
None => {
// TODO: this is a really weird edge case. You're trying to sum an EMPTY iterator
// of VerificationKey. So should it panic here or just return some nonsense value?
return VerificationKeyAuth::identity(0);
}
};
peekable.fold(
VerificationKeyAuth::identity(head_attributes),
|acc, item| acc + item.borrow(),
)
}
}
impl VerificationKeyAuth {
/// Create a (kinda) identity verification key using specified
/// number of 'beta' elements
pub(crate) fn identity(beta_size: usize) -> Self {
VerificationKeyAuth {
alpha: G2Projective::identity(),
beta_g1: vec![G1Projective::identity(); beta_size],
beta_g2: vec![G2Projective::identity(); beta_size],
}
}
pub fn aggregate(sigs: &[Self], indices: Option<&[SignerIndex]>) -> Result<Self> {
aggregate_verification_keys(sigs, indices)
}
pub fn alpha(&self) -> &G2Projective {
&self.alpha
}
pub fn beta_g1(&self) -> &Vec<G1Projective> {
&self.beta_g1
}
pub fn beta_g2(&self) -> &Vec<G2Projective> {
&self.beta_g2
}
pub fn to_bytes(&self) -> Vec<u8> {
let beta_g1_len = self.beta_g1.len();
let beta_g2_len = self.beta_g2.len();
let mut bytes = Vec::with_capacity(96 + 8 + beta_g1_len * 48 + beta_g2_len * 96);
bytes.extend_from_slice(&self.alpha.to_affine().to_compressed());
bytes.extend_from_slice(&beta_g1_len.to_le_bytes());
for beta_g1 in self.beta_g1.iter() {
bytes.extend_from_slice(&beta_g1.to_affine().to_compressed())
}
for beta_g2 in self.beta_g2.iter() {
bytes.extend_from_slice(&beta_g2.to_affine().to_compressed())
}
bytes
}
pub fn from_bytes(bytes: &[u8]) -> Result<VerificationKeyAuth> {
VerificationKeyAuth::try_from(bytes)
}
}
impl Bytable for VerificationKeyAuth {
fn to_byte_vec(&self) -> Vec<u8> {
self.to_bytes().to_vec()
}
fn try_from_byte_slice(slice: &[u8]) -> std::result::Result<Self, CompactEcashError> {
Self::from_bytes(slice)
}
}
impl Base58 for VerificationKeyAuth {}
#[derive(Debug, PartialEq, Clone, Zeroize, ZeroizeOnDrop)]
pub struct SecretKeyUser {
pub sk: Scalar,
}
impl SecretKeyUser {
pub fn public_key(&self, params: &GroupParameters) -> PublicKeyUser {
PublicKeyUser {
pk: params.gen1() * self.sk,
}
}
pub fn to_bytes(&self) -> Vec<u8> {
self.sk.to_bytes().to_vec()
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
let sk = Scalar::try_from_byte_slice(bytes)?;
Ok(SecretKeyUser { sk })
}
}
impl Bytable for SecretKeyUser {
fn to_byte_vec(&self) -> Vec<u8> {
self.to_bytes().to_vec()
}
fn try_from_byte_slice(slice: &[u8]) -> std::result::Result<Self, CompactEcashError> {
Self::from_bytes(slice)
}
}
impl Base58 for SecretKeyUser {}
#[derive(Debug, Eq, PartialEq, Clone)]
pub struct PublicKeyUser {
pub(crate) pk: G1Projective,
}
impl PublicKeyUser {
pub fn to_base58_string(&self) -> String {
bs58::encode(&self.pk.to_bytes()).into_string()
}
pub fn from_base58_string<I: AsRef<[u8]>>(val: I) -> Result<Self> {
let bytes = bs58::decode(val)
.into_vec()
.map_err(|source| CompactEcashError::Deserialization(source.to_string()))?;
Self::from_bytes(&bytes)
}
pub fn to_bytes(&self) -> Vec<u8> {
self.pk.to_affine().to_compressed().to_vec()
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
if bytes.len() != 48 {
return Err(CompactEcashError::Deserialization(
"Failed to deserialize : Invalid length".to_string(),
));
}
let pk_bytes: &[u8; 48] = bytes[..48].try_into().unwrap();
let pk = try_deserialize_g1_projective(
pk_bytes,
CompactEcashError::Deserialization(
"Failed to deserialize verification key G1 point".to_string(),
),
)?;
Ok(PublicKeyUser { pk })
}
}
impl Bytable for PublicKeyUser {
fn to_byte_vec(&self) -> Vec<u8> {
self.to_bytes().to_vec()
}
fn try_from_byte_slice(slice: &[u8]) -> Result<Self> {
Self::from_bytes(slice)
}
}
impl Base58 for PublicKeyUser {}
#[derive(Debug, Zeroize, ZeroizeOnDrop)]
pub struct KeyPairAuth {
secret_key: SecretKeyAuth,
verification_key: VerificationKeyAuth,
/// Optional index value specifying polynomial point used during threshold key generation.
pub index: Option<SignerIndex>,
}
impl PemStorableKeyPair for KeyPairAuth {
type PrivatePemKey = SecretKeyAuth;
type PublicPemKey = VerificationKeyAuth;
fn private_key(&self) -> &Self::PrivatePemKey {
&self.secret_key
}
fn public_key(&self) -> &Self::PublicPemKey {
&self.verification_key
}
fn from_keys(secret_key: Self::PrivatePemKey, verification_key: Self::PublicPemKey) -> Self {
Self::from_keys(secret_key, verification_key)
}
}
impl KeyPairAuth {
pub fn new(
sk: SecretKeyAuth,
vk: VerificationKeyAuth,
index: Option<SignerIndex>,
) -> KeyPairAuth {
KeyPairAuth {
secret_key: sk,
verification_key: vk,
index,
}
}
pub fn from_keys(secret_key: SecretKeyAuth, verification_key: VerificationKeyAuth) -> Self {
Self {
secret_key,
verification_key,
index: None,
}
}
pub fn secret_key(&self) -> SecretKeyAuth {
self.secret_key.clone()
}
pub fn verification_key(&self) -> VerificationKeyAuth {
self.verification_key.clone()
}
}
#[derive(Debug, Clone, PartialEq, Zeroize, ZeroizeOnDrop, Serialize, Deserialize)]
pub struct KeyPairUser {
secret_key: SecretKeyUser,
#[zeroize(skip)]
public_key: PublicKeyUser,
}
impl KeyPairUser {
pub fn secret_key(&self) -> SecretKeyUser {
self.secret_key.clone()
}
pub fn public_key(&self) -> PublicKeyUser {
self.public_key.clone()
}
pub fn to_bytes(&self) -> Vec<u8> {
[self.secret_key.to_bytes(), self.public_key.to_bytes()].concat()
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
if bytes.len() != 32 + 48 {
return Err(CompactEcashError::Deserialization(
"Failed to deserialize keypair : Invalid length".to_string(),
));
}
let sk = SecretKeyUser::from_bytes(&bytes[..32])?;
let pk = PublicKeyUser::from_bytes(&bytes[32..32 + 48])?;
Ok(KeyPairUser {
secret_key: sk,
public_key: pk,
})
}
}
pub fn generate_keypair_user(params: &GroupParameters) -> KeyPairUser {
let sk_user = SecretKeyUser {
sk: params.random_scalar(),
};
let pk_user = PublicKeyUser {
pk: params.gen1() * sk_user.sk,
};
KeyPairUser {
secret_key: sk_user,
public_key: pk_user,
}
}
pub fn generate_keypair_user_from_seed(params: &GroupParameters, seed: &[u8]) -> KeyPairUser {
let sk_user = SecretKeyUser {
sk: hash_to_scalar(seed),
};
let pk_user = PublicKeyUser {
pk: params.gen1() * sk_user.sk,
};
KeyPairUser {
secret_key: sk_user,
public_key: pk_user,
}
}
pub fn ttp_keygen(
params: &GroupParameters,
threshold: u64,
num_authorities: u64,
) -> Result<Vec<KeyPairAuth>> {
if threshold == 0 {
return Err(CompactEcashError::Setup(
"Tried to generate threshold keys with a 0 threshold value".to_string(),
));
}
if threshold > num_authorities {
return Err(
CompactEcashError::Setup(
"Tried to generate threshold keys for threshold value being higher than number of the signing authorities".to_string(),
));
}
let attributes = params.gammas().len();
// generate polynomials
let v = Polynomial::new_random(params, threshold - 1);
let ws = (0..attributes + 1)
.map(|_| Polynomial::new_random(params, threshold - 1))
.collect::<Vec<_>>();
// TODO: potentially if we had some known authority identifier we could use that instead
// of the increasing (1,2,3,...) sequence
let polynomial_indices = (1..=num_authorities).collect::<Vec<_>>();
// generate polynomial shares
let x = polynomial_indices
.iter()
.map(|&id| v.evaluate(&Scalar::from(id)));
let ys = polynomial_indices.iter().map(|&id| {
ws.iter()
.map(|w| w.evaluate(&Scalar::from(id)))
.collect::<Vec<_>>()
});
// finally set the keys
let secret_keys = x.zip(ys).map(|(x, ys)| SecretKeyAuth { x, ys });
let keypairs = secret_keys
.zip(polynomial_indices.iter())
.map(|(secret_key, index)| {
let verification_key = secret_key.verification_key(params);
KeyPairAuth {
secret_key,
verification_key,
index: Some(*index),
}
})
.collect();
Ok(keypairs)
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,560 @@
use bls12_381::{G1Affine, G1Projective, G2Affine, G2Prepared, G2Projective, Scalar};
use ff::Field;
use group::{Curve, GroupEncoding};
use rand::thread_rng;
use crate::error::{CompactEcashError, Result};
use crate::{constants, Base58};
use crate::scheme::keygen::{SecretKeyAuth, VerificationKeyAuth};
use crate::traits::Bytable;
use crate::utils::{check_bilinear_pairing, generate_lagrangian_coefficients_at_origin};
use crate::utils::{hash_g1, try_deserialize_g1_projective};
use itertools::Itertools;
use rayon::prelude::*;
#[derive(Debug)]
pub struct GroupParameters {
/// Generator of the G1 group
g1: G1Affine,
/// Generator of the G2 group
g2: G2Affine,
/// Additional generators of the G1 group
gammas: Vec<G1Projective>,
// Additional generator of the G1 group
delta: G1Projective,
/// Precomputed G2 generator used for the miller loop
_g2_prepared_miller: G2Prepared,
}
impl GroupParameters {
pub fn new() -> GroupParameters {
let gammas = (1..=constants::ATTRIBUTES_LEN)
.map(|i| hash_g1(format!("gamma{}", i)))
.collect();
let delta = hash_g1("delta");
GroupParameters {
g1: G1Affine::generator(),
g2: G2Affine::generator(),
gammas,
delta,
_g2_prepared_miller: G2Prepared::from(G2Affine::generator()),
}
}
pub(crate) fn gen1(&self) -> &G1Affine {
&self.g1
}
pub(crate) fn gen2(&self) -> &G2Affine {
&self.g2
}
pub(crate) fn gammas(&self) -> &Vec<G1Projective> {
&self.gammas
}
pub(crate) fn gammas_to_bytes(&self) -> Vec<u8> {
let mut bytes = Vec::with_capacity(self.gammas.len() * 48);
for g in &self.gammas {
bytes.extend_from_slice(g.to_bytes().as_ref());
}
bytes
}
pub(crate) fn gamma_idx(&self, i: usize) -> Option<&G1Projective> {
self.gammas.get(i)
}
pub(crate) fn delta(&self) -> &G1Projective {
&self.delta
}
pub fn random_scalar(&self) -> Scalar {
// lazily-initialized thread-local random number generator, seeded by the system
let mut rng = thread_rng();
Scalar::random(&mut rng)
}
pub fn n_random_scalars(&self, n: usize) -> Vec<Scalar> {
(0..n).map(|_| self.random_scalar()).collect()
}
pub(crate) fn prepared_miller_g2(&self) -> &G2Prepared {
&self._g2_prepared_miller
}
}
impl Default for GroupParameters {
fn default() -> Self {
GroupParameters::new()
}
}
#[derive(Debug, PartialEq, Clone)]
pub struct SecretKeyRP {
pub(crate) x: Scalar,
pub(crate) y: Scalar,
}
impl SecretKeyRP {
pub fn public_key(&self, params: &GroupParameters) -> PublicKeyRP {
PublicKeyRP {
alpha: params.gen2() * self.x,
beta: params.gen2() * self.y,
}
}
}
#[derive(Debug, PartialEq, Clone)]
pub struct PublicKeyRP {
pub(crate) alpha: G2Projective,
pub(crate) beta: G2Projective,
}
#[derive(Debug)]
pub struct Parameters {
/// group parameters
grp: GroupParameters,
/// Number of coins of fixed denomination in the credential wallet; L in construction
total_coins: u64,
}
impl Parameters {
pub fn grp(&self) -> &GroupParameters {
&self.grp
}
pub fn get_total_coins(&self) -> u64 {
self.total_coins
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct CoinIndexSignature {
pub(crate) h: G1Projective,
pub(crate) s: G1Projective,
}
pub type PartialCoinIndexSignature = CoinIndexSignature;
impl CoinIndexSignature {
pub fn randomise(&self, params: &GroupParameters) -> (CoinIndexSignature, Scalar) {
let r = params.random_scalar();
let r_prime = params.random_scalar();
let h_prime = self.h * r_prime;
let s_prime = (self.s * r_prime) + (h_prime * r);
(
CoinIndexSignature {
h: h_prime,
s: s_prime,
},
r,
)
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut bytes: Vec<u8> = Vec::with_capacity(48 + 48);
bytes.extend(self.h.to_affine().to_compressed());
bytes.extend(self.s.to_affine().to_compressed());
bytes
}
}
impl TryFrom<&[u8]> for CoinIndexSignature {
type Error = CompactEcashError;
fn try_from(bytes: &[u8]) -> Result<CoinIndexSignature> {
if bytes.len() != 96 {
return Err(CompactEcashError::Deserialization(format!(
"CoinIndexSignature must be exactly 96 bytes, got {}",
bytes.len()
)));
}
let h_bytes: &[u8; 48] = &bytes[..48].try_into().expect("Slice size != 48");
let s_bytes: &[u8; 48] = &bytes[48..].try_into().expect("Slice size != 48");
let h = try_deserialize_g1_projective(
h_bytes,
CompactEcashError::Deserialization(
"Failed to deserialize compressed h of the CoinIndexSignature".to_string(),
),
)?;
let s = try_deserialize_g1_projective(
s_bytes,
CompactEcashError::Deserialization(
"Failed to deserialize compressed s of the CoinIndexSignature".to_string(),
),
)?;
Ok(CoinIndexSignature { h, s })
}
}
impl Bytable for CoinIndexSignature {
fn to_byte_vec(&self) -> Vec<u8> {
self.to_bytes().to_vec()
}
fn try_from_byte_slice(slice: &[u8]) -> std::result::Result<Self, CompactEcashError> {
Self::try_from(slice)
}
}
impl Base58 for CoinIndexSignature {}
/// Signs coin indices.
///
/// This function takes cryptographic parameters, a global verification key, and a secret key of the signing authority,
/// and generates partial coin index signatures for a specified number of indices using a parallel fold operation.
///
/// # Arguments
///
/// * `params` - The cryptographic parameters used in the signing process.
/// * `vk` - The global verification key.
/// * `sk_auth` - The secret key associated with the individual signing authority.
///
/// # Returns
///
/// A vector containing partial coin index signatures.
///
/// # Panics
///
/// The function may panic if there is an issue with converting bytes to Scalar during initialization.
///
pub fn sign_coin_indices(
params: &Parameters,
vk: &VerificationKeyAuth,
sk_auth: &SecretKeyAuth,
) -> Vec<PartialCoinIndexSignature> {
let m1: Scalar = Scalar::from_bytes(&constants::TYPE_IDX).unwrap();
let m2: Scalar = Scalar::from_bytes(&constants::TYPE_IDX).unwrap();
(0..params.get_total_coins())
.into_par_iter()
.fold(
|| Vec::with_capacity(params.get_total_coins() as usize),
|mut partial_coins_signatures, l| {
let m0: Scalar = Scalar::from(l);
// Compute the hash h
let mut concatenated_bytes =
Vec::with_capacity(vk.to_bytes().len() + l.to_le_bytes().len());
concatenated_bytes.extend_from_slice(&vk.to_bytes());
concatenated_bytes.extend_from_slice(&l.to_le_bytes());
let h = hash_g1(concatenated_bytes);
// Sign the attributes
let mut s_exponent = sk_auth.x;
s_exponent += sk_auth.ys[0] * m0;
s_exponent += sk_auth.ys[1] * m1;
s_exponent += sk_auth.ys[2] * m2;
// Create the signature struct
let coin_idx_sign = PartialCoinIndexSignature {
h,
s: h * s_exponent,
};
partial_coins_signatures.push(coin_idx_sign);
partial_coins_signatures
},
)
.reduce(Vec::new, |mut v1, mut v2| {
v1.append(&mut v2);
v1
})
}
/// Verifies coin index signatures using parallel iterators.
///
/// This function takes cryptographic parameters, verification keys, and a list of coin index
/// signatures. It verifies each signature's commitment hash and performs a bilinear pairing check.
///
/// # Arguments
///
/// * `params` - The cryptographic parameters used in the verification process.
/// * `vk` - The global verification key.
/// * `vk_auth` - The verification key associated with the authority which issued the partial signatures.
/// * `signatures` - A slice containing coin index signatures to be verified.
///
/// # Returns
///
/// Returns `Ok(())` if all signatures are valid, otherwise returns an error with a description
/// of the verification failure.
///
/// # Panics
///
/// The function may panic if there is an issue with converting bytes to Scalar during initialization.
pub fn verify_coin_indices_signatures(
params: &Parameters,
vk: &VerificationKeyAuth,
vk_auth: &VerificationKeyAuth,
signatures: &[CoinIndexSignature],
) -> Result<()> {
let m1: Scalar = Scalar::from_bytes(&constants::TYPE_IDX).unwrap();
let m2: Scalar = Scalar::from_bytes(&constants::TYPE_IDX).unwrap();
// Precompute concatenated_bytes for each l
let concatenated_bytes_list: Vec<Vec<u8>> = signatures
.iter()
.enumerate()
.map(|(l, _)| {
let mut concatenated_bytes =
Vec::with_capacity(vk.to_bytes().len() + l.to_le_bytes().len());
concatenated_bytes.extend_from_slice(&vk.to_bytes());
concatenated_bytes.extend_from_slice(&(l as u64).to_le_bytes());
concatenated_bytes
})
.collect();
// Create a vector of m0 values
let m0_values: Vec<Scalar> = (0..signatures.len() as u64).map(Scalar::from).collect();
// Verify signatures using precomputed concatenated_bytes and m0 values
m0_values
.par_iter()
.zip(
signatures
.par_iter()
.zip(concatenated_bytes_list.par_iter()),
)
.enumerate()
.try_for_each(|(_, (m0, (sig, concatenated_bytes)))| {
// Compute the hash h
let h = hash_g1(concatenated_bytes.clone());
// Check if the hash is matching
if sig.h != h {
return Err(CompactEcashError::CoinIndices(
"Failed to verify the commitment hash".to_string(),
));
}
let partially_signed_attributes = [*m0, m1, m2]
.iter()
.zip(vk_auth.beta_g2.iter())
.map(|(m, beta_i)| beta_i * m)
.sum::<G2Projective>();
if !check_bilinear_pairing(
&sig.h.to_affine(),
&G2Prepared::from((vk_auth.alpha + partially_signed_attributes).to_affine()),
&sig.s.to_affine(),
params.grp().prepared_miller_g2(),
) {
return Err(CompactEcashError::CoinIndices(
"Verification of the coin signature failed".to_string(),
));
}
Ok(())
})?;
Ok(())
}
/// Aggregates and verifies partial coin index signatures.
///
/// This function takes cryptographic parameters, a master verification key, and a list of tuples
/// containing indices, verification keys, and partial coin index signatures from different authorities.
/// It aggregates these partial signatures into a final set of coin index signatures, and verifying the
/// final aggregated signatures.
///
/// # Arguments
///
/// * `params` - The cryptographic parameters used in the aggregation process.
/// * `vk` - The master verification key against which the partial signatures are verified.
/// * `signatures` - A slice of tuples, where each tuple contains an index, a verification key, and
/// a vector of partial coin index signatures from a specific authority.
///
/// # Returns
///
/// Returns a vector of aggregated coin index signatures if the aggregation is successful.
/// Otherwise, returns an error describing the nature of the failure.
///
/// # Panics
///
/// The function may panic if there is an issue with converting bytes to Scalar during initialization.
pub fn aggregate_indices_signatures(
params: &Parameters,
vk: &VerificationKeyAuth,
signatures: &[(u64, VerificationKeyAuth, Vec<PartialCoinIndexSignature>)],
) -> Result<Vec<CoinIndexSignature>> {
// Check if all indices are unique
if signatures
.iter()
.map(|(index, _, _)| index)
.unique()
.count()
!= signatures.len()
{
return Err(CompactEcashError::CoinIndices(
"Not enough unique indices shares".to_string(),
));
}
// Evaluate at 0 the Lagrange basis polynomials k_i
let coefficients = generate_lagrangian_coefficients_at_origin(
&signatures
.iter()
.map(|(index, _, _)| *index)
.collect::<Vec<_>>(),
);
// Verify that all signatures are valid
signatures
.par_iter()
.try_for_each(|(_, vk_auth, partial_signatures)| {
verify_coin_indices_signatures(params, vk, vk_auth, partial_signatures)
})?;
// Pre-allocate vectors
let mut aggregated_coin_signatures: Vec<CoinIndexSignature> =
Vec::with_capacity(params.get_total_coins() as usize);
for l in 0..params.get_total_coins() {
// Compute the hash h
let mut concatenated_bytes =
Vec::with_capacity(vk.to_bytes().len() + l.to_le_bytes().len());
concatenated_bytes.extend_from_slice(&vk.to_bytes());
concatenated_bytes.extend_from_slice(&l.to_le_bytes());
let h = hash_g1(concatenated_bytes);
// Collect the partial signatures for the same coin index
let collected_at_l: Vec<_> = signatures
.iter()
.filter_map(|(_, _, inner_vec)| inner_vec.get(l as usize))
.cloned()
.collect();
// Aggregate partial signatures for each coin index
let aggr_s: G1Projective = coefficients
.iter()
.zip(collected_at_l.iter())
.map(|(coeff, sig)| sig.s * coeff)
.sum();
let aggr_sig = CoinIndexSignature { h, s: aggr_s };
aggregated_coin_signatures.push(aggr_sig);
}
verify_coin_indices_signatures(params, vk, vk, &aggregated_coin_signatures)?;
Ok(aggregated_coin_signatures)
}
/// Generates parameters for the scheme setup.
///
/// # Arguments
///
/// * `total_coins` - it is the number of coins in a freshly generated wallet. It is the public parameter of the scheme.
///
/// # Returns
///
/// A `Parameters` struct containing group parameters, public key, the number of signatures (`total_coins`),
/// and a map of signatures for each index `l`.
///
pub fn setup(total_coins: u64) -> Parameters {
let grp = GroupParameters::new();
Parameters { grp, total_coins }
}
#[cfg(test)]
mod tests {
use super::*;
use crate::scheme::aggregation::aggregate_verification_keys;
use crate::scheme::keygen::ttp_keygen;
#[test]
fn test_sign_coins() {
let total_coins = 32;
let params = setup(total_coins);
let authorities_keypairs = ttp_keygen(params.grp(), 2, 3).unwrap();
let indices: [u64; 3] = [1, 2, 3];
// Pick one authority to do the signing
let sk_i_auth = authorities_keypairs[0].secret_key();
let vk_i_auth = authorities_keypairs[0].verification_key();
// list of verification keys of each authority
let verification_keys_auth: Vec<VerificationKeyAuth> = authorities_keypairs
.iter()
.map(|keypair| keypair.verification_key())
.collect();
// the global master verification key
let verification_key =
aggregate_verification_keys(&verification_keys_auth, Some(&indices)).unwrap();
let partial_signatures = sign_coin_indices(&params, &verification_key, &sk_i_auth);
assert!(verify_coin_indices_signatures(
&params,
&verification_key,
&vk_i_auth,
&partial_signatures
)
.is_ok());
}
#[test]
fn test_sign_coins_fail() {
let total_coins = 32;
let params = setup(total_coins);
let authorities_keypairs = ttp_keygen(params.grp(), 2, 3).unwrap();
let indices: [u64; 3] = [1, 2, 3];
// Pick one authority to do the signing
let sk_0_auth = authorities_keypairs[0].secret_key();
let vk_1_auth = authorities_keypairs[1].verification_key();
// list of verification keys of each authority
let verification_keys_auth: Vec<VerificationKeyAuth> = authorities_keypairs
.iter()
.map(|keypair| keypair.verification_key())
.collect();
// the global master verification key
let verification_key =
aggregate_verification_keys(&verification_keys_auth, Some(&indices)).unwrap();
let partial_signatures = sign_coin_indices(&params, &verification_key, &sk_0_auth);
// Since we used a non matching verification key to verify the signature, the verification should fail
assert!(verify_coin_indices_signatures(
&params,
&verification_key,
&vk_1_auth,
&partial_signatures
)
.is_err());
}
#[test]
fn test_aggregate_coin_indices_signatures() {
let total_coins = 32;
let params = setup(total_coins);
let authorities_keypairs = ttp_keygen(params.grp(), 2, 3).unwrap();
let indices: [u64; 3] = [1, 2, 3];
// list of secret keys of each authority
let secret_keys_authorities: Vec<SecretKeyAuth> = authorities_keypairs
.iter()
.map(|keypair| keypair.secret_key())
.collect();
// list of verification keys of each authority
let verification_keys_auth: Vec<VerificationKeyAuth> = authorities_keypairs
.iter()
.map(|keypair| keypair.verification_key())
.collect();
// the global master verification key
let verification_key =
aggregate_verification_keys(&verification_keys_auth, Some(&indices)).unwrap();
// create the partial signatures from each authority
let partial_signatures: Vec<Vec<PartialCoinIndexSignature>> = secret_keys_authorities
.iter()
.map(|sk_auth| sign_coin_indices(&params, &verification_key, sk_auth))
.collect();
let combined_data: Vec<(u64, VerificationKeyAuth, Vec<PartialCoinIndexSignature>)> =
indices
.iter()
.zip(verification_keys_auth.iter().zip(partial_signatures.iter()))
.map(|(i, (vk, sigs))| (*i, vk.clone(), sigs.clone()))
.collect();
assert!(aggregate_indices_signatures(&params, &verification_key, &combined_data).is_ok());
}
}
@@ -0,0 +1,760 @@
use std::ops::Neg;
use bls12_381::{multi_miller_loop, G1Projective, G2Prepared, G2Projective, Scalar};
use group::{Curve, Group, GroupEncoding};
use crate::error::{CompactEcashError, Result};
use crate::proofs::proof_withdrawal::{
WithdrawalReqInstance, WithdrawalReqProof, WithdrawalReqWitness,
};
use crate::scheme::keygen::{PublicKeyUser, SecretKeyAuth, SecretKeyUser, VerificationKeyAuth};
use crate::scheme::setup::GroupParameters;
use crate::scheme::PartialWallet;
use crate::traits::Bytable;
use crate::utils::{
check_bilinear_pairing, hash_g1, try_deserialize_g1_projective, try_deserialize_scalar,
SignerIndex,
};
use crate::utils::{BlindedSignature, Signature};
use crate::{Attribute, Base58};
/// Represents a withdrawal request generate by the client who wants to obtain a zk-nym credential.
///
/// This struct encapsulates the necessary components for a withdrawal request, including the joined commitment hash, the joined commitment,
/// individual Pedersen commitments for private attributes, and a zero-knowledge proof for the withdrawal request.
///
/// # Fields
///
/// * `joined_commitment_hash` - The joined commitment hash represented as a G1Projective element.
/// * `joined_commitment` - The joined commitment represented as a G1Projective element.
/// * `private_attributes_commitments` - A vector of individual Pedersen commitments for private attributes represented as G1Projective elements.
/// * `zk_proof` - The zero-knowledge proof for the withdrawal request.
///
/// # Derives
///
/// The struct derives `Debug` and `PartialEq` to provide debug output and basic comparison functionality.
///
#[derive(Debug, PartialEq)]
pub struct WithdrawalRequest {
joined_commitment_hash: G1Projective,
joined_commitment: G1Projective,
private_attributes_commitments: Vec<G1Projective>,
zk_proof: WithdrawalReqProof,
}
impl WithdrawalRequest {
/// Converts the withdrawal request to a byte vector.
///
/// The resulting byte vector contains the serialized representation of the withdrawal request,
/// including the joined commitment hash, the joined commitment, individual commitments for private attributes,
/// and the zero-knowledge proof.
///
/// # Returns
///
/// A `Vec<u8>` containing the serialized representation of the withdrawal request.
///
pub fn to_bytes(&self) -> Vec<u8> {
let joined_commitment_hash_bytes = self.joined_commitment_hash.to_affine().to_compressed();
let joined_commitment_bytes = self.joined_commitment.to_affine().to_compressed();
let private_attributes_len_bytes =
(self.private_attributes_commitments.len() as u64).to_le_bytes();
let private_attributes_commitments_bytes: Vec<u8> = self
.private_attributes_commitments
.iter()
.flat_map(|c| c.to_affine().to_compressed())
.collect();
let zk_proof_bytes = self.zk_proof.to_bytes();
let total_bytes_len = joined_commitment_hash_bytes.len()
+ joined_commitment_bytes.len()
+ private_attributes_len_bytes.len()
+ private_attributes_commitments_bytes.len()
+ zk_proof_bytes.len();
let mut bytes = Vec::with_capacity(total_bytes_len);
bytes.extend_from_slice(&joined_commitment_hash_bytes);
bytes.extend_from_slice(&joined_commitment_bytes);
bytes.extend_from_slice(&private_attributes_len_bytes);
bytes.extend_from_slice(&private_attributes_commitments_bytes);
bytes.extend_from_slice(&zk_proof_bytes);
bytes
}
pub fn get_private_attributes_commitments(&self) -> &[G1Projective] {
&self.private_attributes_commitments
}
}
/// Attempts to deserialize a `WithdrawalRequest` from a byte slice.
///
/// # Arguments
///
/// * `bytes` - A byte slice containing the serialized `WithdrawalRequest`.
///
/// # Errors
///
/// Returns a `CompactEcashError` if deserialization fails, including cases where the byte slice
/// length is insufficient or deserialization of internal components fails.
///
impl TryFrom<&[u8]> for WithdrawalRequest {
type Error = CompactEcashError;
fn try_from(bytes: &[u8]) -> Result<WithdrawalRequest> {
let joined_commitment_hash_bytes_len = 48;
let joined_commitment_bytes_len = 48;
let private_attributes_len_bytes = 8;
let private_attributes_commitments_bytes_len = 48;
let min_length = joined_commitment_hash_bytes_len
+ joined_commitment_bytes_len
+ private_attributes_len_bytes
+ private_attributes_commitments_bytes_len;
if bytes.len() < min_length {
return Err(CompactEcashError::DeserializationMinLength {
min: min_length,
actual: bytes.len(),
});
}
let mut j = 0;
let joined_commitment_hash_bytes = bytes[..j + joined_commitment_hash_bytes_len]
.try_into()
.unwrap();
let joined_commitment_hash = try_deserialize_g1_projective(
&joined_commitment_hash_bytes,
CompactEcashError::Deserialization(
"Failed to deserialize compressed commitment hash".to_string(),
),
)?;
j += joined_commitment_hash_bytes_len;
let joined_commitment_bytes = bytes[j..j + joined_commitment_bytes_len]
.try_into()
.unwrap();
let joined_commitment = try_deserialize_g1_projective(
&joined_commitment_bytes,
CompactEcashError::Deserialization(
"Failed to deserialize compressed commitment".to_string(),
),
)?;
j += joined_commitment_bytes_len;
let private_attributes_len = u64::from_le_bytes(bytes[j..j + 8].try_into().unwrap());
j += 8;
if bytes[j..].len() < private_attributes_len as usize * 48 {
return Err(CompactEcashError::DeserializationMinLength {
min: private_attributes_len as usize * 48,
actual: bytes[56..].len(),
});
}
let mut private_attributes_commitments =
Vec::with_capacity(private_attributes_len as usize);
for i in 0..private_attributes_len as usize {
let start = j + i * 48;
let end = start + 48;
let pc_com_bytes = bytes[start..end].try_into().unwrap();
let pc_com = try_deserialize_g1_projective(
&pc_com_bytes,
CompactEcashError::Deserialization(
"Failed to deserialize compressed Pedersen commitment".to_string(),
),
)?;
private_attributes_commitments.push(pc_com)
}
let zk_proof =
WithdrawalReqProof::try_from(&bytes[j + private_attributes_len as usize * 48..])?;
Ok(WithdrawalRequest {
joined_commitment_hash,
joined_commitment,
private_attributes_commitments,
zk_proof,
})
}
}
impl Bytable for WithdrawalRequest {
fn to_byte_vec(&self) -> Vec<u8> {
self.to_bytes()
}
fn try_from_byte_slice(slice: &[u8]) -> Result<Self> {
WithdrawalRequest::try_from(slice)
}
}
impl Base58 for WithdrawalRequest {}
/// Represents information associated with a withdrawal request.
///
/// This structure holds the commitment hash, commitment opening, private attributes openings,
/// the wallet secret (scalar), and the expiration date related to a withdrawal request.
#[derive(Debug, Clone)]
pub struct RequestInfo {
joined_commitment_hash: G1Projective,
joined_commitment_opening: Scalar,
private_attributes_openings: Vec<Scalar>,
wallet_secret: Scalar,
expiration_date: Scalar,
}
impl RequestInfo {
pub fn get_joined_commitment_hash(&self) -> &G1Projective {
&self.joined_commitment_hash
}
pub fn get_joined_commitment_opening(&self) -> &Scalar {
&self.joined_commitment_opening
}
pub fn get_private_attributes_openings(&self) -> &[Scalar] {
&self.private_attributes_openings
}
pub fn get_v(&self) -> &Scalar {
&self.wallet_secret
}
pub fn get_expiration_date(&self) -> &Scalar {
&self.expiration_date
}
pub fn to_bytes(&self) -> Vec<u8> {
let com_hash_bytes = self.joined_commitment_hash.to_affine().to_compressed();
let com_opening_bytes = self.joined_commitment_opening.to_bytes();
let pr_coms_openings_len = self.private_attributes_openings.len() as u64;
let v_bytes = self.wallet_secret.to_bytes();
let exp_date_bytes = self.expiration_date.to_bytes();
let mut bytes = Vec::with_capacity(48 + 32 + 8 + pr_coms_openings_len as usize * 32 + 32);
bytes.extend_from_slice(&com_hash_bytes);
bytes.extend_from_slice(&com_opening_bytes);
bytes.extend_from_slice(&pr_coms_openings_len.to_le_bytes());
for c in &self.private_attributes_openings {
bytes.extend_from_slice(&c.to_bytes());
}
bytes.extend_from_slice(&v_bytes);
bytes.extend_from_slice(&exp_date_bytes);
bytes
}
}
impl TryFrom<&[u8]> for RequestInfo {
type Error = CompactEcashError;
fn try_from(bytes: &[u8]) -> Result<RequestInfo> {
if bytes.len() < 48 + 32 + 8 + 32 + 32 {
return Err(CompactEcashError::DeserializationMinLength {
min: 48 + 32 + 8 + 32 + 32,
actual: bytes.len(),
});
}
let mut j = 0;
let commitment_hash_bytes_len = 48;
let com_hash_bytes = bytes[j..j + commitment_hash_bytes_len].try_into().unwrap();
let com_hash = try_deserialize_g1_projective(
&com_hash_bytes,
CompactEcashError::Deserialization(
"Failed to deserialize compressed commitment hash".to_string(),
),
)?;
j += commitment_hash_bytes_len;
let com_opening_bytes_len = 32;
let com_opening_bytes = bytes[j..j + com_opening_bytes_len].try_into().unwrap();
let com_opening = try_deserialize_scalar(
&com_opening_bytes,
CompactEcashError::Deserialization(
"Failed to deserialize commitment opening".to_string(),
),
)?;
j += com_opening_bytes_len;
let pc_coms_openings_len = u64::from_le_bytes(bytes[j..j + 8].try_into().unwrap());
j += 8;
if bytes[j..].len() < pc_coms_openings_len as usize * 32 {
return Err(CompactEcashError::DeserializationMinLength {
min: pc_coms_openings_len as usize * 32,
actual: bytes[j..].len(),
});
}
let mut pc_coms_openings = Vec::with_capacity(pc_coms_openings_len as usize);
for i in 0..pc_coms_openings_len as usize {
let start = j + i * 32;
let end = start + 32;
let pc_com_opening_bytes = bytes[start..end].try_into().unwrap();
let pc_com_opening = try_deserialize_scalar(
&pc_com_opening_bytes,
CompactEcashError::Deserialization(
"Failed to deserialize compressed Pedersen commitment opening".to_string(),
),
)?;
pc_coms_openings.push(pc_com_opening)
}
j += pc_coms_openings_len as usize * 32;
let v_len = 32;
let exp_date_len = 32;
if bytes[j..].len() != v_len + exp_date_len {
return Err(CompactEcashError::DeserializationMinLength {
min: v_len,
actual: bytes[j..].len(),
});
}
let v_bytes = bytes[j..j + v_len].try_into().unwrap();
let v = try_deserialize_scalar(
v_bytes,
CompactEcashError::Deserialization("Failed to deserialize v".to_string()),
)?;
j += v_len;
let exp_date_bytes = bytes[j..j + exp_date_len].try_into().unwrap();
let exp_date = try_deserialize_scalar(
exp_date_bytes,
CompactEcashError::Deserialization("Failed to deserialize expiration date".to_string()),
)?;
Ok(RequestInfo {
joined_commitment_hash: com_hash,
joined_commitment_opening: com_opening,
private_attributes_openings: pc_coms_openings,
wallet_secret: v,
expiration_date: exp_date,
})
}
}
/// Computes Pedersen commitments for private attributes.
///
/// Given a set of private attributes and the commitment hash for all attributes,
/// this function generates random blinding factors (`openings`) and computes corresponding
/// Pedersen commitments for each private attribute.
/// Pedersen commitments have the hiding and binding properties, providing a secure way
/// to represent private values in a commitment scheme.
///
/// # Arguments
///
/// * `params` - Group parameters for the cryptographic group.
/// * `joined_commitment_hash` - The commitment hash to be used in the Pedersen commitments.
/// * `private_attributes` - A slice of private attributes to be committed.
///
/// # Returns
///
/// A tuple containing vectors of blinding factors (`openings`) and corresponding
/// Pedersen commitments for each private attribute.
fn compute_private_attribute_commitments(
params: &GroupParameters,
joined_commitment_hash: &G1Projective,
private_attributes: &[Scalar],
) -> (Vec<Scalar>, Vec<G1Projective>) {
let (openings, commitments): (Vec<Scalar>, Vec<G1Projective>) = private_attributes
.iter()
.map(|m_j| {
let o_j = params.random_scalar();
(o_j, params.gen1() * o_j + joined_commitment_hash * m_j)
})
.unzip();
(openings, commitments)
}
/// Generates a withdrawal request for the given user to request a zk-nym credential wallet.
///
/// # Arguments
///
/// * `params` - A reference to the group parameters used in the protocol.
/// * `sk_user` - A reference to the user's secret key.
/// * `expiration_date` - The expiration date for the withdrawal request.
///
/// # Returns
///
/// A tuple containing the generated `WithdrawalRequest` and `RequestInfo`, or an error if the operation fails.
///
/// # Details
///
/// The function starts by generating a random, unique wallet secret `v` and computing the joined commitment for all attributes,
/// including public (expiration date) and private ones (user secret key and wallet secret).
/// It then calculates the commitment hash (`joined_commitment_hash`) and computes Pedersen commitments for private attributes.
/// A zero-knowledge proof of knowledge is constructed to prove possession of specific attributes.
///
/// The resulting `WithdrawalRequest` includes the commitment hash, joined commitment, commitments for private
/// attributes, and the constructed zero-knowledge proof.
///
/// The associated `RequestInfo` includes information such as commitment hash, commitment opening,
/// openings for private attributes, `v`, and the expiration date.
pub fn withdrawal_request(
params: &GroupParameters,
sk_user: &SecretKeyUser,
expiration_date: u64,
) -> Result<(WithdrawalRequest, RequestInfo)> {
// Generate random and unique wallet secret
let v = params.random_scalar();
let joined_commitment_opening = params.random_scalar();
// Compute joined commitment for all attributes (public and private)
let joined_commitment: G1Projective = params.gen1() * joined_commitment_opening
+ params.gamma_idx(0).unwrap() * sk_user.sk
+ params.gamma_idx(1).unwrap() * v;
// Compute commitment hash h
let joined_commitment_hash = hash_g1(
(joined_commitment + params.gamma_idx(2).unwrap() * Scalar::from(expiration_date))
.to_bytes(),
);
// Compute Pedersen commitments for private attributes (wallet secret and user's secret)
let private_attributes = vec![sk_user.sk, v];
let (private_attributes_openings, private_attributes_commitments) =
compute_private_attribute_commitments(params, &joined_commitment_hash, &private_attributes);
// construct a NIZK proof of knowledge proving possession of m1, m2, o, o1, o2
let instance = WithdrawalReqInstance {
joined_commitment,
joined_commitment_hash,
private_attributes_commitments: private_attributes_commitments.clone(),
pk_user: PublicKeyUser {
pk: params.gen1() * sk_user.sk,
},
};
let witness = WithdrawalReqWitness {
private_attributes,
joined_commitment_opening,
private_attributes_openings: private_attributes_openings.clone(),
};
let zk_proof = WithdrawalReqProof::construct(params, &instance, &witness);
// Create and return WithdrawalRequest and RequestInfo
Ok((
WithdrawalRequest {
joined_commitment_hash,
joined_commitment,
private_attributes_commitments,
zk_proof,
},
RequestInfo {
joined_commitment_hash,
joined_commitment_opening,
private_attributes_openings: private_attributes_openings.clone(),
wallet_secret: v,
expiration_date: Scalar::from(expiration_date),
},
))
}
/// Verifies the integrity of a withdrawal request, including the joined commitment hash
/// and the zero-knowledge proof of knowledge.
///
/// # Arguments
///
/// * `params` - Group parameters used in the cryptographic operations.
/// * `req` - The withdrawal request to be verified.
/// * `pk_user` - Public key of the user associated with the withdrawal request.
/// * `expiration_date` - Expiration date for the withdrawal request.
///
/// # Returns
///
/// Returns `Ok(true)` if the verification is successful, otherwise returns an error
/// with a specific message indicating the verification failure.
pub fn request_verify(
params: &GroupParameters,
req: &WithdrawalRequest,
pk_user: PublicKeyUser,
expiration_date: u64,
) -> Result<bool> {
// Verify the joined commitment hash
let expected_commitment_hash = hash_g1(
(req.joined_commitment + params.gamma_idx(2).unwrap() * Scalar::from(expiration_date))
.to_bytes(),
);
if req.joined_commitment_hash != expected_commitment_hash {
return Err(CompactEcashError::WithdrawalRequestVerification(
"Failed to verify the commitment hash".to_string(),
));
}
// Verify zk proof
let instance = WithdrawalReqInstance {
joined_commitment: req.joined_commitment,
joined_commitment_hash: req.joined_commitment_hash,
private_attributes_commitments: req.private_attributes_commitments.clone(),
pk_user,
};
if !req.zk_proof.verify(params, &instance) {
return Err(CompactEcashError::WithdrawalRequestVerification(
"Failed to verify the proof of knowledge".to_string(),
));
}
Ok(true)
}
/// Function to blind sign a private attribute commitments.
/// Given a private attribute commitment (`private_attribute_commitment`) and an element of the signing key,
/// this function computes the blinded commitment by multiplying the commitment with the blinding factor.
///
/// # Arguments
///
/// * `private_attribute_commitment` - The G1Projective point representing the commitment to the private attribute.
/// * `yi` - The element of the secret key of the signing authority.
///
/// # Returns
///
/// A new G1Projective point representing the blinded commitment.
///
pub fn blind_sing_private_attribute(
private_attribute_commitment: &G1Projective,
yi: &Scalar,
) -> G1Projective {
private_attribute_commitment * yi
}
/// Signs an expiration date using a joined commitment hash and a secret key.
///
/// Given a joined commitment hash (`joined_commitment_hash`), an expiration date (`expiration_date`),
/// and a secret key for authentication (`sk_auth`), this function computes the signature of the
/// expiration date by multiplying the commitment hash with the blinding factor derived from the secret key
/// and the expiration date.
///
/// # Arguments
///
/// * `joined_commitment_hash` - The G1Projective point representing the joined commitment hash.
/// * `expiration_date` - The expiration date timestamp to be signed.
/// * `sk_auth` - The secret key of the signing authority.
///
/// # Returns
///
/// A `Result` containing the resulting G1Projective point if successful, or an error if the
/// authentication secret key index is out of bounds.
pub fn sign_expiration_date(
joined_commitment_hash: &G1Projective,
expiration_date: u64,
sk_auth: &SecretKeyAuth,
) -> Result<G1Projective> {
if let Some(yi) = sk_auth.get_y_by_idx(2) {
Ok(joined_commitment_hash * (yi * Scalar::from(expiration_date)))
} else {
Err(CompactEcashError::Issuance(
"The secret key of the authority does not have enough elements".to_string(),
))
}
}
/// Issues a blinded signature for a withdrawal request, after verifying its integrity.
///
/// This function first verifies the withdrawal request using the provided group parameters,
/// user's public key, and expiration date. If the verification is successful,
/// the function proceeds to blind sign the private attributes and sign the expiration date,
/// combining both signatures into a final signature.
///
/// # Arguments
///
/// * `params` - Group parameters used in the cryptographic operations.
/// * `sk_auth` - Secret key of the signing authority.
/// * `pk_user` - Public key of the user associated with the withdrawal request.
/// * `withdrawal_req` - The withdrawal request to be signed.
/// * `expiration_date` - Expiration date for the withdrawal request.
///
/// # Returns
///
/// Returns a `BlindedSignature` if the issuance process is successful, otherwise returns an error
/// with a specific message indicating the failure.
pub fn issue(
params: &GroupParameters,
sk_auth: SecretKeyAuth,
pk_user: PublicKeyUser,
withdrawal_req: &WithdrawalRequest,
expiration_date: u64,
) -> Result<BlindedSignature> {
// Verify the withdrawal request
request_verify(params, withdrawal_req, pk_user, expiration_date)?;
// Blind sign the private attributes
let blind_signatures: G1Projective = withdrawal_req
.private_attributes_commitments
.iter()
.zip(sk_auth.ys.iter().take(2))
.map(|(pc, yi)| blind_sing_private_attribute(pc, yi))
.sum();
// Sign the expiration date
let expiration_date_sign = sign_expiration_date(
&withdrawal_req.joined_commitment_hash,
expiration_date,
&sk_auth,
)?;
// Combine both signatures
let signature =
blind_signatures + withdrawal_req.joined_commitment_hash * sk_auth.x + expiration_date_sign;
Ok(BlindedSignature(
withdrawal_req.joined_commitment_hash,
signature,
))
}
/// Verifies the integrity and correctness of a blinded signature
/// and returns an unblinded partial zk-nym wallet.
///
/// This function first verifies the integrity of the received blinded signature by checking
/// if the joined commitment hash matches the one provided in the `req_info`. If the verification
/// is successful, it proceeds to unblind the blinded signature and verify its correctness.
///
/// # Arguments
///
/// * `params` - Group parameters used in the cryptographic operations.
/// * `vk_auth` - Verification key of the signing authority.
/// * `sk_user` - Secret key of the user.
/// * `blind_signature` - Blinded signature received from the authority.
/// * `req_info` - Information associated with the request, including the joined commitment hash,
/// private attributes openings, v, and expiration date.
///
/// # Returns
///
/// Returns a `PartialWallet` if the verification process is successful, otherwise returns an error
/// with a specific message indicating the failure.
pub fn issue_verify(
params: &GroupParameters,
vk_auth: &VerificationKeyAuth,
sk_user: &SecretKeyUser,
blind_signature: &BlindedSignature,
req_info: &RequestInfo,
signer_index: SignerIndex,
) -> Result<PartialWallet> {
// Verify the integrity of the response from the authority
if req_info.joined_commitment_hash != blind_signature.0 {
return Err(CompactEcashError::IssuanceVfy(
"Integrity verification failed".to_string(),
));
}
// Unblind the blinded signature on the partial signature
let blinding_removers = vk_auth
.beta_g1
.iter()
.zip(&req_info.private_attributes_openings)
.map(|(beta, opening)| beta * opening)
.sum::<G1Projective>();
let unblinded_c = blind_signature.1 - blinding_removers;
let attr = [sk_user.sk, req_info.wallet_secret, req_info.expiration_date];
let signed_attributes = attr
.iter()
.zip(vk_auth.beta_g2.iter())
.map(|(attr, beta_i)| beta_i * attr)
.sum::<G2Projective>();
// Verify the signature correctness on the wallet share
if !check_bilinear_pairing(
&blind_signature.0.to_affine(),
&G2Prepared::from((vk_auth.alpha + signed_attributes).to_affine()),
&unblinded_c.to_affine(),
params.prepared_miller_g2(),
) {
return Err(CompactEcashError::IssuanceVfy(
"Verification of wallet share failed".to_string(),
));
}
Ok(PartialWallet {
sig: Signature(blind_signature.0, unblinded_c),
v: req_info.wallet_secret,
idx: signer_index,
expiration_date: req_info.expiration_date,
})
}
/// Verifies a partial blind signature using the provided parameters and validator's verification key.
///
/// # Arguments
///
/// * `params` - Group parameters used in the cryptographic operations.
/// * `blind_sign_request` - A reference to the blind signature request signed by the client.
/// * `public_attributes` - A reference to the public attributes included in the client's request.
/// * `blind_sig` - A reference to the issued partial blinded signature to be verified.
/// * `partial_verification_key` - A reference to the validator's partial verification key.
///
/// # Returns
///
/// A boolean indicating whether the partial blind signature is valid (`true`) or not (`false`).
///
/// # Remarks
///
/// This function verifies the correctness and validity of a partial blind signature using
/// the provided cryptographic parameters, blind signature request, blinded signature,
/// and partial verification key.
/// It calculates pairings based on the provided values and checks whether the partial blind signature
/// is consistent with the verification key and commitments in the blind signature request.
/// The function returns `true` if the partial blind signature is valid, and `false` otherwise.
pub fn verify_partial_blind_signature(
params: &GroupParameters,
private_attribute_commitments: &[G1Projective],
public_attributes: &[&Attribute],
blind_sig: &BlindedSignature,
partial_verification_key: &VerificationKeyAuth,
) -> bool {
let num_private_attributes = private_attribute_commitments.len();
if num_private_attributes + public_attributes.len() > partial_verification_key.beta_g2.len() {
return false;
}
// TODO: we're losing some memory here due to extra allocation,
// but worst-case scenario (given SANE amount of attributes), it's just few kb at most
let c_neg = blind_sig.1.to_affine().neg();
let g2_prep = params.prepared_miller_g2();
let mut terms = vec![
// (c^{-1}, g2)
(c_neg, g2_prep.clone()),
// (s, alpha)
(
blind_sig.0.to_affine(),
G2Prepared::from(partial_verification_key.alpha.to_affine()),
),
];
// for each private attribute, add (cm_i, beta_i) to the miller terms
for (private_attr_commit, beta_g2) in private_attribute_commitments
.iter()
.zip(&partial_verification_key.beta_g2)
{
// (cm_i, beta_i)
terms.push((
private_attr_commit.to_affine(),
G2Prepared::from(beta_g2.to_affine()),
))
}
// for each public attribute, add (s^pub_j, beta_{priv + j}) to the miller terms
for (&pub_attr, beta_g2) in public_attributes.iter().zip(
partial_verification_key
.beta_g2
.iter()
.skip(num_private_attributes),
) {
// (s^pub_j, beta_j)
terms.push((
(blind_sig.0 * pub_attr).to_affine(),
G2Prepared::from(beta_g2.to_affine()),
))
}
// get the references to all the terms to get the arguments the miller loop expects
#[allow(clippy::map_identity)]
let terms_refs = terms.iter().map(|(g1, g2)| (g1, g2)).collect::<Vec<_>>();
// since checking whether e(a, b) == e(c, d)
// is equivalent to checking e(a, b) • e(c, d)^{-1} == id
// and thus to e(a, b) • e(c^{-1}, d) == id
//
// compute e(c^{-1}, g2) • e(s, alpha) • e(cm_0, beta_0) • e(cm_i, beta_i) • (s^pub_0, beta_{i+1}) (s^pub_j, beta_{i + j})
multi_miller_loop(&terms_refs)
.final_exponentiation()
.is_identity()
.into()
}
@@ -0,0 +1,213 @@
use crate::constants;
use crate::error::Result;
use crate::scheme::expiration_date_signatures::{
aggregate_expiration_signatures, sign_expiration_date, ExpirationDateSignature,
PartialExpirationDateSignature,
};
use crate::scheme::keygen::{SecretKeyAuth, VerificationKeyAuth};
use crate::scheme::setup::{
aggregate_indices_signatures, sign_coin_indices, CoinIndexSignature, Parameters,
PartialCoinIndexSignature,
};
//use bls12_381::Scalar;
pub fn generate_expiration_date_signatures(
params: &Parameters,
expiration_date: u64,
secret_keys_authorities: &[SecretKeyAuth],
verification_keys_auth: &[VerificationKeyAuth],
verification_key: &VerificationKeyAuth,
indices: &[u64],
) -> Result<Vec<ExpirationDateSignature>> {
let mut edt_partial_signatures: Vec<Vec<PartialExpirationDateSignature>> =
Vec::with_capacity(constants::CRED_VALIDITY_PERIOD as usize);
for sk_auth in secret_keys_authorities.iter() {
let sign = sign_expiration_date(sk_auth, expiration_date);
edt_partial_signatures.push(sign);
}
let combined_data: Vec<(
u64,
VerificationKeyAuth,
Vec<PartialExpirationDateSignature>,
)> = indices
.iter()
.zip(
verification_keys_auth
.iter()
.zip(edt_partial_signatures.iter()),
)
.map(|(i, (vk, sigs))| (*i, vk.clone(), sigs.clone()))
.collect();
aggregate_expiration_signatures(params, verification_key, expiration_date, &combined_data)
}
pub fn generate_coin_indices_signatures(
params: &Parameters,
secret_keys_authorities: &[SecretKeyAuth],
verification_keys_auth: &[VerificationKeyAuth],
verification_key: &VerificationKeyAuth,
indices: &[u64],
) -> Result<Vec<CoinIndexSignature>> {
// create the partial signatures from each authority
let partial_signatures: Vec<Vec<PartialCoinIndexSignature>> = secret_keys_authorities
.iter()
.map(|sk_auth| sign_coin_indices(params, verification_key, sk_auth))
.collect();
let combined_data: Vec<(u64, VerificationKeyAuth, Vec<PartialCoinIndexSignature>)> = indices
.iter()
.zip(verification_keys_auth.iter().zip(partial_signatures.iter()))
.map(|(i, (vk, sigs))| (*i, vk.clone(), sigs.clone()))
.collect();
aggregate_indices_signatures(params, verification_key, &combined_data)
}
#[cfg(test)]
mod tests {
use itertools::izip;
use crate::error::Result;
use crate::scheme::aggregation::{aggregate_verification_keys, aggregate_wallets};
use crate::scheme::keygen::{
generate_keypair_user, ttp_keygen, SecretKeyAuth, VerificationKeyAuth,
};
use crate::scheme::setup::setup;
use crate::scheme::withdrawal::{issue, issue_verify, withdrawal_request, WithdrawalRequest};
use crate::scheme::PayInfo;
use crate::scheme::{PartialWallet, Payment, Wallet};
use bls12_381::Scalar;
use super::*;
#[test]
fn main() -> Result<()> {
let total_coins = 32;
let params = setup(total_coins);
let grp_params = params.grp();
// NOTE: Make sure that the date timestamp are calculated at 00:00:00!!
let expiration_date = 1703721600; // Dec 28 2023 00:00:00
let spend_date = Scalar::from(1701907200); // Dec 07 2023 00:00:00
let user_keypair = generate_keypair_user(grp_params);
// generate authorities keys
let authorities_keypairs = ttp_keygen(grp_params, 2, 3).unwrap();
let indices: [u64; 3] = [1, 2, 3];
let secret_keys_authorities: Vec<SecretKeyAuth> = authorities_keypairs
.iter()
.map(|keypair| keypair.secret_key())
.collect();
let verification_keys_auth: Vec<VerificationKeyAuth> = authorities_keypairs
.iter()
.map(|keypair| keypair.verification_key())
.collect();
let verification_key =
aggregate_verification_keys(&verification_keys_auth, Some(&[1, 2, 3]))?;
// generate valid dates signatures
let dates_signatures = generate_expiration_date_signatures(
&params,
expiration_date,
&secret_keys_authorities,
&verification_keys_auth,
&verification_key,
&indices,
)?;
// generate coin indices signatures
let coin_indices_signatures = generate_coin_indices_signatures(
&params,
&secret_keys_authorities,
&verification_keys_auth,
&verification_key,
&indices,
)?;
// request a wallet
let (req, req_info) =
withdrawal_request(grp_params, &user_keypair.secret_key(), expiration_date).unwrap();
let req_bytes = req.to_bytes();
let req2 = WithdrawalRequest::try_from(req_bytes.as_slice()).unwrap();
assert_eq!(req, req2);
// issue partial wallets
let mut wallet_blinded_signatures = Vec::new();
for auth_keypair in authorities_keypairs {
let blind_signature = issue(
grp_params,
auth_keypair.secret_key(),
user_keypair.public_key(),
&req,
expiration_date,
);
wallet_blinded_signatures.push(blind_signature.unwrap());
}
let unblinded_wallet_shares: Vec<PartialWallet> = izip!(
wallet_blinded_signatures.iter(),
verification_keys_auth.iter()
)
.enumerate()
.map(|(idx, (w, vk))| {
issue_verify(
grp_params,
vk,
&user_keypair.secret_key(),
w,
&req_info,
idx as u64 + 1,
)
.unwrap()
})
.collect();
let partial_wallet = unblinded_wallet_shares.first().unwrap().clone();
let partial_wallet_bytes = partial_wallet.to_bytes();
let partial_wallet2 = PartialWallet::try_from(&partial_wallet_bytes[..]).unwrap();
assert_eq!(partial_wallet, partial_wallet2);
// Aggregate partial wallets
let aggr_wallet = aggregate_wallets(
grp_params,
&verification_key,
&user_keypair.secret_key(),
&unblinded_wallet_shares,
&req_info,
)?;
let wallet_bytes = aggr_wallet.to_bytes();
let wallet = Wallet::try_from(&wallet_bytes[..]).unwrap();
assert_eq!(aggr_wallet, wallet);
// Let's try to spend some coins
let pay_info = PayInfo {
pay_info_bytes: [6u8; 72],
};
let spend_vv = 1;
let (payment, _) = aggr_wallet.spend(
&params,
&verification_key,
&user_keypair.secret_key(),
&pay_info,
false,
spend_vv,
dates_signatures,
coin_indices_signatures,
spend_date,
)?;
assert!(payment
.spend_verify(&params, &verification_key, &pay_info, spend_date)
.unwrap());
let payment_bytes = payment.to_bytes();
let payment2 = Payment::try_from(&payment_bytes[..]).unwrap();
assert_eq!(payment, payment2);
Ok(())
}
}
@@ -0,0 +1,130 @@
use itertools::izip;
use crate::aggregate_verification_keys;
use crate::aggregate_wallets;
use crate::error::CompactEcashError;
use crate::generate_keypair_user;
use crate::issue;
use crate::issue_verify;
use crate::scheme::keygen::KeyPairAuth;
use crate::scheme::keygen::SecretKeyAuth;
use crate::scheme::Payment;
use crate::setup::setup;
use crate::setup::GroupParameters;
use crate::tests::e2e::generate_coin_indices_signatures;
use crate::tests::e2e::generate_expiration_date_signatures;
use crate::utils;
use crate::withdrawal_request;
use crate::PartialWallet;
use crate::PayInfo;
use crate::Scalar;
use crate::VerificationKeyAuth;
pub fn payment_from_keys_and_expiration_date(
grp_params: &GroupParameters,
ecash_keypairs: &Vec<KeyPairAuth>,
indices: &[utils::SignerIndex],
expiration_date: u64,
) -> Result<(Payment, PayInfo), CompactEcashError> {
let total_coins = 32;
let params = setup(total_coins);
let spend_date = Scalar::from(expiration_date - 29 * 86400);
let user_keypair = generate_keypair_user(grp_params);
let secret_keys_authorities: Vec<SecretKeyAuth> = ecash_keypairs
.iter()
.map(|keypair| keypair.secret_key())
.collect();
let verification_keys_auth: Vec<VerificationKeyAuth> = ecash_keypairs
.iter()
.map(|keypair| keypair.verification_key())
.collect();
// aggregate verification keys
let verification_key = aggregate_verification_keys(&verification_keys_auth, Some(indices))?;
// generate valid dates signatures
let dates_signatures = generate_expiration_date_signatures(
&params,
expiration_date,
&secret_keys_authorities,
&verification_keys_auth,
&verification_key,
indices,
)?;
// generate coin indices signatures
let coin_indices_signatures = generate_coin_indices_signatures(
&params,
&secret_keys_authorities,
&verification_keys_auth,
&verification_key,
indices,
)?;
// request a wallet
let (req, req_info) =
withdrawal_request(grp_params, &user_keypair.secret_key(), expiration_date).unwrap();
// generate blinded signatures
let mut wallet_blinded_signatures = Vec::new();
for keypair in ecash_keypairs {
let blinded_signature = issue(
grp_params,
keypair.secret_key(),
user_keypair.public_key(),
&req,
expiration_date,
)?;
wallet_blinded_signatures.push(blinded_signature)
}
// Unblind
let unblinded_wallet_shares: Vec<PartialWallet> = izip!(
wallet_blinded_signatures.iter(),
verification_keys_auth.iter()
)
.enumerate()
.map(|(idx, (w, vk))| {
issue_verify(
grp_params,
vk,
&user_keypair.secret_key(),
w,
&req_info,
idx as u64 + 1,
)
.unwrap()
})
.collect();
// Aggregate partial wallets
let aggr_wallet = aggregate_wallets(
grp_params,
&verification_key,
&user_keypair.secret_key(),
&unblinded_wallet_shares,
&req_info,
)?;
// Let's try to spend some coins
let pay_info = PayInfo {
pay_info_bytes: [6u8; 72],
};
let spend_vv = 1;
let (payment, _) = aggr_wallet.spend(
&params,
&verification_key,
&user_keypair.secret_key(),
&pay_info,
false,
spend_vv,
dates_signatures,
coin_indices_signatures,
spend_date,
)?;
Ok((payment, pay_info))
}
@@ -0,0 +1,2 @@
mod e2e;
pub mod helpers;
@@ -0,0 +1,54 @@
use bls12_381::{G1Affine, G1Projective};
use group::GroupEncoding;
use crate::CompactEcashError;
pub trait Bytable
where
Self: Sized,
{
fn to_byte_vec(&self) -> Vec<u8>;
fn try_from_byte_slice(slice: &[u8]) -> Result<Self, CompactEcashError>;
}
pub trait Base58
where
Self: Bytable,
{
fn try_from_bs58<S: AsRef<str>>(x: S) -> Result<Self, CompactEcashError> {
Self::try_from_byte_slice(&bs58::decode(x.as_ref()).into_vec().unwrap())
}
fn to_bs58(&self) -> String {
bs58::encode(self.to_byte_vec()).into_string()
}
}
impl Bytable for G1Projective {
fn to_byte_vec(&self) -> Vec<u8> {
self.to_bytes().as_ref().to_vec()
}
fn try_from_byte_slice(slice: &[u8]) -> Result<Self, CompactEcashError> {
let received = slice.len();
let arr: Result<[u8; 48], _> = slice.try_into();
let Ok(bytes) = arr else {
return Err(CompactEcashError::UnexpectedArrayLength {
typ: "G1Projective".to_string(),
received,
expected: 48,
});
};
let maybe_g1 = G1Affine::from_compressed(&bytes);
if maybe_g1.is_none().into() {
Err(CompactEcashError::G1ProjectiveDeserializationFailure)
} else {
// safety: this unwrap is fine as we've just checked the element is not none
#[allow(clippy::unwrap_used)]
Ok(maybe_g1.unwrap().into())
}
}
}
impl Base58 for G1Projective {}
@@ -0,0 +1,526 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use core::iter::Sum;
use core::ops::Mul;
use std::convert::{TryFrom, TryInto};
use std::ops::Neg;
use bls12_381::hash_to_curve::{ExpandMsgXmd, HashToCurve, HashToField};
use bls12_381::{
multi_miller_loop, G1Affine, G1Projective, G2Affine, G2Prepared, G2Projective, Scalar,
};
use ff::Field;
use group::{Curve, Group};
use crate::error::{CompactEcashError, Result};
use crate::scheme::setup::GroupParameters;
use crate::traits::Bytable;
use crate::{Base58, VerificationKeyAuth};
pub struct Polynomial {
coefficients: Vec<Scalar>,
}
impl Polynomial {
// for polynomial of degree n, we generate n+1 values
// (for example for degree 1, like y = x + 2, we need [2,1])
pub fn new_random(params: &GroupParameters, degree: u64) -> Self {
Polynomial {
coefficients: params.n_random_scalars((degree + 1) as usize),
}
}
/// Evaluates the polynomial at point x.
pub fn evaluate(&self, x: &Scalar) -> Scalar {
if self.coefficients.is_empty() {
Scalar::zero()
// if x is zero then we can ignore most of the expensive computation and
// just return the last term of the polynomial
} else if x.is_zero().unwrap_u8() == 1 {
// we checked that coefficients are not empty so unwrap here is fine
*self.coefficients.first().unwrap()
} else {
self.coefficients
.iter()
.enumerate()
// coefficient[n] * x ^ n
.map(|(i, coefficient)| coefficient * x.pow(&[i as u64, 0, 0, 0]))
.sum()
}
}
}
#[inline]
pub fn generate_lagrangian_coefficients_at_origin(points: &[u64]) -> Vec<Scalar> {
let x = Scalar::zero();
points
.iter()
.enumerate()
.map(|(i, point_i)| {
let mut numerator = Scalar::one();
let mut denominator = Scalar::one();
let xi = Scalar::from(*point_i);
for (j, point_j) in points.iter().enumerate() {
if j != i {
let xj = Scalar::from(*point_j);
// numerator = (x - xs[0]) * ... * (x - xs[j]), j != i
numerator *= x - xj;
// denominator = (xs[i] - x[0]) * ... * (xs[i] - x[j]), j != i
denominator *= xi - xj;
}
}
// numerator / denominator
numerator * denominator.invert().unwrap()
})
.collect()
}
/// Performs a Lagrange interpolation at the origin for a polynomial defined by `points` and `values`.
/// It can be used for Scalars, G1 and G2 points.
pub(crate) fn perform_lagrangian_interpolation_at_origin<T>(
points: &[SignerIndex],
values: &[T],
) -> Result<T>
where
T: Sum,
for<'a> &'a T: Mul<Scalar, Output = T>,
{
if points.is_empty() || values.is_empty() {
return Err(CompactEcashError::Interpolation(
"Tried to perform lagrangian interpolation for an empty set of coordinates".to_string(),
));
}
if points.len() != values.len() {
return Err(CompactEcashError::Interpolation(
"Tried to perform lagrangian interpolation for an incomplete set of coordinates"
.to_string(),
));
}
let coefficients = generate_lagrangian_coefficients_at_origin(points);
Ok(coefficients
.into_iter()
.zip(values.iter())
.map(|(coeff, val)| val * coeff)
.sum())
}
// A temporary way of hashing particular message into G1.
// Implementation idea was taken from `threshold_crypto`:
// https://github.com/poanetwork/threshold_crypto/blob/7709462f2df487ada3bb3243060504b5881f2628/src/lib.rs#L691
// Eventually it should get replaced by, most likely, the osswu map
// method once ideally it's implemented inside the pairing crate.
// note: I have absolutely no idea what are the correct domains for those. I just used whatever
// was given in the test vectors of `Hashing to Elliptic Curves draft-irtf-cfrg-hash-to-curve-11`
// https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-11#appendix-J.9.1
const G1_HASH_DOMAIN: &[u8] = b"QUUX-V01-CS02-with-BLS12381G1_XMD:SHA-256_SSWU_RO_";
// https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-11#appendix-K.1
const SCALAR_HASH_DOMAIN: &[u8] = b"QUUX-V01-CS02-with-expander";
pub fn hash_g1<M: AsRef<[u8]>>(msg: M) -> G1Projective {
<G1Projective as HashToCurve<ExpandMsgXmd<sha2::Sha256>>>::hash_to_curve(msg, G1_HASH_DOMAIN)
}
pub fn hash_to_scalar<M: AsRef<[u8]>>(msg: M) -> Scalar {
let mut output = vec![Scalar::zero()];
Scalar::hash_to_field::<ExpandMsgXmd<sha2::Sha256>>(
msg.as_ref(),
SCALAR_HASH_DOMAIN,
&mut output,
);
output[0]
}
pub fn try_deserialize_scalar_vec(
expected_len: u64,
bytes: &[u8],
err: CompactEcashError,
) -> Result<Vec<Scalar>> {
if bytes.len() != expected_len as usize * 32 {
return Err(err);
}
let mut out = Vec::with_capacity(expected_len as usize);
for i in 0..expected_len as usize {
let s_bytes = bytes[i * 32..(i + 1) * 32].try_into().unwrap();
let s = match Into::<Option<Scalar>>::into(Scalar::from_bytes(&s_bytes)) {
None => return Err(err),
Some(scalar) => scalar,
};
out.push(s)
}
Ok(out)
}
pub fn try_deserialize_scalar(bytes: &[u8; 32], err: CompactEcashError) -> Result<Scalar> {
Into::<Option<Scalar>>::into(Scalar::from_bytes(bytes)).ok_or(err)
}
pub fn try_deserialize_g1_projective(
bytes: &[u8; 48],
err: CompactEcashError,
) -> Result<G1Projective> {
Into::<Option<G1Affine>>::into(G1Affine::from_compressed(bytes))
.ok_or(err)
.map(G1Projective::from)
}
pub fn try_deserialize_g2_projective(
bytes: &[u8; 96],
err: CompactEcashError,
) -> Result<G2Projective> {
Into::<Option<G2Affine>>::into(G2Affine::from_compressed(bytes))
.ok_or(err)
.map(G2Projective::from)
}
/// Checks whether e(P, Q) * e(-R, S) == id
pub fn check_bilinear_pairing(p: &G1Affine, q: &G2Prepared, r: &G1Affine, s: &G2Prepared) -> bool {
// checking e(P, Q) * e(-R, S) == id
// is equivalent to checking e(P, Q) == e(R, S)
// but requires only a single final exponentiation rather than two of them
// and therefore, as seen via benchmarks.rs, is almost 50% faster
// (1.47ms vs 2.45ms, tested on R9 5900X)
let multi_miller = multi_miller_loop(&[(p, q), (&r.neg(), s)]);
multi_miller.final_exponentiation().is_identity().into()
}
pub fn check_vk_pairing(
params: &GroupParameters,
dkg_values: &[G2Projective],
vk: &VerificationKeyAuth,
) -> bool {
let values_len = dkg_values.len();
if values_len == 0 || values_len - 1 != vk.beta_g1.len() || values_len - 1 != vk.beta_g2.len() {
return false;
}
// safety: we made an explicit check for if the length of the slice is 0, thus unwrap here is fine
#[allow(clippy::unwrap_used)]
if &vk.alpha != *dkg_values.first().as_ref().unwrap() {
return false;
}
let dkg_betas = &dkg_values[1..];
if dkg_betas
.iter()
.zip(vk.beta_g2.iter())
.any(|(dkg_beta, vk_beta)| dkg_beta != vk_beta)
{
return false;
}
if vk.beta_g1.iter().zip(vk.beta_g2.iter()).any(|(g1, g2)| {
!check_bilinear_pairing(
params.gen1(),
&G2Prepared::from(g2.to_affine()),
&g1.to_affine(),
params.prepared_miller_g2(),
)
}) {
return false;
}
true
}
pub type SignerIndex = u64;
#[derive(Debug, Clone, Copy, PartialEq)]
// #[cfg_attr(test, derive(PartialEq))]
pub struct Signature(pub(crate) G1Projective, pub(crate) G1Projective);
pub type PartialSignature = Signature;
impl TryFrom<&[u8]> for Signature {
type Error = CompactEcashError;
fn try_from(bytes: &[u8]) -> Result<Signature> {
if bytes.len() != 96 {
return Err(CompactEcashError::Deserialization(format!(
"Signature must be exactly 96 bytes, got {}",
bytes.len()
)));
}
let sig1_bytes: &[u8; 48] = &bytes[..48].try_into().expect("Slice size != 48");
let sig2_bytes: &[u8; 48] = &bytes[48..].try_into().expect("Slice size != 48");
let sig1 = try_deserialize_g1_projective(
sig1_bytes,
CompactEcashError::Deserialization("Failed to deserialize compressed sig1".to_string()),
)?;
let sig2 = try_deserialize_g1_projective(
sig2_bytes,
CompactEcashError::Deserialization("Failed to deserialize compressed sig2".to_string()),
)?;
Ok(Signature(sig1, sig2))
}
}
impl Signature {
pub(crate) fn sig1(&self) -> &G1Projective {
&self.0
}
pub(crate) fn sig2(&self) -> &G1Projective {
&self.1
}
pub fn randomise(&self, params: &GroupParameters) -> (Signature, Scalar) {
let r = params.random_scalar();
let r_prime = params.random_scalar();
let h_prime = self.0 * r_prime;
let s_prime = (self.1 * r_prime) + (h_prime * r);
(Signature(h_prime, s_prime), r)
}
pub fn to_bytes(self) -> [u8; 96] {
let mut bytes = [0u8; 96];
bytes[..48].copy_from_slice(&self.0.to_affine().to_compressed());
bytes[48..].copy_from_slice(&self.1.to_affine().to_compressed());
bytes
}
pub fn from_bytes(bytes: &[u8]) -> Result<Signature> {
Signature::try_from(bytes)
}
}
impl Bytable for Signature {
fn to_byte_vec(&self) -> Vec<u8> {
self.to_bytes().to_vec()
}
fn try_from_byte_slice(slice: &[u8]) -> Result<Self> {
Signature::from_bytes(slice)
}
}
#[derive(Debug, PartialEq)]
pub struct BlindedSignature(pub(crate) G1Projective, pub(crate) G1Projective);
impl TryFrom<&[u8]> for BlindedSignature {
type Error = CompactEcashError;
fn try_from(bytes: &[u8]) -> Result<BlindedSignature> {
if bytes.len() != 96 {
return Err(CompactEcashError::Deserialization(format!(
"BlindedSignature must be exactly 96 bytes, got {}",
bytes.len()
)));
}
let bsig1_bytes: &[u8; 48] = &bytes[..48].try_into().expect("Slice size != 48");
let bsig2_bytes: &[u8; 48] = &bytes[48..].try_into().expect("Slice size != 48");
let bsig1 = try_deserialize_g1_projective(
bsig1_bytes,
CompactEcashError::Deserialization(
"Failed to deserialize compressed bsig1".to_string(),
),
)?;
let bsig2 = try_deserialize_g1_projective(
bsig2_bytes,
CompactEcashError::Deserialization(
"Failed to deserialize compressed bsig2".to_string(),
),
)?;
Ok(BlindedSignature(bsig1, bsig2))
}
}
impl BlindedSignature {
pub fn to_bytes(&self) -> [u8; 96] {
let mut bytes = [0u8; 96];
bytes[..48].copy_from_slice(&self.0.to_affine().to_compressed());
bytes[48..].copy_from_slice(&self.1.to_affine().to_compressed());
bytes
}
pub fn from_bytes(bytes: &[u8]) -> Result<BlindedSignature> {
BlindedSignature::try_from(bytes)
}
}
impl Bytable for BlindedSignature {
fn to_byte_vec(&self) -> Vec<u8> {
self.to_bytes().to_vec()
}
fn try_from_byte_slice(slice: &[u8]) -> Result<Self> {
Self::from_bytes(slice)
}
}
impl Base58 for BlindedSignature {}
pub struct SignatureShare {
signature: Signature,
index: SignerIndex,
}
impl SignatureShare {
pub fn new(signature: Signature, index: SignerIndex) -> Self {
SignatureShare { signature, index }
}
pub fn signature(&self) -> &Signature {
&self.signature
}
pub fn index(&self) -> SignerIndex {
self.index
}
// pub fn aggregate(shares: &[Self]) -> Result<Signature> {
// aggregate_signature_shares(shares)
// }
}
#[cfg(test)]
mod tests {
use rand::RngCore;
use super::*;
#[test]
fn polynomial_evaluation() {
// y = 42 (it should be 42 regardless of x)
let poly = Polynomial {
coefficients: vec![Scalar::from(42)],
};
assert_eq!(Scalar::from(42), poly.evaluate(&Scalar::from(1)));
assert_eq!(Scalar::from(42), poly.evaluate(&Scalar::from(0)));
assert_eq!(Scalar::from(42), poly.evaluate(&Scalar::from(10)));
// y = x + 10, at x = 2 (exp: 12)
let poly = Polynomial {
coefficients: vec![Scalar::from(10), Scalar::from(1)],
};
assert_eq!(Scalar::from(12), poly.evaluate(&Scalar::from(2)));
// y = x^4 - 5x^2 + 2x - 3, at x = 3 (exp: 39)
let poly = Polynomial {
coefficients: vec![
(-Scalar::from(3)),
Scalar::from(2),
(-Scalar::from(5)),
Scalar::zero(),
Scalar::from(1),
],
};
assert_eq!(Scalar::from(39), poly.evaluate(&Scalar::from(3)));
// empty polynomial
let poly = Polynomial {
coefficients: vec![],
};
// should always be 0
assert_eq!(Scalar::from(0), poly.evaluate(&Scalar::from(1)));
assert_eq!(Scalar::from(0), poly.evaluate(&Scalar::from(0)));
assert_eq!(Scalar::from(0), poly.evaluate(&Scalar::from(10)));
}
#[test]
fn performing_lagrangian_scalar_interpolation_at_origin() {
// x^2 + 3
// x, f(x):
// 1, 4,
// 2, 7,
// 3, 12,
let points = vec![1, 2, 3];
let values = vec![Scalar::from(4), Scalar::from(7), Scalar::from(12)];
assert_eq!(
Scalar::from(3),
perform_lagrangian_interpolation_at_origin(&points, &values).unwrap()
);
// x^3 + 3x^2 - 5x + 11
// x, f(x):
// 1, 10
// 2, 21
// 3, 50
// 4, 103
let points = vec![1, 2, 3, 4];
let values = vec![
Scalar::from(10),
Scalar::from(21),
Scalar::from(50),
Scalar::from(103),
];
assert_eq!(
Scalar::from(11),
perform_lagrangian_interpolation_at_origin(&points, &values).unwrap()
);
// more points than it is required
// x^2 + x + 10
// x, f(x)
// 1, 12
// 2, 16
// 3, 22
// 4, 30
// 5, 40
let points = vec![1, 2, 3, 4, 5];
let values = vec![
Scalar::from(12),
Scalar::from(16),
Scalar::from(22),
Scalar::from(30),
Scalar::from(40),
];
assert_eq!(
Scalar::from(10),
perform_lagrangian_interpolation_at_origin(&points, &values).unwrap()
);
}
#[test]
fn hash_g1_sanity_check() {
let mut rng = rand::thread_rng();
let mut msg1 = [0u8; 1024];
rng.fill_bytes(&mut msg1);
let mut msg2 = [0u8; 1024];
rng.fill_bytes(&mut msg2);
assert_eq!(hash_g1(msg1), hash_g1(msg1));
assert_eq!(hash_g1(msg2), hash_g1(msg2));
assert_ne!(hash_g1(msg1), hash_g1(msg2));
}
#[test]
fn hash_scalar_sanity_check() {
let mut rng = rand::thread_rng();
let mut msg1 = [0u8; 1024];
rng.fill_bytes(&mut msg1);
let mut msg2 = [0u8; 1024];
rng.fill_bytes(&mut msg2);
assert_eq!(hash_to_scalar(msg1), hash_to_scalar(msg1));
assert_eq!(hash_to_scalar(msg2), hash_to_scalar(msg2));
assert_ne!(hash_to_scalar(msg1), hash_to_scalar(msg2));
}
}
+35 -35
View File
@@ -33,6 +33,40 @@ fn multi_miller_pairing_affine(g11: &G1Affine, g21: &G2Affine, g12: &G1Affine, g
))
}
#[allow(unused)]
fn bench_pairings(c: &mut Criterion) {
let mut rng = rand::thread_rng();
let g1 = G1Affine::generator();
let g2 = G2Affine::generator();
let r = Scalar::random(&mut rng);
let s = Scalar::random(&mut rng);
let g11 = (g1 * r).to_affine();
let g21 = (g2 * s).to_affine();
let g21_prep = G2Prepared::from(g21);
let g12 = (g1 * s).to_affine();
let g22 = (g2 * r).to_affine();
let g22_prep = G2Prepared::from(g22);
c.bench_function("double pairing", |b| {
b.iter(|| double_pairing(&g11, &g21, &g12, &g22))
});
c.bench_function("multi miller in affine", |b| {
b.iter(|| multi_miller_pairing_affine(&g11, &g21, &g12, &g22))
});
c.bench_function("multi miller with prepared g2", |b| {
b.iter(|| multi_miller_pairing_with_prepared(&g11, &g21_prep, &g12, &g22_prep))
});
c.bench_function("multi miller with semi-prepared g2", |b| {
b.iter(|| multi_miller_pairing_with_semi_prepared(&g11, &g21, &g12, &g22_prep))
});
}
#[allow(unused)]
fn multi_miller_pairing_with_prepared(
g11: &G1Affine,
@@ -125,43 +159,9 @@ impl BenchCase {
}
}
#[allow(unused)]
fn bench_pairings(c: &mut Criterion) {
let mut rng = rand::thread_rng();
let g1 = G1Affine::generator();
let g2 = G2Affine::generator();
let r = Scalar::random(&mut rng);
let s = Scalar::random(&mut rng);
let g11 = (g1 * r).to_affine();
let g21 = (g2 * s).to_affine();
let g21_prep = G2Prepared::from(g21);
let g12 = (g1 * s).to_affine();
let g22 = (g2 * r).to_affine();
let g22_prep = G2Prepared::from(g22);
c.bench_function("double pairing", |b| {
b.iter(|| double_pairing(&g11, &g21, &g12, &g22))
});
c.bench_function("multi miller in affine", |b| {
b.iter(|| multi_miller_pairing_affine(&g11, &g21, &g12, &g22))
});
c.bench_function("multi miller with prepared g2", |b| {
b.iter(|| multi_miller_pairing_with_prepared(&g11, &g21_prep, &g12, &g22_prep))
});
c.bench_function("multi miller with semi-prepared g2", |b| {
b.iter(|| multi_miller_pairing_with_semi_prepared(&g11, &g21, &g12, &g22_prep))
});
}
fn bench_coconut(c: &mut Criterion) {
let mut group = c.benchmark_group("benchmark-coconut");
group.measurement_time(Duration::from_secs(100));
group.measurement_time(Duration::from_secs(1000));
let case = BenchCase {
num_authorities: 100,
threshold_p: 0.7,
+1 -1
View File
@@ -40,7 +40,7 @@ mod proofs;
mod scheme;
pub mod tests;
mod traits;
mod utils;
pub mod utils;
pub type Attribute = bls12_381::Scalar;
pub type PrivateAttribute = Attribute;
+5 -11
View File
@@ -122,7 +122,7 @@ const G1_HASH_DOMAIN: &[u8] = b"QUUX-V01-CS02-with-BLS12381G1_XMD:SHA-256_SSWU_R
// https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-11#appendix-K.1
const SCALAR_HASH_DOMAIN: &[u8] = b"QUUX-V01-CS02-with-expander";
pub(crate) fn hash_g1<M: AsRef<[u8]>>(msg: M) -> G1Projective {
pub fn hash_g1<M: AsRef<[u8]>>(msg: M) -> G1Projective {
<G1Projective as HashToCurve<ExpandMsgXmd<sha2::Sha256>>>::hash_to_curve(msg, G1_HASH_DOMAIN)
}
@@ -137,7 +137,7 @@ pub fn hash_to_scalar<M: AsRef<[u8]>>(msg: M) -> Scalar {
output[0]
}
pub(crate) fn try_deserialize_scalar_vec(
pub fn try_deserialize_scalar_vec(
expected_len: u64,
bytes: &[u8],
err: CoconutError,
@@ -161,23 +161,17 @@ pub(crate) fn try_deserialize_scalar_vec(
Ok(out)
}
pub(crate) fn try_deserialize_scalar(bytes: &[u8; 32], err: CoconutError) -> Result<Scalar> {
pub fn try_deserialize_scalar(bytes: &[u8; 32], err: CoconutError) -> Result<Scalar> {
Into::<Option<Scalar>>::into(Scalar::from_bytes(bytes)).ok_or(err)
}
pub(crate) fn try_deserialize_g1_projective(
bytes: &[u8; 48],
err: CoconutError,
) -> Result<G1Projective> {
pub fn try_deserialize_g1_projective(bytes: &[u8; 48], err: CoconutError) -> Result<G1Projective> {
Into::<Option<G1Affine>>::into(G1Affine::from_compressed(bytes))
.ok_or(err)
.map(G1Projective::from)
}
pub(crate) fn try_deserialize_g2_projective(
bytes: &[u8; 96],
err: CoconutError,
) -> Result<G2Projective> {
pub fn try_deserialize_g2_projective(bytes: &[u8; 96], err: CoconutError) -> Result<G2Projective> {
Into::<Option<G2Affine>>::into(G2Affine::from_compressed(bytes))
.ok_or(err)
.map(G2Projective::from)
+194 -3
View File
@@ -223,6 +223,21 @@ version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "520fbf3c07483f94e3e3ca9d0cfd913d7718ef2483d2cfd91c0d9e91474ab913"
[[package]]
name = "const_panic"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6051f239ecec86fde3410901ab7860d458d160371533842974fc61f96d15879b"
[[package]]
name = "convert_case"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
dependencies = [
"unicode-segmentation",
]
[[package]]
name = "cosmwasm-crypto"
version = "1.4.3"
@@ -401,9 +416,9 @@ dependencies = [
[[package]]
name = "cw-multi-test"
version = "0.16.4"
version = "0.16.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a18afd2e201221c6d72a57f0886ef2a22151bbc9e6db7af276fde8a91081042"
checksum = "127c7bb95853b8e828bdab97065c81cb5ddc20f7339180b61b2300565aaa99d1"
dependencies = [
"anyhow",
"cosmwasm-std",
@@ -704,7 +719,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c24f403d068ad0b359e577a77f92392118be3f3c927538f2bb544a5ecd828c6"
dependencies = [
"curve25519-dalek",
"hashbrown",
"hashbrown 0.12.3",
"hex",
"rand_core 0.6.4",
"serde",
@@ -777,6 +792,12 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "equivalent"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "ff"
version = "0.12.1"
@@ -1007,6 +1028,12 @@ dependencies = [
"ahash",
]
[[package]]
name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
[[package]]
name = "hex"
version = "0.4.3"
@@ -1068,6 +1095,16 @@ dependencies = [
"unicode-normalization",
]
[[package]]
name = "indexmap"
version = "2.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
dependencies = [
"equivalent",
"hashbrown 0.14.5",
]
[[package]]
name = "itertools"
version = "0.10.5"
@@ -1133,6 +1170,33 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c33070833c9ee02266356de0c43f723152bd38bd96ddf52c82b3af10c9138b28"
[[package]]
name = "konst"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50a0ba6de5f7af397afff922f22c149ff605c766cd3269cf6c1cd5e466dbe3b9"
dependencies = [
"const_panic",
"konst_kernel",
"konst_proc_macros",
"typewit",
]
[[package]]
name = "konst_kernel"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be0a455a1719220fd6adf756088e1c69a85bf14b6a9e24537a5cc04f503edb2b"
dependencies = [
"typewit",
]
[[package]]
name = "konst_proc_macros"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e28ab1dc35e09d60c2b8c90d12a9a8d9666c876c10a3739a3196db0103b6043"
[[package]]
name = "libc"
version = "0.2.146"
@@ -1307,6 +1371,34 @@ dependencies = [
"zeroize",
]
[[package]]
name = "nym-ecash"
version = "0.1.0"
dependencies = [
"cosmwasm-schema",
"cosmwasm-std",
"cosmwasm-storage",
"cw-controllers",
"cw-storage-plus",
"cw3",
"cw4",
"nym-ecash-contract-common",
"nym-multisig-contract-common",
"schemars",
"serde",
"sylvia",
"thiserror",
]
[[package]]
name = "nym-ecash-contract-common"
version = "0.1.0"
dependencies = [
"cosmwasm-schema",
"cosmwasm-std",
"nym-multisig-contract-common",
]
[[package]]
name = "nym-group-contract-common"
version = "0.1.0"
@@ -1598,6 +1690,16 @@ version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]]
name = "proc-macro-crate"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919"
dependencies = [
"once_cell",
"toml_edit",
]
[[package]]
name = "proc-macro-error"
version = "1.0.4"
@@ -1894,6 +1996,15 @@ dependencies = [
"serde_derive",
]
[[package]]
name = "serde-cw-value"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75d32da6b8ed758b7d850b6c3c08f1d7df51a4df3cb201296e63e34a78e99d4"
dependencies = [
"serde",
]
[[package]]
name = "serde-json-wasm"
version = "0.5.0"
@@ -2065,6 +2176,39 @@ dependencies = [
"zeroize",
]
[[package]]
name = "sylvia"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f33388920659b494dab887f3bb40ebb071c602750597575034bea7c63ab12800"
dependencies = [
"anyhow",
"cosmwasm-schema",
"cosmwasm-std",
"cw-multi-test",
"derivative",
"konst",
"schemars",
"serde",
"serde-cw-value",
"serde-json-wasm",
"sylvia-derive",
]
[[package]]
name = "sylvia-derive"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8031f53dbfda341acd7bd321e10d0d684b673324145026e23705da4b6d5c4919"
dependencies = [
"convert_case",
"proc-macro-crate",
"proc-macro-error",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "syn"
version = "1.0.109"
@@ -2151,12 +2295,44 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "toml_datetime"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1"
[[package]]
name = "toml_edit"
version = "0.19.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
dependencies = [
"indexmap",
"toml_datetime",
"winnow",
]
[[package]]
name = "typenum"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
[[package]]
name = "typewit"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6fb9ae6a3cafaf0a5d14c2302ca525f9ae8e07a0f0e6949de88d882c37a6e24"
dependencies = [
"typewit_proc_macros",
]
[[package]]
name = "typewit_proc_macros"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e36a83ea2b3c704935a01b4642946aadd445cea40b10935e3f8bd8052b8193d6"
[[package]]
name = "unicode-bidi"
version = "0.3.12"
@@ -2178,6 +2354,12 @@ dependencies = [
"tinyvec",
]
[[package]]
name = "unicode-segmentation"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202"
[[package]]
name = "url"
version = "2.3.1"
@@ -2284,6 +2466,15 @@ version = "0.2.84"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d"
[[package]]
name = "winnow"
version = "0.5.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876"
dependencies = [
"memchr",
]
[[package]]
name = "x25519-dalek"
version = "1.1.1"
+2 -2
View File
@@ -3,7 +3,7 @@ resolver = "2"
members = [
"coconut-bandwidth",
"coconut-dkg",
"coconut-test",
"coconut-test", "ecash",
"mixnet",
"mixnet-vesting-integration-tests",
"multisig/cw3-flex-multisig",
@@ -40,7 +40,7 @@ cosmwasm-schema = "=1.4.3"
cosmwasm-std = "=1.4.3"
cosmwasm-storage = "=1.4.3"
cw-controllers = "=1.1.0"
cw-multi-test = "=0.16.4"
cw-multi-test = "=0.16.5"
cw-storage-plus = "=1.2.0"
cw-utils = "=1.0.1"
cw2 = "=1.1.2"
+27
View File
@@ -0,0 +1,27 @@
[package]
name = "nym-ecash"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
sylvia = "0.8.0"
schemars = "0.8.16"
cosmwasm-std = { workspace = true }
cosmwasm-schema = { workspace = true }
serde = "1.0.180"
cw-storage-plus = { workspace = true }
thiserror = { workspace = true }
cw-controllers = { workspace = true }
cosmwasm-storage = { workspace = true }
cw3 = { workspace = true }
cw4 = { workspace = true }
nym-ecash-contract-common = { path = "../../common/cosmwasm-smart-contracts/ecash-contract" }
nym-multisig-contract-common = { path = "../../common/cosmwasm-smart-contracts/multisig-contract" }
[dev-dependencies]
sylvia = { version = "0.8.0", features = ["mt"] }
+430
View File
@@ -0,0 +1,430 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_std::{
to_binary, BankMsg, Coin, CosmosMsg, Event, Order, Reply, Response, StdError, StdResult,
SubMsg, WasmMsg,
};
use cw3::ProposalResponse;
use cw4::Cw4Contract;
use cw_controllers::Admin;
use cw_storage_plus::{Bound, IndexedMap, Item};
use nym_ecash_contract_common::blacklist::{
BlacklistProposal, BlacklistedAccount, BlacklistedAccountResponse,
PagedBlacklistedAccountResponse,
};
use nym_ecash_contract_common::events::{
BLACKLIST_PROPOSAL_ID, BLACKLIST_PROPOSAL_REPLY_ID, DEPOSITED_FUNDS_EVENT_TYPE,
DEPOSIT_ENCRYPTION_KEY, DEPOSIT_IDENTITY_KEY, DEPOSIT_INFO, DEPOSIT_VALUE,
};
use nym_ecash_contract_common::msg::ExecuteMsg;
use nym_multisig_contract_common::msg::ExecuteMsg as MultisigExecuteMsg;
use nym_multisig_contract_common::msg::QueryMsg as MultisigQueryMsg;
use sylvia::types::{ExecCtx, InstantiateCtx, QueryCtx, ReplyCtx};
use sylvia::{contract, entry_points};
use crate::errors::ContractError;
use crate::state::Config;
use crate::storage::{
self, BlacklistIndex, BlacklistProposalIndex, SpendCredentialIndex,
BLACKLIST_PAGE_DEFAULT_LIMIT, BLACKLIST_PAGE_MAX_LIMIT, SPEND_CREDENTIAL_PAGE_DEFAULT_LIMIT,
SPEND_CREDENTIAL_PAGE_MAX_LIMIT,
};
use nym_ecash_contract_common::events::{TICKET_BOOK_VALUE, TICKET_VALUE};
use nym_ecash_contract_common::spend_credential::{
EcashSpentCredential, EcashSpentCredentialResponse, PagedEcashSpentCredentialResponse,
};
pub struct NymEcashContract<'a> {
pub(crate) admin: Admin<'a>,
pub(crate) config: Item<'a, Config>,
pub(crate) spent_credentials:
IndexedMap<'a, &'a str, EcashSpentCredential, SpendCredentialIndex<'a>>,
pub(crate) blacklist: IndexedMap<'a, &'a str, BlacklistedAccount, BlacklistIndex<'a>>,
pub(crate) blacklist_proposals:
IndexedMap<'a, &'a str, BlacklistProposal, BlacklistProposalIndex<'a>>,
}
#[entry_points]
#[contract]
#[error(ContractError)]
impl NymEcashContract<'_> {
pub const fn new() -> Self {
Self {
admin: Admin::new("admin"),
config: Item::new("config"),
spent_credentials: storage::spent_credentials(),
blacklist: storage::blacklist(),
blacklist_proposals: storage::blacklist_proposal(),
}
}
#[msg(instantiate)]
pub fn instantiate(
&self,
mut ctx: InstantiateCtx,
multisig_addr: String,
group_addr: String,
mix_denom: String,
) -> Result<Response, ContractError> {
let multisig_addr = ctx.deps.api.addr_validate(&multisig_addr)?;
let group_addr = Cw4Contract(ctx.deps.api.addr_validate(&group_addr).map_err(|_| {
ContractError::InvalidGroup {
addr: group_addr.clone(),
}
})?);
self.admin
.set(ctx.deps.branch(), Some(multisig_addr.clone()))?;
let cfg = Config {
multisig_addr,
group_addr,
mix_denom,
};
self.config.save(ctx.deps.storage, &cfg)?;
Ok(Response::default())
}
/*==================
======QUERIES=======
==================*/
#[msg(query)]
pub fn get_all_spent_credentials(
&self,
ctx: QueryCtx,
limit: Option<u32>,
start_after: Option<String>,
) -> StdResult<PagedEcashSpentCredentialResponse> {
let limit = limit
.unwrap_or(SPEND_CREDENTIAL_PAGE_DEFAULT_LIMIT)
.min(SPEND_CREDENTIAL_PAGE_MAX_LIMIT) as usize;
let start = start_after.as_deref().map(Bound::exclusive);
let nodes = self
.spent_credentials
.range(ctx.deps.storage, start, None, Order::Ascending)
.take(limit)
.map(|res| res.map(|item| item.1))
.collect::<StdResult<Vec<EcashSpentCredential>>>()?;
let start_next_after = nodes
.last()
.map(|spend_credential| spend_credential.serial_number().to_string());
Ok(PagedEcashSpentCredentialResponse::new(
nodes,
limit,
start_next_after,
))
}
#[msg(query)]
pub fn get_spent_credential(
&self,
ctx: QueryCtx,
serial_number: String,
) -> StdResult<EcashSpentCredentialResponse> {
let spend_credential = self
.spent_credentials
.may_load(ctx.deps.storage, &serial_number)?;
Ok(EcashSpentCredentialResponse::new(spend_credential))
}
#[msg(query)]
pub fn get_blacklist(
&self,
ctx: QueryCtx,
limit: Option<u32>,
start_after: Option<String>,
) -> StdResult<PagedBlacklistedAccountResponse> {
let limit = limit
.unwrap_or(BLACKLIST_PAGE_DEFAULT_LIMIT)
.min(BLACKLIST_PAGE_MAX_LIMIT) as usize;
let start = start_after.as_deref().map(Bound::exclusive);
let nodes = self
.blacklist
.range(ctx.deps.storage, start, None, Order::Ascending)
.take(limit)
.map(|res| res.map(|item| item.1))
.collect::<StdResult<Vec<BlacklistedAccount>>>()?;
let start_next_after = nodes
.last()
.map(|account: &BlacklistedAccount| account.public_key().to_string());
Ok(PagedBlacklistedAccountResponse::new(
nodes,
limit,
start_next_after,
))
}
#[msg(query)]
pub fn get_blacklisted_account(
&self,
ctx: QueryCtx,
public_key: String,
) -> StdResult<BlacklistedAccountResponse> {
let account = self.blacklist.may_load(ctx.deps.storage, &public_key)?;
Ok(BlacklistedAccountResponse::new(account))
}
/*=====================
======EXECUTIONS=======
=====================*/
#[msg(exec)]
pub fn deposit_funds(
&self,
ctx: ExecCtx,
deposit_info: String,
identity_key: String,
encryption_key: String,
) -> Result<Response, ContractError> {
if ctx.info.funds.is_empty() {
return Err(ContractError::NoCoin);
}
if ctx.info.funds.len() > 1 {
return Err(ContractError::MultipleDenoms);
}
let mix_denom = self.config.load(ctx.deps.storage)?.mix_denom;
if ctx.info.funds[0].denom != mix_denom {
return Err(ContractError::WrongDenom { mix_denom });
}
let voucher_value = ctx.info.funds.last().unwrap();
if u128::from(voucher_value.amount) != TICKET_BOOK_VALUE {
return Err(ContractError::WrongAmount {
amount: TICKET_BOOK_VALUE,
});
}
let event = Event::new(DEPOSITED_FUNDS_EVENT_TYPE)
.add_attribute(DEPOSIT_VALUE, voucher_value.amount)
.add_attribute(DEPOSIT_INFO, deposit_info)
.add_attribute(DEPOSIT_IDENTITY_KEY, identity_key)
.add_attribute(DEPOSIT_ENCRYPTION_KEY, encryption_key);
Ok(Response::new().add_event(event))
}
#[msg(exec)]
pub fn prepare_credential(
&self,
ctx: ExecCtx,
serial_number: String,
gateway_cosmos_address: String,
) -> StdResult<Response> {
let cfg = self.config.load(ctx.deps.storage)?;
let gateway_cosmos_address = ctx.deps.api.addr_validate(&gateway_cosmos_address)?;
let msg = Self::create_spend_proposal(
serial_number.to_string(),
gateway_cosmos_address.to_string(),
ctx.env.contract.address.into_string(),
cfg.multisig_addr.into_string(),
)?;
Ok(Response::new().add_message(msg))
}
#[msg(exec)]
pub fn spend_credential(
&self,
ctx: ExecCtx,
serial_number: String,
gateway_cosmos_address: String,
) -> Result<Response, ContractError> {
let mix_denom = self.config.load(ctx.deps.storage)?.mix_denom;
let ticket_fund = Coin::new(TICKET_VALUE, mix_denom.clone());
let current_balance = ctx
.deps
.querier
.query_balance(ctx.env.contract.address, mix_denom)?;
if ticket_fund.amount > current_balance.amount {
return Err(ContractError::NotEnoughFunds);
}
//only a mutlisig proposal can do that
self.admin
.assert_admin(ctx.deps.as_ref(), &ctx.info.sender)?;
let return_tokens = BankMsg::Send {
to_address: gateway_cosmos_address.clone(),
amount: vec![ticket_fund],
};
self.spent_credentials.save(
ctx.deps.storage,
&serial_number,
&EcashSpentCredential::new(serial_number.to_owned(), gateway_cosmos_address),
)?;
let response = Response::new().add_message(return_tokens);
Ok(response)
}
#[msg(exec)]
pub fn propose_to_blacklist(
&self,
ctx: ExecCtx,
public_key: String,
) -> Result<Response, ContractError> {
let cfg = self.config.load(ctx.deps.storage)?;
cfg.group_addr
.is_voting_member(&ctx.deps.querier, &ctx.info.sender, ctx.env.block.height)?
.ok_or(ContractError::Unauthorized)?;
if let Some(blacklist_proposal) = self
.blacklist_proposals
.may_load(ctx.deps.storage, &public_key)?
{
Ok(Response::new().add_attribute(
BLACKLIST_PROPOSAL_ID,
blacklist_proposal.proposal_id().to_string(),
))
} else {
let msg = Self::create_blacklist_proposal(
public_key.to_string(),
ctx.env.contract.address.into_string(),
cfg.multisig_addr.into_string(),
)?;
Ok(Response::new().add_submessage(msg))
}
}
#[msg(exec)]
pub fn add_to_blacklist(
&self,
ctx: ExecCtx,
public_key: String,
) -> Result<Response, ContractError> {
//Only by multisig contract, actually add public key to blacklist
self.admin
.assert_admin(ctx.deps.as_ref(), &ctx.info.sender)?;
self.blacklist.save(
ctx.deps.storage,
&public_key.clone(),
&BlacklistedAccount::new(public_key),
)?;
Ok(Response::new())
}
#[msg(reply)]
pub fn reply(&self, ctx: ReplyCtx, msg: Reply) -> Result<Response, ContractError> {
match msg.id {
BLACKLIST_PROPOSAL_REPLY_ID => self.handle_blacklist_proposal_reply(ctx, msg),
id => Err(ContractError::Std(cosmwasm_std::StdError::GenericErr {
msg: format!("Unknown reply Id {}", id),
})),
}
}
fn handle_blacklist_proposal_reply(
&self,
ctx: ReplyCtx,
msg: Reply,
) -> Result<Response, ContractError> {
let reply = msg.result.into_result().map_err(StdError::generic_err)?;
let proposal_attribute = reply
.events
.iter()
.find(|event| event.ty == "wasm")
.ok_or(ContractError::ProposalError(
"Wasm event not found".to_string(),
))?
.attributes
.iter()
.find(|attr| attr.key == BLACKLIST_PROPOSAL_ID)
.ok_or(ContractError::ProposalError(
"Proposal id not found".to_string(),
))?;
let proposal_id = proposal_attribute.value.parse::<u64>().map_err(|_| {
ContractError::ProposalError(String::from("proposal id could not be parsed to u64"))
})?;
let cfg = self.config.load(ctx.deps.storage)?;
let msg = MultisigQueryMsg::Proposal { proposal_id };
let proposal_response: ProposalResponse = ctx.deps.querier.query(
&cosmwasm_std::QueryRequest::Wasm(cosmwasm_std::WasmQuery::Smart {
contract_addr: cfg.multisig_addr.to_string(),
msg: to_binary(&msg)?,
}),
)?;
let public_key = proposal_response.description;
self.blacklist_proposals.save(
ctx.deps.storage,
&public_key.clone(),
&BlacklistProposal::new(public_key, proposal_id),
)?;
Ok(Response::new().add_attribute(BLACKLIST_PROPOSAL_ID, proposal_id.to_string()))
}
fn create_spend_proposal(
serial_number: String,
gateway_cosmos_address: String,
ecash_bandwidth_address: String,
multisig_addr: String,
) -> StdResult<CosmosMsg> {
let release_funds_req = ExecuteMsg::SpendCredential {
serial_number: serial_number.clone(),
gateway_cosmos_address,
};
let release_funds_msg = CosmosMsg::Wasm(WasmMsg::Execute {
contract_addr: ecash_bandwidth_address,
msg: to_binary(&release_funds_req)?,
funds: vec![],
});
let req = MultisigExecuteMsg::Propose {
title: String::from("Spend credential, as ordered by Ecash Bandwidth Contract"),
description: serial_number,
msgs: vec![release_funds_msg],
latest: None,
};
let msg = CosmosMsg::Wasm(WasmMsg::Execute {
contract_addr: multisig_addr,
msg: to_binary(&req)?,
funds: vec![],
});
Ok(msg)
}
fn create_blacklist_proposal(
public_key: String,
ecash_bandwidth_address: String,
multisig_addr: String,
) -> StdResult<SubMsg> {
let blacklist_req = ExecuteMsg::AddToBlacklist {
public_key: public_key.clone(),
};
let blacklist_req_msg = CosmosMsg::Wasm(WasmMsg::Execute {
contract_addr: ecash_bandwidth_address,
msg: to_binary(&blacklist_req)?,
funds: vec![],
});
let req = MultisigExecuteMsg::Propose {
title: String::from("Add to blacklist, as ordered by Ecash Bandwidth Contract"),
description: public_key,
msgs: vec![blacklist_req_msg],
latest: None,
};
let msg = CosmosMsg::Wasm(WasmMsg::Execute {
contract_addr: multisig_addr,
msg: to_binary(&req)?,
funds: vec![],
});
let submsg = SubMsg::reply_always(msg, BLACKLIST_PROPOSAL_REPLY_ID);
Ok(submsg)
}
}
+42
View File
@@ -0,0 +1,42 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_std::StdError;
use cw_controllers::AdminError;
use thiserror::Error;
#[derive(Error, Debug, PartialEq)]
pub enum ContractError {
#[error(transparent)]
Std(#[from] StdError),
#[error("Received multiple coin types")]
MultipleDenoms,
#[error("No coin was sent for voucher")]
NoCoin,
#[error("Wrong coin denomination, you must send {mix_denom}")]
WrongDenom { mix_denom: String },
#[error("Wrong amount for deposit, you must send {amount}")]
WrongAmount { amount: u128 },
#[error("There aren't enough funds in the contract")]
NotEnoughFunds,
#[error("Credential already spent or in process of spending")]
DuplicateBlindedSerialNumber,
#[error(transparent)]
Admin(#[from] AdminError),
#[error("Proposal error - {0}")]
ProposalError(String),
#[error("Group contract invalid address '{addr}'")]
InvalidGroup { addr: String },
#[error("Unauthorized")]
Unauthorized,
}
+9
View File
@@ -0,0 +1,9 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod contract;
pub mod errors;
#[cfg(test)]
pub mod multitest;
mod state;
mod storage;
+86
View File
@@ -0,0 +1,86 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_std::{Addr, Coin};
use sylvia::{cw_multi_test::App as MtApp, multitest::App};
use crate::{contract::multitest_utils::CodeId, errors::ContractError};
#[test]
fn invalid_deposit() {
let owner = "owner";
let denom = "unym";
let mtapp = MtApp::new(|router, _, storage| {
router
.bank
.init_balance(
storage,
&Addr::unchecked(owner),
vec![
Coin::new(10000000, denom),
Coin::new(10000000, "some_denom"),
],
)
.unwrap()
});
let app = App::new(mtapp);
let code_id = CodeId::store_code(&app);
let contract = code_id
.instantiate(
"multisig_addr".to_string(),
"group_addr".to_string(),
denom.to_string(),
)
.call(owner)
.unwrap();
let deposit_info = "Deposit info";
let verification_key = "Verification key";
let encryption_key = "Encryption key";
assert_eq!(
contract
.deposit_funds(
deposit_info.to_string(),
verification_key.to_string(),
encryption_key.to_string()
)
.call(owner)
.unwrap_err(),
ContractError::NoCoin
);
let coin = Coin::new(1000000, denom.to_string());
let second_coin = Coin::new(1000000, "some_denom");
assert_eq!(
contract
.deposit_funds(
deposit_info.to_string(),
verification_key.to_string(),
encryption_key.to_string()
)
.with_funds(&[coin, second_coin.clone()])
.call(owner)
.unwrap_err(),
ContractError::MultipleDenoms
);
assert_eq!(
contract
.deposit_funds(
deposit_info.to_string(),
verification_key.to_string(),
encryption_key.to_string()
)
.with_funds(&[second_coin])
.call(owner)
.unwrap_err(),
ContractError::WrongDenom {
mix_denom: denom.to_string()
}
);
}
+13
View File
@@ -0,0 +1,13 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_std::Addr;
use cw4::Cw4Contract;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Config {
pub multisig_addr: Addr,
pub group_addr: Cw4Contract,
pub mix_denom: String,
}
+100
View File
@@ -0,0 +1,100 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cw_storage_plus::{Index, IndexList, IndexedMap, UniqueIndex};
use nym_ecash_contract_common::{
blacklist::{BlacklistProposal, BlacklistedAccount},
spend_credential::EcashSpentCredential,
};
// storage prefixes
const SPEND_CREDENTIAL_PK_NAMESPACE: &str = "ecsc";
const SPEND_CREDENTIAL_BLINDED_SERIAL_NO_IDX_NAMESPACE: &str = "ecscn";
// paged retrieval limits for all queries and transactions
pub(crate) const SPEND_CREDENTIAL_PAGE_MAX_LIMIT: u32 = 75;
pub(crate) const SPEND_CREDENTIAL_PAGE_DEFAULT_LIMIT: u32 = 50;
pub(crate) struct SpendCredentialIndex<'a> {
pub(crate) blinded_serial_number: UniqueIndex<'a, String, EcashSpentCredential>,
}
// IndexList is just boilerplate code for fetching a struct's indexes
// note that from my understanding this will be converted into a macro at some point in the future
impl<'a> IndexList<EcashSpentCredential> for SpendCredentialIndex<'a> {
fn get_indexes(&'_ self) -> Box<dyn Iterator<Item = &'_ dyn Index<EcashSpentCredential>> + '_> {
let v: Vec<&dyn Index<EcashSpentCredential>> = vec![&self.blinded_serial_number];
Box::new(v.into_iter())
}
}
// spent_credentials() is the storage access function.
pub(crate) const fn spent_credentials<'a>(
) -> IndexedMap<'a, &'a str, EcashSpentCredential, SpendCredentialIndex<'a>> {
let indexes = SpendCredentialIndex {
blinded_serial_number: UniqueIndex::new(
|d| d.serial_number().to_string(),
SPEND_CREDENTIAL_BLINDED_SERIAL_NO_IDX_NAMESPACE,
),
};
IndexedMap::new(SPEND_CREDENTIAL_PK_NAMESPACE, indexes)
}
// storage prefixes
const BLACKLIST_PK_NAMESPACE: &str = "blacklist";
const BLACKLIST_NO_IDX_NAMESPACE: &str = "blacklistnoidx";
// paged retrieval limits for all queries and transactions
pub(crate) const BLACKLIST_PAGE_MAX_LIMIT: u32 = 75;
pub(crate) const BLACKLIST_PAGE_DEFAULT_LIMIT: u32 = 50;
pub(crate) struct BlacklistIndex<'a> {
pub(crate) blacklist: UniqueIndex<'a, String, BlacklistedAccount>,
}
// IndexList is just boilerplate code for fetching a struct's indexes
// note that from my understanding this will be converted into a macro at some point in the future
impl<'a> IndexList<BlacklistedAccount> for BlacklistIndex<'a> {
fn get_indexes(&'_ self) -> Box<dyn Iterator<Item = &'_ dyn Index<BlacklistedAccount>> + '_> {
let v: Vec<&dyn Index<BlacklistedAccount>> = vec![&self.blacklist];
Box::new(v.into_iter())
}
}
// spent_credentials() is the storage access function.
pub(crate) const fn blacklist<'a>(
) -> IndexedMap<'a, &'a str, BlacklistedAccount, BlacklistIndex<'a>> {
let indexes = BlacklistIndex {
blacklist: UniqueIndex::new(|d| d.public_key().to_string(), BLACKLIST_NO_IDX_NAMESPACE),
};
IndexedMap::new(BLACKLIST_PK_NAMESPACE, indexes)
}
// storage prefixes
const BLACKLIST_PROPOSAL_PK_NAMESPACE: &str = "blacklistproposal";
const BLACKLIST_PROPOSAL_NO_IDX_NAMESPACE: &str = "blacklistproposalnoidx";
pub(crate) struct BlacklistProposalIndex<'a> {
pub(crate) blacklist_proposal: UniqueIndex<'a, String, BlacklistProposal>,
}
// IndexList is just boilerplate code for fetching a struct's indexes
// note that from my understanding this will be converted into a macro at some point in the future
impl<'a> IndexList<BlacklistProposal> for BlacklistProposalIndex<'a> {
fn get_indexes(&'_ self) -> Box<dyn Iterator<Item = &'_ dyn Index<BlacklistProposal>> + '_> {
let v: Vec<&dyn Index<BlacklistProposal>> = vec![&self.blacklist_proposal];
Box::new(v.into_iter())
}
}
// spent_credentials() is the storage access function.
pub(crate) const fn blacklist_proposal<'a>(
) -> IndexedMap<'a, &'a str, BlacklistProposal, BlacklistProposalIndex<'a>> {
let indexes = BlacklistProposalIndex {
blacklist_proposal: UniqueIndex::new(
|d| d.public_key().to_string(),
BLACKLIST_PROPOSAL_NO_IDX_NAMESPACE,
),
};
IndexedMap::new(BLACKLIST_PROPOSAL_PK_NAMESPACE, indexes)
}
+2
View File
@@ -21,6 +21,7 @@ async-trait = { workspace = true }
bip39 = { workspace = true }
bs58 = { workspace = true }
clap = { workspace = true, features = ["cargo", "derive"] }
chrono = "0.4.19"
colored = "2.0"
dashmap = { workspace = true }
dirs = "4.0"
@@ -55,6 +56,7 @@ tokio-util = { workspace = true, features = ["codec"] }
url = { workspace = true, features = ["serde"] }
time = { workspace = true }
zeroize = { workspace = true }
bloomfilter = "1.0.13"
# internal
+3
View File
@@ -40,4 +40,7 @@ features = ["tokio"]
workspace = true
default-features = false
[dev-dependencies]
nym-compact-ecash = { path = "../../common/nym_offline_compact_ecash" } # we need specific imports in tests
+114 -38
View File
@@ -3,9 +3,53 @@
use crate::GatewayRequestsError;
use nym_credentials::coconut::bandwidth::CredentialSpendingData;
use nym_credentials_interface::{CoconutError, VerifyCredentialRequest};
use nym_credentials_interface::CompactEcashError;
use nym_credentials_interface::{
Base58, Bytable, CoconutError, OldCredentialSpendingData, VerifyCredentialRequest,
};
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
pub struct CredentialSpendingRequest {
/// The cryptographic material required for spending the underlying credential.
pub data: CredentialSpendingData,
}
impl CredentialSpendingRequest {
pub fn new(data: CredentialSpendingData) -> Self {
CredentialSpendingRequest { data }
}
pub fn matches_serial_number(
&self,
serial_number_bs58: &str,
) -> Result<bool, CompactEcashError> {
self.data.payment.has_serial_number(serial_number_bs58)
}
pub fn to_bytes(&self) -> Vec<u8> {
self.data.to_bytes()
}
pub fn try_from_bytes(raw: &[u8]) -> Result<Self, CompactEcashError> {
Ok(CredentialSpendingRequest {
data: CredentialSpendingData::try_from_bytes(raw)?,
})
}
}
impl Bytable for CredentialSpendingRequest {
fn to_byte_vec(&self) -> Vec<u8> {
self.to_bytes()
}
fn try_from_byte_slice(slice: &[u8]) -> Result<Self, CompactEcashError> {
Self::try_from_bytes(slice)
}
}
impl Base58 for CredentialSpendingRequest {}
// reimplements old coconut-interface::Credential for backwards compatibility sake
// (so that 'new' gateways could still understand those requests)
#[derive(Debug, PartialEq, Eq)]
@@ -22,7 +66,7 @@ pub struct OldV1Credential {
}
// attempt to convert the old request type into the new variant
impl TryFrom<OldV1Credential> for CredentialSpendingRequest {
impl TryFrom<OldV1Credential> for OldCredentialSpendingRequest {
type Error = GatewayRequestsError;
fn try_from(value: OldV1Credential) -> Result<Self, Self::Error> {
@@ -35,8 +79,8 @@ impl TryFrom<OldV1Credential> for CredentialSpendingRequest {
let typ = value.voucher_info.parse()?;
let public_attributes_plain = vec![value.voucher_value.to_string(), value.voucher_info];
Ok(CredentialSpendingRequest {
data: CredentialSpendingData {
Ok(OldCredentialSpendingRequest {
data: OldCredentialSpendingData {
embedded_private_attributes,
verify_credential_request: value.theta,
public_attributes_plain,
@@ -105,12 +149,6 @@ impl OldV1Credential {
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct CredentialSpendingRequest {
/// The cryptographic material required for spending the underlying credential.
pub data: CredentialSpendingData,
}
// just a helper macro for checking required length and advancing the buffer
macro_rules! ensure_len_and_advance {
($b:expr, $n:expr) => {{
@@ -128,9 +166,15 @@ macro_rules! ensure_len_and_advance {
}};
}
impl CredentialSpendingRequest {
pub fn new(data: CredentialSpendingData) -> Self {
CredentialSpendingRequest { data }
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct OldCredentialSpendingRequest {
/// The cryptographic material required for spending the underlying credential.
pub data: OldCredentialSpendingData,
}
impl OldCredentialSpendingRequest {
pub fn new(data: OldCredentialSpendingData) -> Self {
OldCredentialSpendingRequest { data }
}
pub fn matches_blinded_serial_number(
@@ -223,8 +267,8 @@ impl CredentialSpendingRequest {
let epoch_id_bytes = ensure_len_and_advance!(b, 8);
let epoch_id = u64::from_be_bytes(epoch_id_bytes.try_into().unwrap());
Ok(CredentialSpendingRequest {
data: CredentialSpendingData {
Ok(OldCredentialSpendingRequest {
data: nym_credentials_interface::OldCredentialSpendingData {
embedded_private_attributes,
verify_credential_request: theta,
public_attributes_plain,
@@ -238,11 +282,17 @@ impl CredentialSpendingRequest {
#[cfg(test)]
mod tests {
use super::*;
use nym_credentials::coconut::bandwidth::bandwidth_credential_params;
use nym_compact_ecash::{
identify::{generate_coin_indices_signatures, generate_expiration_date_signatures},
issue, ttp_keygen, PayInfo,
};
use nym_credentials::coconut::{
bandwidth::bandwidth_credential_params, utils::freepass_exp_date_timestamp,
};
use nym_credentials::IssuanceBandwidthCredential;
use nym_credentials_interface::{
blind_sign, hash_to_scalar, prove_bandwidth_credential, Attribute, Base58, Parameters,
Signature, VerificationKey,
prove_bandwidth_credential, Attribute, CoconutBase58, CoconutParameters, CoconutSignature,
VerificationKey,
};
#[test]
@@ -253,13 +303,13 @@ mod tests {
Attribute::try_from_bs58("7Rp3imcuNX3w9se9wm5th8gSvc2czsnMrGsdt5HsrycA").unwrap();
let binding_number =
Attribute::try_from_bs58("Auf8yVEgyEAWNHaXUZmimS4n9g5YiYnNYqp6F9BtBe9E").unwrap();
let signature = Signature::try_from_bs58(
let signature = CoconutSignature::try_from_bs58(
"ta3pM9ffj5T6YGbwjSBp2W118rcwyP9PXStc\
7ssb91g5GQYMQHhuTNajbdZcjxUFBFL5rhED8EHpRzE8r432ss3qbPBfpNev4CdkfMkQ3wepyM7hy7q1W6Rn9WmFoZL\
ZR9j",
)
.unwrap();
let params = Parameters::new(4).unwrap();
let params = CoconutParameters::new(4).unwrap();
let verification_key = VerificationKey::try_from_bs58("8CFtVVXdwLy4WHMQPE4\
woe89q3DRHoNxBSchftrEjSBPWA4r4xZv4Y9qSvS5x5bMmFtp7BX6ikECAnuXr5EjXWSsgjirZJmpS5XDUynVfht1cD\
FWGDvy2XFrRCuoCMotNXi3PoF6wYqdTR9Rqcfoj3i2H5Nid422WBaLtVoC9QNobvpvaqq6vX5PbsSyPayvU8HCXFxM6\
@@ -299,33 +349,59 @@ mod tests {
fn credential_roundtrip() {
// make valid request
let params = bandwidth_credential_params();
let keypair = nym_credentials_interface::keygen(params);
let keypair = ttp_keygen(params.grp(), 1, 1).unwrap().remove(0);
let issuance = IssuanceBandwidthCredential::new_freepass(None);
let issuance = IssuanceBandwidthCredential::new_freepass(freepass_exp_date_timestamp());
let sig_req = issuance.prepare_for_signing();
let pub_attrs_hashed = sig_req
.public_attributes_plain
.iter()
.map(hash_to_scalar)
.collect::<Vec<_>>();
let pub_attrs = pub_attrs_hashed.iter().collect::<Vec<_>>();
let blind_sig = blind_sign(
let exp_date_sigs = generate_expiration_date_signatures(
params,
keypair.secret_key(),
&sig_req.blind_sign_request,
&pub_attrs,
sig_req.expiration_date,
&[keypair.secret_key()],
&vec![keypair.verification_key()],
&keypair.verification_key(),
&[keypair.index.unwrap()],
)
.unwrap();
let sig = blind_sig
.unblind(
keypair.verification_key(),
&sig_req.pedersen_commitments_openings,
let blind_sig = issue(
params.grp(),
keypair.secret_key(),
sig_req.ecash_pub_key.clone(),
&sig_req.withdrawal_request,
freepass_exp_date_timestamp(),
)
.unwrap();
let partial_wallet = issuance
.unblind_signature(
&keypair.verification_key(),
&sig_req,
blind_sig,
keypair.index.unwrap(),
)
.unwrap();
let issued = issuance.into_issued_credential(sig, 42);
let wallet = issuance
.aggregate_signature_shares(&keypair.verification_key(), &vec![partial_wallet], sig_req)
.unwrap();
let issued = issuance.into_issued_credential(wallet, exp_date_sigs, 1);
let coin_indices_signatures = generate_coin_indices_signatures(
params,
&[keypair.secret_key()],
&vec![keypair.verification_key()],
&keypair.verification_key(),
&[keypair.index.unwrap()],
)
.unwrap();
let pay_info = PayInfo {
pay_info_bytes: [6u8; 72],
};
let spending = issued
.prepare_for_spending(keypair.verification_key())
.prepare_for_spending(
&keypair.verification_key(),
pay_info,
coin_indices_signatures,
)
.unwrap();
let with_epoch = CredentialSpendingRequest { data: spending };
+40 -5
View File
@@ -3,12 +3,14 @@
use crate::authentication::encrypted_address::EncryptedAddressBytes;
use crate::iv::IV;
use crate::models::{CredentialSpendingRequest, OldV1Credential};
use crate::models::{CredentialSpendingRequest, OldCredentialSpendingRequest, OldV1Credential};
use crate::registration::handshake::SharedKeys;
use crate::{GatewayMacSize, CURRENT_PROTOCOL_VERSION, INITIAL_PROTOCOL_VERSION};
use log::error;
use nym_credentials::coconut::bandwidth::CredentialSpendingData;
use nym_credentials_interface::{CoconutError, UnknownCredentialType};
use nym_credentials_interface::{
CoconutError, CompactEcashError, OldCredentialSpendingData, UnknownCredentialType,
};
use nym_crypto::generic_array::typenum::Unsigned;
use nym_crypto::hmac::recompute_keyed_hmac_and_verify_tag;
use nym_crypto::symmetric::stream_cipher;
@@ -122,6 +124,9 @@ pub enum GatewayRequestsError {
source: MixPacketFormattingError,
},
#[error("failed to deserialize provided credential: {0}")]
EcashCredentialDeserializationFailure(#[from] CompactEcashError),
#[error("failed to deserialize provided credential: EOF")]
CredentialDeserializationFailureEOF,
@@ -164,6 +169,10 @@ pub enum ClientControlRequest {
enc_credential: Vec<u8>,
iv: Vec<u8>,
},
EcashCredential {
enc_credential: Vec<u8>,
iv: Vec<u8>,
},
ClaimFreeTestnetBandwidth,
}
@@ -200,6 +209,7 @@ impl ClientControlRequest {
ClientControlRequest::BandwidthCredentialV2 { .. } => {
"BandwidthCredentialV2".to_string()
}
ClientControlRequest::EcashCredential { .. } => "EcashCredential".to_string(),
ClientControlRequest::ClaimFreeTestnetBandwidth => {
"ClaimFreeTestnetBandwidth".to_string()
}
@@ -231,11 +241,11 @@ impl ClientControlRequest {
}
pub fn new_enc_coconut_bandwidth_credential_v2(
credential: CredentialSpendingData,
credential: OldCredentialSpendingData,
shared_key: &SharedKeys,
iv: IV,
) -> Self {
let cred = CredentialSpendingRequest::new(credential);
let cred = OldCredentialSpendingRequest::new(credential);
let serialized_credential = cred.to_bytes();
let enc_credential = shared_key.encrypt_and_tag(&serialized_credential, Some(iv.inner()));
@@ -249,9 +259,34 @@ impl ClientControlRequest {
enc_credential: Vec<u8>,
shared_key: &SharedKeys,
iv: IV,
) -> Result<OldCredentialSpendingRequest, GatewayRequestsError> {
let credential_bytes = shared_key.decrypt_tagged(&enc_credential, Some(iv.inner()))?;
OldCredentialSpendingRequest::try_from_bytes(&credential_bytes)
.map_err(|_| GatewayRequestsError::MalformedEncryption)
}
pub fn new_enc_ecash_credential(
credential: CredentialSpendingData,
shared_key: &SharedKeys,
iv: IV,
) -> Self {
let cred = CredentialSpendingRequest::new(credential);
let serialized_credential = cred.to_bytes();
let enc_credential = shared_key.encrypt_and_tag(&serialized_credential, Some(iv.inner()));
ClientControlRequest::EcashCredential {
enc_credential,
iv: iv.to_bytes(),
}
}
pub fn try_from_enc_ecash_credential(
enc_credential: Vec<u8>,
shared_key: &SharedKeys,
iv: IV,
) -> Result<CredentialSpendingRequest, GatewayRequestsError> {
let credential_bytes = shared_key.decrypt_tagged(&enc_credential, Some(iv.inner()))?;
CredentialSpendingRequest::try_from_bytes(&credential_bytes)
CredentialSpendingRequest::try_from_bytes(credential_bytes.as_slice())
.map_err(|_| GatewayRequestsError::MalformedEncryption)
}
}
@@ -0,0 +1,20 @@
/*
* Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
* SPDX-License-Identifier: Apache-2.0
*/
CREATE TABLE credentials
(
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
credentials TEXT NOT NULL
);
CREATE TABLE pending
(
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
credential TEXT NOT NULL,
address TEXT NOT NULL,
api_urls TEXT NOT NULL,
proposal_id TEXT NOT NULL
);
@@ -0,0 +1,8 @@
/*
* Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
* SPDX-License-Identifier: Apache-2.0
*/
ALTER TABLE available_bandwidth
RENAME COLUMN freepass_expiration TO expiration;

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