Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1a8d586eca | |||
| 21d84b40bb | |||
| d72f7bf882 | |||
| d4d55d11f0 | |||
| 1feeba26b5 | |||
| c76c8e5b4c | |||
| 0d69bfbef7 | |||
| 0d133187e0 | |||
| 382056bce2 | |||
| 9441057e31 | |||
| 3a3cba06b1 | |||
| facc1338aa | |||
| 1df5ae9c2e | |||
| d57ff735df | |||
| 24358176fd | |||
| 4cc1b65e0a | |||
| 3c7b7c7a8f | |||
| 82d427aa60 | |||
| aa60001fbd | |||
| 587161f658 | |||
| 24d75e10cf | |||
| 7247e11b9e | |||
| 0c1eb4a2ce | |||
| 0e8742579c | |||
| e968a271ce | |||
| db3bd66cf1 | |||
| fb340c4028 | |||
| 7ed11617d2 | |||
| 5f656e69b5 | |||
| 452a5b45fe | |||
| 3e635473a6 | |||
| 2194b05310 | |||
| 5195a71251 | |||
| b1114536ea | |||
| 98be79d309 |
@@ -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
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(×tamp.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";
|
||||
|
||||
-100
@@ -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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
-153
@@ -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
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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?;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}")
|
||||
}
|
||||
|
||||
|
||||
@@ -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) =
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(¶ms, &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(¶ms, &verification_key, &sk_i_auth)),
|
||||
);
|
||||
|
||||
// CLIENT: verify the correctness of the (partial)) signatures for coin indices
|
||||
assert!(verify_coin_indices_signatures(
|
||||
¶ms,
|
||||
&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(
|
||||
¶ms,
|
||||
&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(¶ms, &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(¶ms, &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(
|
||||
¶ms,
|
||||
expiration_date,
|
||||
&secret_keys_authorities,
|
||||
&verification_keys_auth,
|
||||
&verification_key,
|
||||
&indices,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// generate coin indices signatures
|
||||
let coin_indices_signatures = generate_coin_indices_signatures(
|
||||
¶ms,
|
||||
&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(
|
||||
¶ms,
|
||||
&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(
|
||||
¶ms,
|
||||
&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(¶ms, &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(
|
||||
¶ms,
|
||||
&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(¶ms, &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(
|
||||
¶ms,
|
||||
&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(
|
||||
¶ms,
|
||||
&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(¶ms, &instance, &witness);
|
||||
assert!(zk_proof.verify(¶ms, &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(
|
||||
¶ms,
|
||||
&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(
|
||||
¶ms,
|
||||
&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(
|
||||
¶ms,
|
||||
expiration_date,
|
||||
&secret_keys_authorities,
|
||||
&verification_keys_auth,
|
||||
&verification_key,
|
||||
&indices,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// generate coin indices signatures
|
||||
let coin_indices_signatures = generate_coin_indices_signatures(
|
||||
¶ms,
|
||||
&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(
|
||||
¶ms,
|
||||
&verification_key,
|
||||
&user_keypair.secret_key(),
|
||||
&pay_info1,
|
||||
false,
|
||||
spend_vv,
|
||||
dates_signatures,
|
||||
coin_indices_signatures,
|
||||
spend_date,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(payment1
|
||||
.spend_verify(¶ms, &verification_key, &pay_info1, spend_date)
|
||||
.unwrap());
|
||||
|
||||
let payment2 = payment1.clone();
|
||||
assert!(payment2
|
||||
.spend_verify(¶ms, &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(
|
||||
¶ms,
|
||||
expiration_date,
|
||||
&secret_keys_authorities,
|
||||
&verification_keys_auth,
|
||||
&verification_key,
|
||||
&indices,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// generate coin indices signatures
|
||||
let coin_indices_signatures = generate_coin_indices_signatures(
|
||||
¶ms,
|
||||
&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(
|
||||
¶ms,
|
||||
&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(¶ms, &verification_key, &pay_info1, spend_date)
|
||||
.unwrap());
|
||||
|
||||
let pay_info2 = PayInfo {
|
||||
pay_info_bytes: [7u8; 72],
|
||||
};
|
||||
let (payment2, _) = upd_wallet
|
||||
.spend(
|
||||
¶ms,
|
||||
&verification_key,
|
||||
&user_keypair.secret_key(),
|
||||
&pay_info2,
|
||||
false,
|
||||
spend_vv,
|
||||
dates_signatures,
|
||||
coin_indices_signatures,
|
||||
spend_date,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(payment2
|
||||
.spend_verify(¶ms, &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(
|
||||
¶ms,
|
||||
expiration_date,
|
||||
&secret_keys_authorities,
|
||||
&verification_keys_auth,
|
||||
&verification_key,
|
||||
&indices,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// generate coin indices signatures
|
||||
let coin_indices_signatures = generate_coin_indices_signatures(
|
||||
¶ms,
|
||||
&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(
|
||||
¶ms,
|
||||
&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(¶ms, &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(
|
||||
¶ms,
|
||||
&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(¶ms, &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(
|
||||
¶ms,
|
||||
expiration_date,
|
||||
&secret_keys_authorities,
|
||||
&verification_keys_auth,
|
||||
&verification_key,
|
||||
&indices,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// generate coin indices signatures
|
||||
let coin_indices_signatures = generate_coin_indices_signatures(
|
||||
¶ms,
|
||||
&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(
|
||||
¶ms,
|
||||
&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(¶ms, &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(
|
||||
¶ms,
|
||||
&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(¶ms, &verification_key, &sk_i_auth);
|
||||
assert!(verify_coin_indices_signatures(
|
||||
¶ms,
|
||||
&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(¶ms, &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(
|
||||
¶ms,
|
||||
&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(¶ms, &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(¶ms, &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(
|
||||
¶ms,
|
||||
expiration_date,
|
||||
&secret_keys_authorities,
|
||||
&verification_keys_auth,
|
||||
&verification_key,
|
||||
&indices,
|
||||
)?;
|
||||
|
||||
// generate coin indices signatures
|
||||
let coin_indices_signatures = generate_coin_indices_signatures(
|
||||
¶ms,
|
||||
&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(
|
||||
¶ms,
|
||||
&verification_key,
|
||||
&user_keypair.secret_key(),
|
||||
&pay_info,
|
||||
false,
|
||||
spend_vv,
|
||||
dates_signatures,
|
||||
coin_indices_signatures,
|
||||
spend_date,
|
||||
)?;
|
||||
|
||||
assert!(payment
|
||||
.spend_verify(¶ms, &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(
|
||||
¶ms,
|
||||
expiration_date,
|
||||
&secret_keys_authorities,
|
||||
&verification_keys_auth,
|
||||
&verification_key,
|
||||
indices,
|
||||
)?;
|
||||
|
||||
// generate coin indices signatures
|
||||
let coin_indices_signatures = generate_coin_indices_signatures(
|
||||
¶ms,
|
||||
&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(
|
||||
¶ms,
|
||||
&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));
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
Generated
+194
-3
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"] }
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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()
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user