Feature/issued credentials api (#4207)
* split up coconut module a bit * internal tool for watching dkg state and updating group contract * debug dkg state * display past dealer data * improved EpochState Display impl * display contract errors + advance epoch state * check admin * panic handler * simplify app.rs * split action enum * added new tab with logger information * new dealing display * sort by index * [fixedup] wip: updating epoch issued credentials - OG 92ade10384a6d7b6c6c222d2e29d69d3b3446a4c * storing and signing partial blinded credentials * starting cleanup * fixed coconut tests + clippy * fixed nym-api tests * removed dkg-manager tool it was moved to a different branch * implemented remaining endpoints * unit tests + bug fixes * clippy * added persistent identity keys to nym-api theyre not yet announced - this will be in another PR * cargo fmt * clippy * fixed loading of old configs without storage paths set * added additional logs for blind-sign endpoint * fixed up licenses * lowercasing error variants * changed 'issued_credentials' to a post * added minimal client support * fixed the unit test
This commit is contained in:
committed by
GitHub
parent
6bba371c90
commit
bac0f24cf7
@@ -9,6 +9,7 @@
|
||||
target
|
||||
.env
|
||||
.env.dev
|
||||
envs/devnet.env
|
||||
/.vscode/settings.json
|
||||
validator/.vscode
|
||||
sample-configs/validator-config.toml
|
||||
|
||||
Generated
+36
-87
@@ -987,27 +987,13 @@ checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae"
|
||||
|
||||
[[package]]
|
||||
name = "bls12_381"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "54757888b09a69be70b5ec303e382a74227392086ba808cb01eeca29233a2397"
|
||||
version = "0.8.0"
|
||||
source = "git+https://github.com/jstuczyn/bls12_381?branch=feature/gt-serialization-0.8.0#c4543fde7d02efea6ecfcf22e14476ddb516b483"
|
||||
dependencies = [
|
||||
"digest 0.9.0",
|
||||
"ff 0.10.1",
|
||||
"group 0.10.0",
|
||||
"pairing 0.20.0",
|
||||
"rand_core 0.6.4",
|
||||
"subtle 2.4.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bls12_381"
|
||||
version = "0.6.0"
|
||||
source = "git+https://github.com/jstuczyn/bls12_381?branch=gt-serialisation#10fb6f700bfda17c8475af3bfd31e3fec15f2278"
|
||||
dependencies = [
|
||||
"digest 0.9.0",
|
||||
"ff 0.11.1",
|
||||
"group 0.11.0",
|
||||
"pairing 0.21.0",
|
||||
"ff 0.13.0",
|
||||
"group 0.13.0",
|
||||
"pairing",
|
||||
"rand_core 0.6.4",
|
||||
"subtle 2.4.1",
|
||||
"zeroize",
|
||||
@@ -3030,26 +3016,6 @@ version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5"
|
||||
|
||||
[[package]]
|
||||
name = "ff"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0f40b2dcd8bc322217a5f6559ae5f9e9d1de202a2ecee2e9eafcbece7562a4f"
|
||||
dependencies = [
|
||||
"rand_core 0.6.4",
|
||||
"subtle 2.4.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ff"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "131655483be284720a17d74ff97592b8e76576dc25563148601df2d7c9080924"
|
||||
dependencies = [
|
||||
"rand_core 0.6.4",
|
||||
"subtle 2.4.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ff"
|
||||
version = "0.12.1"
|
||||
@@ -3066,6 +3032,7 @@ version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449"
|
||||
dependencies = [
|
||||
"bitvec",
|
||||
"rand_core 0.6.4",
|
||||
"subtle 2.4.1",
|
||||
]
|
||||
@@ -3519,30 +3486,6 @@ dependencies = [
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "group"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c363a5301b8f153d80747126a04b3c82073b9fe3130571a9d170cacdeaf7912"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"ff 0.10.1",
|
||||
"rand_core 0.6.4",
|
||||
"subtle 2.4.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "group"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc5ac374b108929de78460075f3dc439fa66df9d8fc77e8f12caa5165fcf0c89"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"ff 0.11.1",
|
||||
"rand_core 0.6.4",
|
||||
"subtle 2.4.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "group"
|
||||
version = "0.12.1"
|
||||
@@ -4286,6 +4229,15 @@ dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "25db6b064527c5d482d0423354fcd07a89a2dfe07b67892e62411946db7f07b0"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.9"
|
||||
@@ -5937,6 +5889,7 @@ dependencies = [
|
||||
"futures-util",
|
||||
"getset",
|
||||
"humantime-serde",
|
||||
"itertools 0.12.0",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"nym-api-requests",
|
||||
@@ -6001,10 +5954,12 @@ dependencies = [
|
||||
"cosmwasm-std",
|
||||
"getset",
|
||||
"nym-coconut-interface",
|
||||
"nym-crypto",
|
||||
"nym-mixnet-contract-common",
|
||||
"nym-node-requests",
|
||||
"schemars",
|
||||
"serde",
|
||||
"tendermint",
|
||||
"ts-rs",
|
||||
]
|
||||
|
||||
@@ -6022,6 +5977,7 @@ dependencies = [
|
||||
"rand 0.7.3",
|
||||
"thiserror",
|
||||
"url",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6257,14 +6213,14 @@ dependencies = [
|
||||
name = "nym-coconut"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"bls12_381 0.6.0",
|
||||
"bls12_381",
|
||||
"bs58 0.4.0",
|
||||
"criterion",
|
||||
"digest 0.9.0",
|
||||
"doc-comment",
|
||||
"ff 0.11.1",
|
||||
"ff 0.13.0",
|
||||
"getrandom 0.2.10",
|
||||
"group 0.11.0",
|
||||
"group 0.13.0",
|
||||
"itertools 0.10.5",
|
||||
"nym-dkg",
|
||||
"nym-pemstore",
|
||||
@@ -6274,6 +6230,7 @@ dependencies = [
|
||||
"serde_derive",
|
||||
"sha2 0.9.9",
|
||||
"thiserror",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6364,7 +6321,7 @@ dependencies = [
|
||||
name = "nym-credentials"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bls12_381 0.5.0",
|
||||
"bls12_381",
|
||||
"cosmrs",
|
||||
"log",
|
||||
"nym-api-requests",
|
||||
@@ -6373,6 +6330,7 @@ dependencies = [
|
||||
"nym-validator-client",
|
||||
"rand 0.7.3",
|
||||
"thiserror",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6406,11 +6364,11 @@ name = "nym-dkg"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bitvec",
|
||||
"bls12_381 0.6.0",
|
||||
"bls12_381",
|
||||
"bs58 0.4.0",
|
||||
"criterion",
|
||||
"ff 0.11.1",
|
||||
"group 0.11.0",
|
||||
"ff 0.13.0",
|
||||
"group 0.13.0",
|
||||
"lazy_static",
|
||||
"nym-contracts-common",
|
||||
"nym-pemstore",
|
||||
@@ -7882,20 +7840,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pairing"
|
||||
version = "0.20.0"
|
||||
version = "0.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7de9d09263c9966e8196fe0380c9dbbc7ea114b5cf371ba29004bc1f9c6db7f3"
|
||||
checksum = "81fec4625e73cf41ef4bb6846cafa6d44736525f442ba45e407c4a000a13996f"
|
||||
dependencies = [
|
||||
"group 0.10.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pairing"
|
||||
version = "0.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2e415e349a3006dd7d9482cdab1c980a845bed1377777d768cb693a44540b42"
|
||||
dependencies = [
|
||||
"group 0.11.0",
|
||||
"group 0.13.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -9790,9 +9739,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.107"
|
||||
version = "1.0.108"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65"
|
||||
checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"ryu",
|
||||
@@ -11014,16 +10963,16 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.9"
|
||||
version = "0.7.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d68074620f57a0b21594d9735eb2e98ab38b17f80d3fcb189fca266771ca60d"
|
||||
checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-sink",
|
||||
"futures-util",
|
||||
"hashbrown 0.12.3",
|
||||
"hashbrown 0.14.1",
|
||||
"pin-project-lite 0.2.13",
|
||||
"slab",
|
||||
"tokio",
|
||||
|
||||
+11
-1
@@ -162,7 +162,8 @@ serde_json = "1.0.91"
|
||||
tap = "1.0.1"
|
||||
time = "0.3.30"
|
||||
thiserror = "1.0.48"
|
||||
tokio = "1.24.1"
|
||||
tokio = "1.33.0"
|
||||
tokio-util = "0.7.10"
|
||||
tokio-tungstenite = "0.20.1"
|
||||
tracing = "0.1.37"
|
||||
tungstenite = { version = "0.20.1", default-features = false }
|
||||
@@ -172,6 +173,14 @@ utoipa-swagger-ui = "3.1.5"
|
||||
url = "2.4"
|
||||
zeroize = "1.6.0"
|
||||
|
||||
# coconut/DKG related
|
||||
# unfortunately until https://github.com/zkcrypto/bls12_381/issues/10 is resolved, we have to rely on the fork
|
||||
# as we need to be able to serialize Gt so that we could create the lookup table for baby-step-giant-step algorithm
|
||||
bls12_381 = { git = "https://github.com/jstuczyn/bls12_381", branch ="feature/gt-serialization-0.8.0" }
|
||||
group = "0.13.0"
|
||||
ff = "0.13.0"
|
||||
|
||||
|
||||
# cosmwasm-related
|
||||
cosmwasm-derive = "=1.3.0"
|
||||
cosmwasm-schema = "=1.3.0"
|
||||
@@ -192,6 +201,7 @@ cw-controllers = { version = "=1.1.0" }
|
||||
bip32 = "0.5.1"
|
||||
cosmrs = "=0.15.0"
|
||||
tendermint-rpc = "0.34" # same version as used by cosmrs
|
||||
tendermint = "0.34" # same version as used by cosmrs
|
||||
prost = "0.12"
|
||||
|
||||
# wasm-related dependencies
|
||||
|
||||
@@ -10,6 +10,7 @@ bip39 = { workspace = true }
|
||||
rand = "0.7.3"
|
||||
thiserror = { workspace = true }
|
||||
url = { workspace = true }
|
||||
zeroize = { workspace = true }
|
||||
|
||||
nym-coconut-interface = { path = "../coconut-interface" }
|
||||
nym-credential-storage = { path = "../credential-storage" }
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::error::BandwidthControllerError;
|
||||
use nym_coconut_interface::{Base58, Parameters};
|
||||
use nym_coconut_interface::Base58;
|
||||
use nym_credential_storage::storage::Storage;
|
||||
use nym_credentials::coconut::bandwidth::{BandwidthVoucher, TOTAL_ATTRIBUTES};
|
||||
use nym_credentials::coconut::bandwidth::BandwidthVoucher;
|
||||
use nym_credentials::coconut::utils::obtain_aggregate_signature;
|
||||
use nym_crypto::asymmetric::{encryption, identity};
|
||||
use nym_network_defaults::VOUCHER_INFO;
|
||||
@@ -12,10 +12,8 @@ use nym_validator_client::coconut::all_coconut_api_clients;
|
||||
use nym_validator_client::nyxd::contract_traits::CoconutBandwidthSigningClient;
|
||||
use nym_validator_client::nyxd::contract_traits::DkgQueryClient;
|
||||
use nym_validator_client::nyxd::Coin;
|
||||
use nym_validator_client::nyxd::Hash;
|
||||
use rand::rngs::OsRng;
|
||||
use state::{KeyPair, State};
|
||||
use std::str::FromStr;
|
||||
use state::State;
|
||||
|
||||
pub mod state;
|
||||
|
||||
@@ -24,30 +22,29 @@ where
|
||||
C: CoconutBandwidthSigningClient + Sync,
|
||||
{
|
||||
let mut rng = OsRng;
|
||||
let signing_keypair = KeyPair::from(identity::KeyPair::new(&mut rng));
|
||||
let encryption_keypair = KeyPair::from(encryption::KeyPair::new(&mut rng));
|
||||
let params = Parameters::new(TOTAL_ATTRIBUTES).unwrap();
|
||||
let signing_key = identity::PrivateKey::new(&mut rng);
|
||||
let encryption_key = encryption::PrivateKey::new(&mut rng);
|
||||
let params = BandwidthVoucher::default_parameters();
|
||||
let voucher_value = amount.amount.to_string();
|
||||
|
||||
let tx_hash = client
|
||||
.deposit(
|
||||
amount,
|
||||
String::from(VOUCHER_INFO),
|
||||
signing_keypair.public_key.clone(),
|
||||
encryption_keypair.public_key.clone(),
|
||||
signing_key.public_key().to_base58_string(),
|
||||
encryption_key.public_key().to_base58_string(),
|
||||
None,
|
||||
)
|
||||
.await?
|
||||
.transaction_hash
|
||||
.to_string();
|
||||
.transaction_hash;
|
||||
|
||||
let voucher = BandwidthVoucher::new(
|
||||
¶ms,
|
||||
voucher_value,
|
||||
VOUCHER_INFO.to_string(),
|
||||
Hash::from_str(&tx_hash).map_err(|_| BandwidthControllerError::InvalidTxHash)?,
|
||||
identity::PrivateKey::from_base58_string(&signing_keypair.private_key)?,
|
||||
encryption::PrivateKey::from_base58_string(&encryption_keypair.private_key)?,
|
||||
tx_hash,
|
||||
signing_key,
|
||||
encryption_key,
|
||||
);
|
||||
|
||||
let state = State { voucher, params };
|
||||
|
||||
@@ -2,32 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use nym_coconut_interface::Parameters;
|
||||
use nym_credentials::coconut::bandwidth::{BandwidthVoucher, TOTAL_ATTRIBUTES};
|
||||
|
||||
use nym_crypto::asymmetric::{encryption, identity};
|
||||
|
||||
pub(crate) struct KeyPair {
|
||||
pub public_key: String,
|
||||
pub private_key: String,
|
||||
}
|
||||
|
||||
impl From<identity::KeyPair> for KeyPair {
|
||||
fn from(kp: identity::KeyPair) -> Self {
|
||||
Self {
|
||||
public_key: kp.public_key().to_base58_string(),
|
||||
private_key: kp.private_key().to_base58_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<encryption::KeyPair> for KeyPair {
|
||||
fn from(kp: encryption::KeyPair) -> Self {
|
||||
Self {
|
||||
public_key: kp.public_key().to_base58_string(),
|
||||
private_key: kp.private_key().to_base58_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
use nym_credentials::coconut::bandwidth::BandwidthVoucher;
|
||||
|
||||
pub struct State {
|
||||
pub voucher: BandwidthVoucher,
|
||||
@@ -38,7 +13,7 @@ impl State {
|
||||
pub fn new(voucher: BandwidthVoucher) -> Self {
|
||||
State {
|
||||
voucher,
|
||||
params: Parameters::new(TOTAL_ATTRIBUTES).unwrap(),
|
||||
params: BandwidthVoucher::default_parameters(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use nym_credential_storage::storage::Storage;
|
||||
use nym_validator_client::coconut::all_coconut_api_clients;
|
||||
use nym_validator_client::nyxd::contract_traits::DkgQueryClient;
|
||||
use std::str::FromStr;
|
||||
use zeroize::Zeroizing;
|
||||
use {
|
||||
nym_coconut_interface::Base58,
|
||||
nym_credentials::coconut::{
|
||||
@@ -46,10 +47,12 @@ impl<C, St: Storage> BandwidthController<C, St> {
|
||||
let voucher_value = u64::from_str(&bandwidth_credential.voucher_value)
|
||||
.map_err(|_| StorageError::InconsistentData)?;
|
||||
let voucher_info = bandwidth_credential.voucher_info.clone();
|
||||
let serial_number =
|
||||
nym_coconut_interface::Attribute::try_from_bs58(bandwidth_credential.serial_number)?;
|
||||
let binding_number =
|
||||
nym_coconut_interface::Attribute::try_from_bs58(bandwidth_credential.binding_number)?;
|
||||
let serial_number = Zeroizing::new(nym_coconut_interface::Attribute::try_from_bs58(
|
||||
bandwidth_credential.serial_number,
|
||||
)?);
|
||||
let binding_number = Zeroizing::new(nym_coconut_interface::Attribute::try_from_bs58(
|
||||
bandwidth_credential.binding_number,
|
||||
)?);
|
||||
let signature =
|
||||
nym_coconut_interface::Signature::try_from_bs58(bandwidth_credential.signature)?;
|
||||
let epoch_id = u64::from_str(&bandwidth_credential.epoch_id)
|
||||
@@ -64,8 +67,8 @@ impl<C, St: Storage> BandwidthController<C, St> {
|
||||
prepare_for_spending(
|
||||
voucher_value,
|
||||
voucher_info,
|
||||
serial_number,
|
||||
binding_number,
|
||||
&serial_number,
|
||||
&binding_number,
|
||||
epoch_id,
|
||||
&signature,
|
||||
&verification_key,
|
||||
|
||||
@@ -9,8 +9,8 @@ edition = "2021"
|
||||
[dependencies]
|
||||
futures = { workspace = true }
|
||||
log = { workspace = true }
|
||||
tokio = { version = "1.24.1", features = ["time", "net", "rt"] }
|
||||
tokio-util = { version = "0.7.4", features = ["codec"] }
|
||||
tokio = { workspace = true, features = ["time", "net", "rt"] }
|
||||
tokio-util = { workspace = true, features = ["codec"] }
|
||||
|
||||
# internal
|
||||
nym-sphinx = { path = "../../nymsphinx" }
|
||||
|
||||
@@ -5,8 +5,12 @@ use crate::nym_api::error::NymAPIError;
|
||||
use crate::nym_api::routes::{CORE_STATUS_COUNT, SINCE_ARG};
|
||||
use async_trait::async_trait;
|
||||
use http_api_client::{ApiClient, NO_PARAMS};
|
||||
use nym_api_requests::coconut::models::{
|
||||
EpochCredentialsResponse, IssuedCredentialResponse, IssuedCredentialsResponse,
|
||||
};
|
||||
use nym_api_requests::coconut::{
|
||||
BlindSignRequestBody, BlindedSignatureResponse, VerifyCredentialBody, VerifyCredentialResponse,
|
||||
BlindSignRequestBody, BlindedSignatureResponse, CredentialsRequestBody, VerifyCredentialBody,
|
||||
VerifyCredentialResponse,
|
||||
};
|
||||
use nym_api_requests::models::{
|
||||
ComputeRewardEstParam, DescribedGateway, GatewayBondAnnotated, GatewayCoreStatusResponse,
|
||||
@@ -15,6 +19,7 @@ use nym_api_requests::models::{
|
||||
MixnodeStatusResponse, MixnodeUptimeHistoryResponse, RewardEstimationResponse,
|
||||
StakeSaturationResponse, UptimeResponse,
|
||||
};
|
||||
use nym_coconut_dkg_common::types::EpochId;
|
||||
use nym_mixnet_contract_common::mixnode::MixNodeDetails;
|
||||
use nym_mixnet_contract_common::{GatewayBond, IdentityKeyRef, MixId};
|
||||
use nym_name_service_common::response::NamesListResponse;
|
||||
@@ -399,6 +404,60 @@ pub trait NymApiClientExt: ApiClient {
|
||||
.await
|
||||
}
|
||||
|
||||
async fn epoch_credentials(
|
||||
&self,
|
||||
dkg_epoch: EpochId,
|
||||
) -> Result<EpochCredentialsResponse, NymAPIError> {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::COCONUT_ROUTES,
|
||||
routes::BANDWIDTH,
|
||||
routes::COCONUT_EPOCH_CREDENTIALS,
|
||||
&dkg_epoch.to_string(),
|
||||
],
|
||||
NO_PARAMS,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn issued_credential(
|
||||
&self,
|
||||
credential_id: i64,
|
||||
) -> Result<IssuedCredentialResponse, NymAPIError> {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::COCONUT_ROUTES,
|
||||
routes::BANDWIDTH,
|
||||
routes::COCONUT_ISSUED_CREDENTIAL,
|
||||
&credential_id.to_string(),
|
||||
],
|
||||
NO_PARAMS,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn issued_credentials(
|
||||
&self,
|
||||
credential_ids: Vec<i64>,
|
||||
) -> Result<IssuedCredentialsResponse, NymAPIError> {
|
||||
self.post_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::COCONUT_ROUTES,
|
||||
routes::BANDWIDTH,
|
||||
routes::COCONUT_ISSUED_CREDENTIALS,
|
||||
],
|
||||
NO_PARAMS,
|
||||
&CredentialsRequestBody {
|
||||
credential_ids,
|
||||
pagination: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_service_providers(&self) -> Result<ServicesListResponse, NymAPIError> {
|
||||
log::trace!("Getting service providers");
|
||||
self.get_json(&[routes::API_VERSION, routes::SERVICE_PROVIDERS], NO_PARAMS)
|
||||
|
||||
@@ -17,6 +17,9 @@ pub const BANDWIDTH: &str = "bandwidth";
|
||||
|
||||
pub const COCONUT_BLIND_SIGN: &str = "blind-sign";
|
||||
pub const COCONUT_VERIFY_BANDWIDTH_CREDENTIAL: &str = "verify-bandwidth-credential";
|
||||
pub const COCONUT_EPOCH_CREDENTIALS: &str = "epoch-credentials";
|
||||
pub const COCONUT_ISSUED_CREDENTIAL: &str = "issued-credential";
|
||||
pub const COCONUT_ISSUED_CREDENTIALS: &str = "issued-credentials";
|
||||
|
||||
pub const STATUS_ROUTES: &str = "status";
|
||||
pub const MIXNODE: &str = "mixnode";
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ pub trait CoconutBandwidthSigningClient {
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
let req = CoconutBandwidthExecuteMsg::DepositFunds {
|
||||
data: DepositData::new(info.to_string(), verification_key, encryption_key),
|
||||
data: DepositData::new(info, verification_key, encryption_key),
|
||||
};
|
||||
self.execute_coconut_bandwidth_contract(
|
||||
fee,
|
||||
|
||||
+22
-2
@@ -6,8 +6,8 @@ use crate::nyxd::error::NyxdError;
|
||||
use crate::nyxd::CosmWasmClient;
|
||||
use async_trait::async_trait;
|
||||
use cw3::{
|
||||
ProposalListResponse, ProposalResponse, VoteListResponse, VoteResponse, VoterListResponse,
|
||||
VoterResponse,
|
||||
ProposalListResponse, ProposalResponse, VoteListResponse, VoteResponse, VoterDetail,
|
||||
VoterListResponse, VoterResponse,
|
||||
};
|
||||
use cw_utils::ThresholdResponse;
|
||||
use nym_multisig_contract_common::msg::QueryMsg as MultisigQueryMsg;
|
||||
@@ -114,6 +114,26 @@ pub trait PagedMultisigQueryClient: MultisigQueryClient {
|
||||
|
||||
Ok(proposals)
|
||||
}
|
||||
|
||||
async fn get_all_voters(&self) -> Result<Vec<VoterDetail>, NyxdError> {
|
||||
let mut voters = Vec::new();
|
||||
let mut start_after = None;
|
||||
|
||||
loop {
|
||||
let mut paged_response = self.list_voters(start_after.take(), None).await?;
|
||||
|
||||
let last_voter = paged_response.voters.last().map(|prop| prop.addr.clone());
|
||||
voters.append(&mut paged_response.voters);
|
||||
|
||||
if let Some(start_after_res) = last_voter {
|
||||
start_after = Some(start_after_res)
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(voters)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::nyxd::TxResponse;
|
||||
|
||||
pub fn find_tx_attribute(tx: &TxResponse, event_type: &str, attribute_key: &str) -> Option<String> {
|
||||
let event = tx.tx_result.events.iter().find(|e| e.kind == event_type)?;
|
||||
let attribute = event
|
||||
.attributes
|
||||
.iter()
|
||||
.find(|attr| attr.key == attribute_key)?;
|
||||
Some(attribute.value.clone())
|
||||
}
|
||||
@@ -47,6 +47,10 @@ pub use cosmrs::Coin as CosmosCoin;
|
||||
pub use cosmrs::Gas;
|
||||
pub use cosmrs::{bip32, AccountId, Denom};
|
||||
pub use cosmwasm_std::Coin as CosmWasmCoin;
|
||||
pub use cw2;
|
||||
pub use cw3;
|
||||
pub use cw4;
|
||||
pub use cw_controllers;
|
||||
pub use fee::{gas_price::GasPrice, GasAdjustable, GasAdjustment};
|
||||
pub use tendermint_rpc::{
|
||||
endpoint::{tx::Response as TxResponse, validators::Response as ValidatorResponse},
|
||||
@@ -67,6 +71,7 @@ pub mod contract_traits;
|
||||
pub mod cosmwasm_client;
|
||||
pub mod error;
|
||||
pub mod fee;
|
||||
pub mod helpers;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Config {
|
||||
|
||||
@@ -21,10 +21,14 @@ pub use nym_coconut::{
|
||||
pub struct Credential {
|
||||
#[getset(get = "pub")]
|
||||
n_params: u32,
|
||||
|
||||
#[getset(get = "pub")]
|
||||
theta: Theta,
|
||||
|
||||
voucher_value: u64,
|
||||
|
||||
voucher_info: String,
|
||||
|
||||
#[getset(get = "pub")]
|
||||
epoch_id: u64,
|
||||
}
|
||||
@@ -64,14 +68,12 @@ impl Credential {
|
||||
|
||||
pub fn verify(&self, verification_key: &VerificationKey) -> bool {
|
||||
let params = Parameters::new(self.n_params).unwrap();
|
||||
let public_attributes = [
|
||||
self.voucher_value.to_string().as_bytes(),
|
||||
self.voucher_info.as_bytes(),
|
||||
]
|
||||
.iter()
|
||||
.map(hash_to_scalar)
|
||||
.collect::<Vec<Attribute>>();
|
||||
nym_coconut::verify_credential(¶ms, verification_key, &self.theta, &public_attributes)
|
||||
|
||||
let hashed_value = hash_to_scalar(self.voucher_value.to_string());
|
||||
let hashed_info = hash_to_scalar(&self.voucher_info);
|
||||
let public_attributes = &[&hashed_value, &hashed_info];
|
||||
|
||||
nym_coconut::verify_credential(¶ms, verification_key, &self.theta, public_attributes)
|
||||
}
|
||||
|
||||
pub fn as_bytes(&self) -> Vec<u8> {
|
||||
@@ -180,8 +182,8 @@ mod tests {
|
||||
¶ms,
|
||||
&verification_key,
|
||||
&signature,
|
||||
serial_number,
|
||||
binding_number,
|
||||
&serial_number,
|
||||
&binding_number,
|
||||
)
|
||||
.unwrap();
|
||||
let credential = Credential::new(4, theta, voucher_value, voucher_info, 42);
|
||||
|
||||
@@ -26,6 +26,10 @@ pub struct Args {
|
||||
}
|
||||
|
||||
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() {
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
// event types
|
||||
pub const DEPOSITED_FUNDS_EVENT_TYPE: &str = "deposited-funds";
|
||||
|
||||
// a 'wasm-' prefix is added to all cosmwasm events
|
||||
pub const COSMWASM_DEPOSITED_FUNDS_EVENT_TYPE: &str = "wasm-deposited-funds";
|
||||
|
||||
// attributes that are used in multiple places
|
||||
pub const DEPOSIT_VALUE: &str = "deposit-value";
|
||||
pub const DEPOSIT_INFO: &str = "deposit-info";
|
||||
|
||||
@@ -174,17 +174,19 @@ impl Display for EpochState {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
EpochState::PublicKeySubmission { resharing } => {
|
||||
write!(f, "PublicKeySubmission with resharing {resharing}")
|
||||
write!(f, "PublicKeySubmission (resharing: {resharing})")
|
||||
}
|
||||
EpochState::DealingExchange { resharing } => {
|
||||
write!(f, "DealingExchange (resharing: {resharing})")
|
||||
}
|
||||
EpochState::DealingExchange { resharing } => write!(f, "DealingExchange {resharing}"),
|
||||
EpochState::VerificationKeySubmission { resharing } => {
|
||||
write!(f, "VerificationKeySubmission with resharing {resharing}")
|
||||
write!(f, "VerificationKeySubmission (resharing: {resharing})")
|
||||
}
|
||||
EpochState::VerificationKeyValidation { resharing } => {
|
||||
write!(f, "VerificationKeyValidation with resharing {resharing}")
|
||||
write!(f, "VerificationKeyValidation (resharing: {resharing})")
|
||||
}
|
||||
EpochState::VerificationKeyFinalization { resharing } => {
|
||||
write!(f, "VerificationKeyFinalization with resharing {resharing}")
|
||||
write!(f, "VerificationKeyFinalization (resharing: {resharing})")
|
||||
}
|
||||
EpochState::InProgress => write!(f, "InProgress"),
|
||||
}
|
||||
|
||||
@@ -6,14 +6,15 @@ edition = "2021"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
bls12_381 = { version = "0.5", default-features = false, features = ["pairings", "alloc", "experimental"] }
|
||||
bls12_381 = { workspace = true, default-features = false, features = ["pairings", "alloc", "experimental"] }
|
||||
cosmrs = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
log = { workspace = true }
|
||||
zeroize = { workspace = true }
|
||||
|
||||
# I guess temporarily until we get serde support in coconut up and running
|
||||
nym-coconut-interface = { path = "../coconut-interface" }
|
||||
nym-crypto = { path = "../crypto", features = ["rand", "asymmetric", "symmetric", "hashing"] }
|
||||
nym-crypto = { path = "../crypto", features = ["rand", "asymmetric"] }
|
||||
nym-api-requests = { path = "../../nym-api/nym-api-requests" }
|
||||
nym-validator-client = { path = "../client-libs/validator-client", default-features = false }
|
||||
|
||||
|
||||
@@ -13,38 +13,60 @@ use nym_coconut_interface::{
|
||||
PrivateAttribute, PublicAttribute, Signature, VerificationKey,
|
||||
};
|
||||
use nym_crypto::asymmetric::{encryption, identity};
|
||||
use zeroize::{Zeroize, ZeroizeOnDrop};
|
||||
|
||||
use super::utils::prepare_credential_for_spending;
|
||||
use crate::error::Error;
|
||||
|
||||
pub const PUBLIC_ATTRIBUTES: u32 = 2;
|
||||
pub const PRIVATE_ATTRIBUTES: u32 = 2;
|
||||
pub const TOTAL_ATTRIBUTES: u32 = PUBLIC_ATTRIBUTES + PRIVATE_ATTRIBUTES;
|
||||
|
||||
#[derive(Zeroize, ZeroizeOnDrop)]
|
||||
pub struct BandwidthVoucher {
|
||||
// a random secret value generated by the client used for double-spending detection
|
||||
// private attributes
|
||||
/// a random secret value generated by the client used for double-spending detection
|
||||
serial_number: PrivateAttribute,
|
||||
// a random secret value generated by the client used to bind multiple credentials together
|
||||
|
||||
/// a random secret value generated by the client used to bind multiple credentials together
|
||||
binding_number: PrivateAttribute,
|
||||
// the value (e.g., bandwidth) encoded in this voucher
|
||||
voucher_value: PublicAttribute,
|
||||
// the plain text value (e.g., bandwidth) encoded in this voucher
|
||||
|
||||
// public atttributes:
|
||||
/// the plain text value (e.g., bandwidth) encoded in this voucher
|
||||
// TODO: in another PR change the value from `"1000"` to `"1000unym"`
|
||||
voucher_value_plain: String,
|
||||
// a field with public information, e.g., type of voucher, interval etc.
|
||||
voucher_info: PublicAttribute,
|
||||
// the plain text information
|
||||
|
||||
/// the plain text information
|
||||
voucher_info_plain: String,
|
||||
// the hash of the deposit transaction
|
||||
|
||||
/// the precomputed value (e.g., bandwidth) encoded in this voucher
|
||||
_voucher_value_prehashed: PublicAttribute,
|
||||
|
||||
/// the precomputed field with public information, e.g., type of voucher, interval etc.
|
||||
_voucher_info_prehashed: PublicAttribute,
|
||||
|
||||
/// the hash of the deposit transaction
|
||||
#[zeroize(skip)]
|
||||
tx_hash: Hash,
|
||||
// base58 encoded private key ensuring the depositer requested these attributes
|
||||
|
||||
/// base58 encoded private key ensuring the depositer requested these attributes
|
||||
signing_key: identity::PrivateKey,
|
||||
// base58 encoded private key ensuring only this client receives the signature share
|
||||
encryption_key: encryption::PrivateKey,
|
||||
|
||||
/// base58 encoded private key ensuring only this client receives the signature share
|
||||
unused_ed25519: encryption::PrivateKey,
|
||||
|
||||
pedersen_commitments_openings: Vec<Attribute>,
|
||||
|
||||
#[zeroize(skip)]
|
||||
blind_sign_request: BlindSignRequest,
|
||||
}
|
||||
|
||||
impl BandwidthVoucher {
|
||||
pub const PUBLIC_ATTRIBUTES: u32 = 2;
|
||||
pub const PRIVATE_ATTRIBUTES: u32 = 2;
|
||||
pub const ENCODED_ATTRIBUTES: u32 = 4;
|
||||
|
||||
pub fn default_parameters() -> Parameters {
|
||||
// safety: the unwrap is fine here as Self::ENCODED_ATTRIBUTES is non-zero
|
||||
Parameters::new(Self::ENCODED_ATTRIBUTES).unwrap()
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
params: &Parameters,
|
||||
voucher_value: String,
|
||||
@@ -57,24 +79,26 @@ impl BandwidthVoucher {
|
||||
let binding_number = params.random_scalar();
|
||||
let voucher_value_plain = voucher_value.clone();
|
||||
let voucher_info_plain = voucher_info.clone();
|
||||
let voucher_value = hash_to_scalar(voucher_value.as_bytes());
|
||||
let voucher_info = hash_to_scalar(voucher_info.as_bytes());
|
||||
|
||||
let _voucher_value_prehashed = hash_to_scalar(voucher_value);
|
||||
let _voucher_info_prehashed = hash_to_scalar(voucher_info);
|
||||
|
||||
let (pedersen_commitments_openings, blind_sign_request) = prepare_blind_sign(
|
||||
params,
|
||||
&[serial_number, binding_number],
|
||||
&[voucher_value, voucher_info],
|
||||
&[&serial_number, &binding_number],
|
||||
&[&_voucher_value_prehashed, &_voucher_info_prehashed],
|
||||
)
|
||||
.unwrap();
|
||||
BandwidthVoucher {
|
||||
serial_number,
|
||||
binding_number,
|
||||
voucher_value,
|
||||
_voucher_value_prehashed,
|
||||
voucher_value_plain,
|
||||
voucher_info,
|
||||
_voucher_info_prehashed,
|
||||
voucher_info_plain,
|
||||
tx_hash,
|
||||
signing_key,
|
||||
encryption_key,
|
||||
unused_ed25519: encryption_key,
|
||||
pedersen_commitments_openings,
|
||||
blind_sign_request,
|
||||
}
|
||||
@@ -87,7 +111,7 @@ impl BandwidthVoucher {
|
||||
let voucher_info_plain_b = self.voucher_info_plain.as_bytes();
|
||||
let tx_hash_b = self.tx_hash.as_bytes();
|
||||
let signing_key_b = self.signing_key.to_bytes();
|
||||
let encryption_key_b = self.encryption_key.to_bytes();
|
||||
let encryption_key_b = self.unused_ed25519.to_bytes();
|
||||
let blind_sign_request_b = self.blind_sign_request.to_bytes();
|
||||
|
||||
let mut ret = Vec::new();
|
||||
@@ -171,13 +195,13 @@ impl BandwidthVoucher {
|
||||
bytes[var_length_pointer..var_length_pointer + voucher_value_plain_no].to_vec(),
|
||||
)
|
||||
.or_else(utf_err)?;
|
||||
let voucher_value = hash_to_scalar(&voucher_value_plain);
|
||||
let _voucher_value_prehashed = hash_to_scalar(&voucher_value_plain);
|
||||
var_length_pointer += voucher_value_plain_no;
|
||||
let voucher_info_plain = String::from_utf8(
|
||||
bytes[var_length_pointer..var_length_pointer + voucher_info_plain_no].to_vec(),
|
||||
)
|
||||
.or_else(utf_err)?;
|
||||
let voucher_info = hash_to_scalar(&voucher_info_plain);
|
||||
let _voucher_info_prehashed = hash_to_scalar(&voucher_info_plain);
|
||||
var_length_pointer += voucher_info_plain_no;
|
||||
let blind_sign_request = BlindSignRequest::from_bytes(
|
||||
&bytes[var_length_pointer..var_length_pointer + blind_sign_request_no],
|
||||
@@ -196,36 +220,43 @@ impl BandwidthVoucher {
|
||||
Ok(Self {
|
||||
serial_number,
|
||||
binding_number,
|
||||
voucher_value,
|
||||
_voucher_value_prehashed,
|
||||
voucher_value_plain,
|
||||
voucher_info,
|
||||
_voucher_info_prehashed,
|
||||
voucher_info_plain,
|
||||
tx_hash,
|
||||
signing_key,
|
||||
encryption_key,
|
||||
unused_ed25519: encryption_key,
|
||||
pedersen_commitments_openings,
|
||||
blind_sign_request,
|
||||
})
|
||||
}
|
||||
|
||||
/// Check if the plain values correspond to the PublicAttributes
|
||||
pub fn verify_against_plain(values: &[PublicAttribute], plain_values: &[String]) -> bool {
|
||||
pub fn verify_against_plain(values: &[&PublicAttribute], plain_values: &[String]) -> bool {
|
||||
values.len() == 2
|
||||
&& plain_values.len() == 2
|
||||
&& values[0] == hash_to_scalar(&plain_values[0])
|
||||
&& values[1] == hash_to_scalar(&plain_values[1])
|
||||
&& values[0] == &hash_to_scalar(&plain_values[0])
|
||||
&& values[1] == &hash_to_scalar(&plain_values[1])
|
||||
}
|
||||
|
||||
pub fn tx_hash(&self) -> &Hash {
|
||||
&self.tx_hash
|
||||
pub fn tx_hash(&self) -> Hash {
|
||||
self.tx_hash
|
||||
}
|
||||
|
||||
pub fn get_public_attributes(&self) -> Vec<PublicAttribute> {
|
||||
vec![self.voucher_value, self.voucher_info]
|
||||
pub fn get_public_attributes(&self) -> Vec<&PublicAttribute> {
|
||||
vec![
|
||||
&self._voucher_value_prehashed,
|
||||
&self._voucher_info_prehashed,
|
||||
]
|
||||
}
|
||||
|
||||
pub fn identity_key(&self) -> &identity::PrivateKey {
|
||||
&self.signing_key
|
||||
}
|
||||
|
||||
pub fn encryption_key(&self) -> &encryption::PrivateKey {
|
||||
&self.encryption_key
|
||||
&self.unused_ed25519
|
||||
}
|
||||
|
||||
pub fn pedersen_commitments_openings(&self) -> &Vec<Attribute> {
|
||||
@@ -247,27 +278,32 @@ impl BandwidthVoucher {
|
||||
]
|
||||
}
|
||||
|
||||
pub fn get_private_attributes(&self) -> Vec<PrivateAttribute> {
|
||||
vec![self.serial_number, self.binding_number]
|
||||
pub fn get_private_attributes(&self) -> Vec<&PrivateAttribute> {
|
||||
vec![&self.serial_number, &self.binding_number]
|
||||
}
|
||||
|
||||
pub fn sign(&self, request: &BlindSignRequest) -> identity::Signature {
|
||||
pub fn signable_plaintext(request: &BlindSignRequest, tx_hash: Hash) -> Vec<u8> {
|
||||
let mut message = request.to_bytes();
|
||||
message.extend_from_slice(self.tx_hash.to_string().as_bytes());
|
||||
self.signing_key.sign(&message)
|
||||
message.extend_from_slice(tx_hash.as_bytes());
|
||||
message
|
||||
}
|
||||
|
||||
pub fn sign(&self) -> identity::Signature {
|
||||
let message = Self::signable_plaintext(&self.blind_sign_request, self.tx_hash);
|
||||
self.signing_key.sign(message)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn prepare_for_spending(
|
||||
voucher_value: u64,
|
||||
voucher_info: String,
|
||||
serial_number: PrivateAttribute,
|
||||
binding_number: PrivateAttribute,
|
||||
serial_number: &PrivateAttribute,
|
||||
binding_number: &PrivateAttribute,
|
||||
epoch_id: u64,
|
||||
signature: &Signature,
|
||||
verification_key: &VerificationKey,
|
||||
) -> Result<Credential, Error> {
|
||||
let params = Parameters::new(TOTAL_ATTRIBUTES)?;
|
||||
let params = Parameters::new(BandwidthVoucher::ENCODED_ATTRIBUTES)?;
|
||||
|
||||
prepare_credential_for_spending(
|
||||
¶ms,
|
||||
@@ -316,24 +352,30 @@ mod test {
|
||||
let deserialized_voucher = BandwidthVoucher::try_from_bytes(&bytes).unwrap();
|
||||
assert_eq!(voucher.serial_number, deserialized_voucher.serial_number);
|
||||
assert_eq!(voucher.binding_number, deserialized_voucher.binding_number);
|
||||
assert_eq!(voucher.voucher_value, deserialized_voucher.voucher_value);
|
||||
assert_eq!(
|
||||
voucher.voucher_value_plain,
|
||||
deserialized_voucher.voucher_value_plain
|
||||
);
|
||||
assert_eq!(voucher.voucher_info, deserialized_voucher.voucher_info);
|
||||
assert_eq!(
|
||||
voucher.voucher_info_plain,
|
||||
deserialized_voucher.voucher_info_plain
|
||||
);
|
||||
assert_eq!(
|
||||
voucher._voucher_value_prehashed,
|
||||
deserialized_voucher._voucher_value_prehashed
|
||||
);
|
||||
assert_eq!(
|
||||
voucher._voucher_info_prehashed,
|
||||
deserialized_voucher._voucher_info_prehashed
|
||||
);
|
||||
assert_eq!(voucher.tx_hash, deserialized_voucher.tx_hash);
|
||||
assert_eq!(
|
||||
voucher.signing_key.to_string(),
|
||||
deserialized_voucher.signing_key.to_string()
|
||||
);
|
||||
assert_eq!(
|
||||
voucher.encryption_key.to_string(),
|
||||
deserialized_voucher.encryption_key.to_string()
|
||||
voucher.unused_ed25519.to_string(),
|
||||
deserialized_voucher.unused_ed25519.to_string()
|
||||
);
|
||||
assert_eq!(
|
||||
voucher.pedersen_commitments_openings,
|
||||
@@ -371,11 +413,11 @@ mod test {
|
||||
]
|
||||
));
|
||||
assert!(!BandwidthVoucher::verify_against_plain(
|
||||
&[voucher.get_public_attributes()[0], Attribute::one()],
|
||||
&[voucher.get_public_attributes()[0], &Attribute::one()],
|
||||
&voucher.get_public_attributes_plain()
|
||||
));
|
||||
assert!(!BandwidthVoucher::verify_against_plain(
|
||||
&[Attribute::one(), voucher.get_public_attributes()[1]],
|
||||
&[&Attribute::one(), voucher.get_public_attributes()[1]],
|
||||
&voucher.get_public_attributes_plain()
|
||||
));
|
||||
assert!(BandwidthVoucher::verify_against_plain(
|
||||
|
||||
@@ -2,5 +2,4 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub mod bandwidth;
|
||||
pub mod params;
|
||||
pub mod utils;
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use nym_crypto::aes::Aes128;
|
||||
use nym_crypto::blake3;
|
||||
use nym_crypto::ctr;
|
||||
|
||||
type Aes128Ctr = ctr::Ctr64LE<Aes128>;
|
||||
|
||||
/// Hashing algorithm used during hkdf for ephemeral shared key generation per blinded signature
|
||||
/// response encryption.
|
||||
pub type NymApiCredentialHkdfAlgorithm = blake3::Hasher;
|
||||
|
||||
/// Encryption algorithm used for end-to-end encryption of blinded signature response
|
||||
pub type NymApiCredentialEncryptionAlgorithm = Aes128Ctr;
|
||||
@@ -1,18 +1,14 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::coconut::bandwidth::{BandwidthVoucher, PRIVATE_ATTRIBUTES, PUBLIC_ATTRIBUTES};
|
||||
use crate::coconut::params::{NymApiCredentialEncryptionAlgorithm, NymApiCredentialHkdfAlgorithm};
|
||||
use crate::coconut::bandwidth::BandwidthVoucher;
|
||||
use crate::error::Error;
|
||||
use log::{debug, warn};
|
||||
use nym_api_requests::coconut::BlindSignRequestBody;
|
||||
use nym_coconut_interface::{
|
||||
aggregate_signature_shares, aggregate_verification_keys, prove_bandwidth_credential, Attribute,
|
||||
BlindedSignature, Credential, Parameters, Signature, SignatureShare, VerificationKey,
|
||||
Credential, Parameters, Signature, SignatureShare, VerificationKey,
|
||||
};
|
||||
use nym_crypto::asymmetric::encryption::PublicKey;
|
||||
use nym_crypto::shared_key::recompute_shared_key;
|
||||
use nym_crypto::symmetric::stream_cipher;
|
||||
use nym_validator_client::client::CoconutApiClient;
|
||||
|
||||
pub async fn obtain_aggregate_verification_key(
|
||||
@@ -36,47 +32,34 @@ pub async fn obtain_aggregate_verification_key(
|
||||
|
||||
async fn obtain_partial_credential(
|
||||
params: &Parameters,
|
||||
attributes: &BandwidthVoucher,
|
||||
voucher: &BandwidthVoucher,
|
||||
client: &nym_validator_client::client::NymApiClient,
|
||||
validator_vk: &VerificationKey,
|
||||
) -> Result<Signature, Error> {
|
||||
let public_attributes = attributes.get_public_attributes();
|
||||
let public_attributes_plain = attributes.get_public_attributes_plain();
|
||||
let private_attributes = attributes.get_private_attributes();
|
||||
let blind_sign_request = attributes.blind_sign_request();
|
||||
let public_attributes_plain = voucher.get_public_attributes_plain();
|
||||
let blind_sign_request = voucher.blind_sign_request();
|
||||
let request_signature = voucher.sign();
|
||||
|
||||
let blind_sign_request_body = BlindSignRequestBody::new(
|
||||
blind_sign_request,
|
||||
attributes.tx_hash().to_string(),
|
||||
attributes.sign(blind_sign_request).to_base58_string(),
|
||||
&public_attributes,
|
||||
blind_sign_request.clone(),
|
||||
voucher.tx_hash(),
|
||||
request_signature,
|
||||
public_attributes_plain,
|
||||
(public_attributes.len() + private_attributes.len()) as u32,
|
||||
);
|
||||
let response = client.blind_sign(&blind_sign_request_body).await?;
|
||||
let encrypted_signature = response.encrypted_signature;
|
||||
let remote_key = PublicKey::from_bytes(&response.remote_key)?;
|
||||
|
||||
let encryption_key = recompute_shared_key::<
|
||||
NymApiCredentialEncryptionAlgorithm,
|
||||
NymApiCredentialHkdfAlgorithm,
|
||||
>(&remote_key, attributes.encryption_key());
|
||||
let zero_iv = stream_cipher::zero_iv::<NymApiCredentialEncryptionAlgorithm>();
|
||||
let blinded_signature_bytes = stream_cipher::decrypt::<NymApiCredentialEncryptionAlgorithm>(
|
||||
&encryption_key,
|
||||
&zero_iv,
|
||||
&encrypted_signature,
|
||||
);
|
||||
let blinded_signature = response.blinded_signature;
|
||||
|
||||
let blinded_signature = BlindedSignature::from_bytes(&blinded_signature_bytes)?;
|
||||
let public_attributes = voucher.get_public_attributes();
|
||||
let private_attributes = voucher.get_private_attributes();
|
||||
|
||||
let unblinded_signature = blinded_signature.unblind(
|
||||
let unblinded_signature = blinded_signature.unblind_and_verify(
|
||||
params,
|
||||
validator_vk,
|
||||
&private_attributes,
|
||||
&public_attributes,
|
||||
&blind_sign_request.get_commitment_hash(),
|
||||
attributes.pedersen_commitments_openings(),
|
||||
voucher.pedersen_commitments_openings(),
|
||||
)?;
|
||||
|
||||
Ok(unblinded_signature)
|
||||
@@ -84,16 +67,13 @@ async fn obtain_partial_credential(
|
||||
|
||||
pub async fn obtain_aggregate_signature(
|
||||
params: &Parameters,
|
||||
attributes: &BandwidthVoucher,
|
||||
voucher: &BandwidthVoucher,
|
||||
coconut_api_clients: &[CoconutApiClient],
|
||||
threshold: u64,
|
||||
) -> Result<Signature, Error> {
|
||||
if coconut_api_clients.is_empty() {
|
||||
return Err(Error::NoValidatorsAvailable);
|
||||
}
|
||||
let public_attributes = attributes.get_public_attributes();
|
||||
let private_attributes = attributes.get_private_attributes();
|
||||
|
||||
let mut shares = Vec::with_capacity(coconut_api_clients.len());
|
||||
let validators_partial_vks: Vec<_> = coconut_api_clients
|
||||
.iter()
|
||||
@@ -114,7 +94,7 @@ pub async fn obtain_aggregate_signature(
|
||||
|
||||
match obtain_partial_credential(
|
||||
params,
|
||||
attributes,
|
||||
voucher,
|
||||
&coconut_api_client.api_client,
|
||||
&coconut_api_client.verification_key,
|
||||
)
|
||||
@@ -136,6 +116,9 @@ pub async fn obtain_aggregate_signature(
|
||||
return Err(Error::NotEnoughShares);
|
||||
}
|
||||
|
||||
let public_attributes = voucher.get_public_attributes();
|
||||
let private_attributes = voucher.get_private_attributes();
|
||||
|
||||
let mut attributes = Vec::with_capacity(private_attributes.len() + public_attributes.len());
|
||||
attributes.extend_from_slice(&private_attributes);
|
||||
attributes.extend_from_slice(&public_attributes);
|
||||
@@ -150,8 +133,8 @@ pub fn prepare_credential_for_spending(
|
||||
params: &Parameters,
|
||||
voucher_value: u64,
|
||||
voucher_info: String,
|
||||
serial_number: Attribute,
|
||||
binding_number: Attribute,
|
||||
serial_number: &Attribute,
|
||||
binding_number: &Attribute,
|
||||
epoch_id: u64,
|
||||
signature: &Signature,
|
||||
verification_key: &VerificationKey,
|
||||
@@ -165,7 +148,7 @@ pub fn prepare_credential_for_spending(
|
||||
)?;
|
||||
|
||||
Ok(Credential::new(
|
||||
PUBLIC_ATTRIBUTES + PRIVATE_ATTRIBUTES,
|
||||
BandwidthVoucher::ENCODED_ATTRIBUTES,
|
||||
theta,
|
||||
voucher_value,
|
||||
voucher_info,
|
||||
|
||||
@@ -201,6 +201,17 @@ impl<'a> From<&'a PrivateKey> for PublicKey {
|
||||
}
|
||||
|
||||
impl PrivateKey {
|
||||
#[cfg(feature = "rand")]
|
||||
pub fn new<R: RngCore + CryptoRng>(rng: &mut R) -> Self {
|
||||
let x25519_secret = x25519_dalek::StaticSecret::new(rng);
|
||||
|
||||
PrivateKey(x25519_secret)
|
||||
}
|
||||
|
||||
pub fn public_key(&self) -> PublicKey {
|
||||
self.into()
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> [u8; PRIVATE_KEY_SIZE] {
|
||||
self.0.to_bytes()
|
||||
}
|
||||
|
||||
@@ -5,13 +5,14 @@ pub use ed25519_dalek::ed25519::signature::Signature as SignatureTrait;
|
||||
pub use ed25519_dalek::SignatureError;
|
||||
pub use ed25519_dalek::{Verifier, PUBLIC_KEY_LENGTH, SECRET_KEY_LENGTH, SIGNATURE_LENGTH};
|
||||
use nym_pemstore::traits::{PemStorableKey, PemStorableKeyPair};
|
||||
#[cfg(feature = "sphinx")]
|
||||
use nym_sphinx_types::{DestinationAddressBytes, DESTINATION_ADDRESS_LENGTH};
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
use std::str::FromStr;
|
||||
use thiserror::Error;
|
||||
use zeroize::{Zeroize, ZeroizeOnDrop};
|
||||
|
||||
#[cfg(feature = "sphinx")]
|
||||
use nym_sphinx_types::{DestinationAddressBytes, DESTINATION_ADDRESS_LENGTH};
|
||||
|
||||
#[cfg(feature = "rand")]
|
||||
use rand::{CryptoRng, RngCore};
|
||||
#[cfg(feature = "serde")]
|
||||
@@ -224,6 +225,17 @@ impl<'a> From<&'a PrivateKey> for PublicKey {
|
||||
}
|
||||
|
||||
impl PrivateKey {
|
||||
#[cfg(feature = "rand")]
|
||||
pub fn new<R: RngCore + CryptoRng>(rng: &mut R) -> Self {
|
||||
let ed25519_secret = ed25519_dalek::SecretKey::generate(rng);
|
||||
|
||||
PrivateKey(ed25519_secret)
|
||||
}
|
||||
|
||||
pub fn public_key(&self) -> PublicKey {
|
||||
self.into()
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> [u8; SECRET_KEY_LENGTH] {
|
||||
self.0.to_bytes()
|
||||
}
|
||||
@@ -295,7 +307,7 @@ impl PemStorableKey for PrivateKey {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
pub struct Signature(ed25519_dalek::Signature);
|
||||
|
||||
impl Signature {
|
||||
@@ -319,6 +331,14 @@ impl Signature {
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Signature {
|
||||
type Err = Ed25519RecoveryError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Signature::from_base58_string(s)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl Serialize for Signature {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
|
||||
@@ -11,7 +11,7 @@ bitvec = "1.0.0"
|
||||
|
||||
# unfortunately until https://github.com/zkcrypto/bls12_381/issues/10 is resolved, we have to rely on the fork
|
||||
# as we need to be able to serialize Gt so that we could create the lookup table for baby-step-giant-step algorithm
|
||||
bls12_381 = { git = "https://github.com/jstuczyn/bls12_381", branch ="gt-serialisation", default-features = false, features = ["alloc", "pairings", "experimental", "zeroize"] }
|
||||
bls12_381 = { workspace = true, default-features = false, features = ["alloc", "pairings", "experimental", "zeroize"] }
|
||||
nym-contracts-common = { path = "../cosmwasm-smart-contracts/contracts-common", optional = true }
|
||||
bs58 = "0.4"
|
||||
|
||||
@@ -29,11 +29,11 @@ zeroize = { workspace = true, features = ["zeroize_derive"] }
|
||||
nym-pemstore = { path = "../pemstore" }
|
||||
|
||||
[dependencies.group]
|
||||
version = "0.11"
|
||||
workspace = true
|
||||
default-features = false
|
||||
|
||||
[dependencies.ff]
|
||||
version = "0.11"
|
||||
workspace = true
|
||||
default-features = false
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -20,7 +20,7 @@ tokio = { version = "1.24.1", features = [
|
||||
"net",
|
||||
"io-util",
|
||||
] }
|
||||
tokio-util = { version = "0.7.4", features = ["codec"] }
|
||||
tokio-util = { workspace = true, features = ["codec"] }
|
||||
url = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ edition = "2021"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
bls12_381 = { git = "https://github.com/jstuczyn/bls12_381", branch ="gt-serialisation", default-features = false, features = ["pairings", "alloc", "experimental"] }
|
||||
bls12_381 = { workspace = true, default-features = false, features = ["pairings", "alloc", "experimental"] }
|
||||
itertools = "0.10"
|
||||
digest = "0.9"
|
||||
rand = "0.8"
|
||||
@@ -16,16 +16,17 @@ serde = { workspace = true }
|
||||
serde_derive = "1.0"
|
||||
bs58 = "0.4.0"
|
||||
sha2 = "0.9"
|
||||
zeroize = { workspace = true, optional = true }
|
||||
|
||||
nym-dkg = { path = "../dkg" }
|
||||
nym-pemstore = { path = "../pemstore" }
|
||||
|
||||
[dependencies.ff]
|
||||
version = "0.11"
|
||||
workspace = true
|
||||
default-features = false
|
||||
|
||||
[dependencies.group]
|
||||
version = "0.11"
|
||||
workspace = true
|
||||
default-features = false
|
||||
|
||||
[dev-dependencies]
|
||||
@@ -38,7 +39,9 @@ name = "benchmarks"
|
||||
harness = false
|
||||
|
||||
[features]
|
||||
key-zeroize = ["zeroize", "bls12_381/zeroize"]
|
||||
default = []
|
||||
|
||||
|
||||
[target.'cfg(target_env = "wasm32-unknown-unknown")'.dependencies]
|
||||
getrandom = { version="0.2", features=["js"] }
|
||||
|
||||
@@ -7,7 +7,7 @@ use ff::Field;
|
||||
use group::{Curve, Group};
|
||||
use nym_coconut::{
|
||||
aggregate_signature_shares, aggregate_verification_keys, blind_sign, prepare_blind_sign,
|
||||
prove_bandwidth_credential, setup, ttp_keygen, verify_credential,
|
||||
prove_bandwidth_credential, random_scalars_refs, setup, ttp_keygen, verify_credential,
|
||||
verify_partial_blind_signature, Attribute, BlindedSignature, Parameters, Signature,
|
||||
SignatureShare, VerificationKey,
|
||||
};
|
||||
@@ -66,8 +66,8 @@ fn unblind_and_aggregate(
|
||||
params: &Parameters,
|
||||
blinded_signatures: &[BlindedSignature],
|
||||
partial_verification_keys: &[VerificationKey],
|
||||
private_attributes: &[Attribute],
|
||||
public_attributes: &[Attribute],
|
||||
private_attributes: &[&Attribute],
|
||||
public_attributes: &[&Attribute],
|
||||
commitment_hash: &G1Projective,
|
||||
pedersen_commitments_openings: &[Scalar],
|
||||
verification_key: &VerificationKey,
|
||||
@@ -78,7 +78,7 @@ fn unblind_and_aggregate(
|
||||
.zip(partial_verification_keys.iter())
|
||||
.map(|(signature, partial_verification_key)| {
|
||||
signature
|
||||
.unblind(
|
||||
.unblind_and_verify(
|
||||
params,
|
||||
partial_verification_key,
|
||||
private_attributes,
|
||||
@@ -171,10 +171,10 @@ fn bench_coconut(c: &mut Criterion) {
|
||||
|
||||
let params = setup(case.num_public_attrs + case.num_private_attrs).unwrap();
|
||||
|
||||
let public_attributes = params.n_random_scalars(case.num_public_attrs as usize);
|
||||
random_scalars_refs!(public_attributes, params, case.num_public_attrs as usize);
|
||||
let serial_number = params.random_scalar();
|
||||
let binding_number = params.random_scalar();
|
||||
let private_attributes = vec![serial_number, binding_number];
|
||||
let private_attributes = vec![&serial_number, &binding_number];
|
||||
|
||||
// The prepare blind sign is performed by the user
|
||||
let (pedersen_commitments_openings, blind_sign_request) =
|
||||
@@ -213,7 +213,7 @@ fn bench_coconut(c: &mut Criterion) {
|
||||
b.iter(|| {
|
||||
blind_sign(
|
||||
¶ms,
|
||||
&keypair.secret_key(),
|
||||
keypair.secret_key(),
|
||||
&blind_sign_request,
|
||||
&public_attributes,
|
||||
)
|
||||
@@ -228,7 +228,7 @@ fn bench_coconut(c: &mut Criterion) {
|
||||
for keypair in coconut_keypairs.iter() {
|
||||
let blinded_signature = blind_sign(
|
||||
¶ms,
|
||||
&keypair.secret_key(),
|
||||
keypair.secret_key(),
|
||||
&blind_sign_request,
|
||||
&public_attributes,
|
||||
)
|
||||
@@ -238,7 +238,7 @@ fn bench_coconut(c: &mut Criterion) {
|
||||
|
||||
let verification_keys: Vec<VerificationKey> = coconut_keypairs
|
||||
.iter()
|
||||
.map(|keypair| keypair.verification_key())
|
||||
.map(|keypair| keypair.verification_key().clone())
|
||||
.collect();
|
||||
|
||||
// verify a random partial blind signature
|
||||
@@ -310,8 +310,8 @@ fn bench_coconut(c: &mut Criterion) {
|
||||
¶ms,
|
||||
&aggr_verification_key,
|
||||
&aggregated_signature,
|
||||
serial_number,
|
||||
binding_number,
|
||||
&serial_number,
|
||||
&binding_number,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@@ -329,8 +329,8 @@ fn bench_coconut(c: &mut Criterion) {
|
||||
¶ms,
|
||||
&aggr_verification_key,
|
||||
&aggregated_signature,
|
||||
serial_number,
|
||||
binding_number,
|
||||
&serial_number,
|
||||
&binding_number,
|
||||
)
|
||||
.unwrap()
|
||||
})
|
||||
|
||||
@@ -34,7 +34,10 @@ impl TryFrom<&[u8]> for Ciphertext {
|
||||
)));
|
||||
}
|
||||
|
||||
// safety: we just checked for the length so the unwraps are fine
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let c1_bytes: &[u8; 48] = &bytes[..48].try_into().unwrap();
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let c2_bytes: &[u8; 48] = &bytes[48..].try_into().unwrap();
|
||||
|
||||
let c1 = try_deserialize_g1_projective(
|
||||
@@ -112,7 +115,16 @@ impl Bytable for PrivateKey {
|
||||
}
|
||||
|
||||
fn try_from_byte_slice(slice: &[u8]) -> Result<Self> {
|
||||
PrivateKey::from_bytes(slice.try_into().unwrap())
|
||||
let received = slice.len();
|
||||
let Ok(arr) = slice.try_into() else {
|
||||
return Err(CoconutError::UnexpectedArrayLength {
|
||||
typ: "elgamal::PrivateKey".to_string(),
|
||||
received,
|
||||
expected: 32,
|
||||
});
|
||||
};
|
||||
|
||||
PrivateKey::from_bytes(arr)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,21 +157,36 @@ impl PublicKey {
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> [u8; 48] {
|
||||
self.to_byte_vec().try_into().unwrap()
|
||||
self.0.to_affine().to_compressed()
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: &[u8; 48]) -> Result<PublicKey> {
|
||||
Ok(PublicKey::try_from(bytes.to_vec().as_slice()).unwrap())
|
||||
try_deserialize_g1_projective(
|
||||
bytes,
|
||||
CoconutError::Deserialization(
|
||||
"Failed to deserialize compressed ElGamal public key".to_string(),
|
||||
),
|
||||
)
|
||||
.map(PublicKey)
|
||||
}
|
||||
}
|
||||
|
||||
impl Bytable for PublicKey {
|
||||
fn to_byte_vec(&self) -> Vec<u8> {
|
||||
self.0.to_affine().to_compressed().into()
|
||||
self.to_bytes().into()
|
||||
}
|
||||
|
||||
fn try_from_byte_slice(slice: &[u8]) -> Result<Self> {
|
||||
Ok(PublicKey::from_bytes(slice.try_into().unwrap()).unwrap())
|
||||
let received = slice.len();
|
||||
let Ok(arr) = slice.try_into() else {
|
||||
return Err(CoconutError::UnexpectedArrayLength {
|
||||
typ: "elgamal::PublicKey".to_string(),
|
||||
received,
|
||||
expected: 48,
|
||||
});
|
||||
};
|
||||
|
||||
PublicKey::from_bytes(arr)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,13 +194,7 @@ impl TryFrom<&[u8]> for PublicKey {
|
||||
type Error = CoconutError;
|
||||
|
||||
fn try_from(slice: &[u8]) -> Result<PublicKey> {
|
||||
try_deserialize_g1_projective(
|
||||
slice.try_into().unwrap(),
|
||||
CoconutError::Deserialization(
|
||||
"Failed to deserialize compressed ElGamal public key".to_string(),
|
||||
),
|
||||
)
|
||||
.map(PublicKey)
|
||||
PublicKey::try_from_byte_slice(slice)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,7 +246,7 @@ pub fn elgamal_keygen(params: &Parameters) -> ElGamalKeyPair {
|
||||
|
||||
pub fn compute_attribute_encryption(
|
||||
params: &Parameters,
|
||||
private_attributes: &[Attribute],
|
||||
private_attributes: &[&Attribute],
|
||||
pub_key: &PublicKey,
|
||||
commitment_hash: G1Projective,
|
||||
) -> (Vec<Ciphertext>, Vec<EphemeralKey>) {
|
||||
|
||||
@@ -50,4 +50,20 @@ pub enum CoconutError {
|
||||
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 decode the base58 representation: {0}")]
|
||||
Base58DecodingFailure(#[from] bs58::decode::Error),
|
||||
|
||||
#[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,
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use std::convert::TryInto;
|
||||
#![warn(clippy::expect_used)]
|
||||
#![warn(clippy::unwrap_used)]
|
||||
|
||||
use bls12_381::Scalar;
|
||||
|
||||
pub use crate::traits::Bytable;
|
||||
pub use elgamal::elgamal_keygen;
|
||||
pub use elgamal::ElGamalKeyPair;
|
||||
pub use elgamal::PublicKey;
|
||||
@@ -30,6 +28,7 @@ pub use scheme::BlindedSignature;
|
||||
pub use scheme::Signature;
|
||||
pub use scheme::SignatureShare;
|
||||
pub use traits::Base58;
|
||||
pub use traits::Bytable;
|
||||
pub use utils::hash_to_scalar;
|
||||
|
||||
pub mod elgamal;
|
||||
@@ -41,18 +40,6 @@ pub mod tests;
|
||||
mod traits;
|
||||
mod utils;
|
||||
|
||||
pub type Attribute = Scalar;
|
||||
pub type Attribute = bls12_381::Scalar;
|
||||
pub type PrivateAttribute = Attribute;
|
||||
pub type PublicAttribute = Attribute;
|
||||
|
||||
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, CoconutError> {
|
||||
Ok(Attribute::from_bytes(slice.try_into().unwrap()).unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
impl Base58 for Attribute {}
|
||||
|
||||
@@ -91,8 +91,8 @@ impl ProofCmCs {
|
||||
commitment_opening: &Scalar,
|
||||
commitments: &[G1Projective],
|
||||
pedersen_commitments_openings: &[Scalar],
|
||||
private_attributes: &[Attribute],
|
||||
public_attributes: &[Attribute],
|
||||
private_attributes: &[&Attribute],
|
||||
public_attributes: &[&Attribute],
|
||||
) -> Self {
|
||||
// note: this is only called from `prepare_blind_sign` that already checks
|
||||
// whether private attributes are non-empty and whether we don't have too many
|
||||
@@ -162,11 +162,8 @@ impl ProofCmCs {
|
||||
&challenge,
|
||||
&pedersen_commitments_openings.iter().collect::<Vec<_>>(),
|
||||
);
|
||||
let response_attributes = produce_responses(
|
||||
&witness_attributes,
|
||||
&challenge,
|
||||
&private_attributes.iter().collect::<Vec<_>>(),
|
||||
);
|
||||
let response_attributes =
|
||||
produce_responses(&witness_attributes, &challenge, private_attributes);
|
||||
|
||||
ProofCmCs {
|
||||
challenge,
|
||||
@@ -181,7 +178,7 @@ impl ProofCmCs {
|
||||
params: &Parameters,
|
||||
commitment: &G1Projective,
|
||||
commitments: &[G1Projective],
|
||||
public_attributes: &[Attribute],
|
||||
public_attributes: &[&Attribute],
|
||||
) -> bool {
|
||||
if self.response_attributes.len() != commitments.len() {
|
||||
return false;
|
||||
@@ -203,7 +200,7 @@ impl ProofCmCs {
|
||||
- public_attributes
|
||||
.iter()
|
||||
.zip(params.gen_hs().iter().skip(self.response_attributes.len()))
|
||||
.map(|(pub_attr, hs)| hs * pub_attr)
|
||||
.map(|(&pub_attr, hs)| hs * pub_attr)
|
||||
.sum::<G1Projective>())
|
||||
* self.challenge
|
||||
+ g1 * self.response_opening
|
||||
@@ -280,8 +277,12 @@ impl ProofCmCs {
|
||||
}
|
||||
|
||||
let mut idx = 0;
|
||||
// safety: bound checked + constant offset
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let challenge_bytes = bytes[idx..idx + 32].try_into().unwrap();
|
||||
idx += 32;
|
||||
// safety: bound checked + constant offset
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let response_opening_bytes = bytes[idx..idx + 32].try_into().unwrap();
|
||||
idx += 32;
|
||||
|
||||
@@ -297,6 +298,8 @@ impl ProofCmCs {
|
||||
),
|
||||
)?;
|
||||
|
||||
// safety: bound checked + constant offset
|
||||
#[allow(clippy::unwrap_used)]
|
||||
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 {
|
||||
@@ -313,6 +316,8 @@ impl ProofCmCs {
|
||||
CoconutError::Deserialization("Failed to deserialize openings response".to_string()),
|
||||
)?;
|
||||
|
||||
// safety: bound checked + constant offset
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let rm_len = u64::from_le_bytes(bytes[ro_end..ro_end + 8].try_into().unwrap());
|
||||
let response_attributes = try_deserialize_scalar_vec(
|
||||
rm_len,
|
||||
@@ -462,7 +467,7 @@ impl ProofKappaZeta {
|
||||
|
||||
pub(crate) fn from_bytes(bytes: &[u8]) -> Result<Self> {
|
||||
// at the very minimum there must be a single attribute being proven
|
||||
if bytes.len() < 32 * 4 || (bytes.len()) % 32 != 0 {
|
||||
if bytes.len() != 128 {
|
||||
return Err(CoconutError::DeserializationInvalidLength {
|
||||
actual: bytes.len(),
|
||||
modulus_target: bytes.len(),
|
||||
@@ -472,24 +477,32 @@ impl ProofKappaZeta {
|
||||
});
|
||||
}
|
||||
|
||||
// safety: bound checked + constant offset
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let challenge_bytes = bytes[..32].try_into().unwrap();
|
||||
let challenge = try_deserialize_scalar(
|
||||
&challenge_bytes,
|
||||
CoconutError::Deserialization("Failed to deserialize challenge".to_string()),
|
||||
)?;
|
||||
|
||||
// safety: bound checked + constant offset
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let serial_number_bytes = &bytes[32..64].try_into().unwrap();
|
||||
let response_serial_number = try_deserialize_scalar(
|
||||
serial_number_bytes,
|
||||
CoconutError::Deserialization("failed to deserialize the serial number".to_string()),
|
||||
)?;
|
||||
|
||||
// safety: bound checked + constant offset
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let binding_number_bytes = &bytes[64..96].try_into().unwrap();
|
||||
let response_binding_number = try_deserialize_scalar(
|
||||
binding_number_bytes,
|
||||
CoconutError::Deserialization("failed to deserialize the binding number".to_string()),
|
||||
)?;
|
||||
|
||||
// safety: bound checked + constant offset
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let blinder_bytes = bytes[96..].try_into().unwrap();
|
||||
let response_blinder = try_deserialize_scalar(
|
||||
&blinder_bytes,
|
||||
@@ -512,14 +525,13 @@ impl ProofKappaZeta {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use group::Group;
|
||||
use rand::thread_rng;
|
||||
|
||||
use super::*;
|
||||
use crate::scheme::keygen::keygen;
|
||||
use crate::scheme::setup::setup;
|
||||
use crate::scheme::verification::{compute_kappa, compute_zeta};
|
||||
|
||||
use super::*;
|
||||
use crate::tests::helpers::random_scalars_refs;
|
||||
use group::Group;
|
||||
use rand::thread_rng;
|
||||
|
||||
#[test]
|
||||
fn proof_cm_cs_bytes_roundtrip() {
|
||||
@@ -530,7 +542,7 @@ mod tests {
|
||||
let r = params.random_scalar();
|
||||
let cms: [G1Projective; 1] = [G1Projective::random(&mut rng)];
|
||||
let rs = params.n_random_scalars(1);
|
||||
let private_attributes = params.n_random_scalars(1);
|
||||
random_scalars_refs!(private_attributes, params, 1);
|
||||
|
||||
// 0 public 1 private
|
||||
let pi_s = ProofCmCs::construct(¶ms, &cm, &r, &cms, &rs, &private_attributes, &[]);
|
||||
@@ -546,7 +558,7 @@ mod tests {
|
||||
G1Projective::random(&mut rng),
|
||||
];
|
||||
let rs = params.n_random_scalars(2);
|
||||
let private_attributes = params.n_random_scalars(2);
|
||||
random_scalars_refs!(private_attributes, params, 2);
|
||||
|
||||
// 0 public 2 privates
|
||||
let pi_s = ProofCmCs::construct(¶ms, &cm, &r, &cms, &rs, &private_attributes, &[]);
|
||||
@@ -562,20 +574,20 @@ mod tests {
|
||||
let keypair = keygen(¶ms);
|
||||
|
||||
// we don't care about 'correctness' of the proof. only whether we can correctly recover it from bytes
|
||||
let serial_number = params.random_scalar();
|
||||
let binding_number = params.random_scalar();
|
||||
let serial_number = ¶ms.random_scalar();
|
||||
let binding_number = ¶ms.random_scalar();
|
||||
let private_attributes = vec![serial_number, binding_number];
|
||||
|
||||
let r = params.random_scalar();
|
||||
let kappa = compute_kappa(¶ms, &keypair.verification_key(), &private_attributes, r);
|
||||
let kappa = compute_kappa(¶ms, keypair.verification_key(), &private_attributes, r);
|
||||
let zeta = compute_zeta(¶ms, serial_number);
|
||||
|
||||
// 0 public 2 private
|
||||
let pi_v = ProofKappaZeta::construct(
|
||||
¶ms,
|
||||
&keypair.verification_key(),
|
||||
&serial_number,
|
||||
&binding_number,
|
||||
keypair.verification_key(),
|
||||
serial_number,
|
||||
binding_number,
|
||||
&r,
|
||||
&kappa,
|
||||
&zeta,
|
||||
@@ -592,9 +604,9 @@ mod tests {
|
||||
|
||||
let pi_v = ProofKappaZeta::construct(
|
||||
¶ms,
|
||||
&keypair.verification_key(),
|
||||
&serial_number,
|
||||
&binding_number,
|
||||
keypair.verification_key(),
|
||||
serial_number,
|
||||
binding_number,
|
||||
&r,
|
||||
&kappa,
|
||||
&zeta,
|
||||
|
||||
@@ -83,7 +83,7 @@ pub fn aggregate_verification_keys(
|
||||
pub fn aggregate_signatures(
|
||||
params: &Parameters,
|
||||
verification_key: &VerificationKey,
|
||||
attributes: &[Attribute],
|
||||
attributes: &[&Attribute],
|
||||
signatures: &[PartialSignature],
|
||||
indices: Option<&[SignerIndex]>,
|
||||
) -> Result<Signature> {
|
||||
@@ -100,7 +100,7 @@ pub fn aggregate_signatures(
|
||||
let tmp = attributes
|
||||
.iter()
|
||||
.zip(verification_key.beta_g2.iter())
|
||||
.map(|(attr, beta_i)| beta_i * attr)
|
||||
.map(|(&attr, beta_i)| beta_i * attr)
|
||||
.sum::<G2Projective>();
|
||||
|
||||
if !check_bilinear_pairing(
|
||||
@@ -119,7 +119,7 @@ pub fn aggregate_signatures(
|
||||
pub fn aggregate_signature_shares(
|
||||
params: &Parameters,
|
||||
verification_key: &VerificationKey,
|
||||
attributes: &[Attribute],
|
||||
attributes: &[&Attribute],
|
||||
shares: &[SignatureShare],
|
||||
) -> Result<Signature> {
|
||||
let (signatures, indices): (Vec<_>, Vec<_>) = shares
|
||||
@@ -138,13 +138,13 @@ pub fn aggregate_signature_shares(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use bls12_381::G1Projective;
|
||||
use group::Group;
|
||||
|
||||
use crate::scheme::issuance::sign;
|
||||
use crate::scheme::keygen::ttp_keygen;
|
||||
use crate::scheme::setup::Parameters;
|
||||
use crate::scheme::verification::verify;
|
||||
use crate::tests::helpers::random_scalars_refs;
|
||||
use bls12_381::G1Projective;
|
||||
use group::Group;
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -155,7 +155,7 @@ mod tests {
|
||||
|
||||
let vks = keypairs
|
||||
.into_iter()
|
||||
.map(|keypair| keypair.verification_key())
|
||||
.map(|keypair| keypair.verification_key().clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let aggr_vk1 = aggregate_verification_keys(&vks[..3], Some(&[1, 2, 3])).unwrap();
|
||||
@@ -212,13 +212,18 @@ mod tests {
|
||||
#[test]
|
||||
fn signature_aggregation_works_for_any_subset_of_signatures() {
|
||||
let mut params = Parameters::new(2).unwrap();
|
||||
let attributes = params.n_random_scalars(2);
|
||||
random_scalars_refs!(attributes, params, 2);
|
||||
|
||||
let keypairs = ttp_keygen(¶ms, 3, 5).unwrap();
|
||||
|
||||
let (sks, vks): (Vec<_>, Vec<_>) = keypairs
|
||||
.into_iter()
|
||||
.map(|keypair| (keypair.secret_key(), keypair.verification_key()))
|
||||
.map(|keypair| {
|
||||
(
|
||||
keypair.secret_key().clone(),
|
||||
keypair.verification_key().clone(),
|
||||
)
|
||||
})
|
||||
.unzip();
|
||||
|
||||
let sigs = sks
|
||||
@@ -312,12 +317,17 @@ mod tests {
|
||||
fn signature_aggregation_doesnt_work_for_empty_set_of_signatures() {
|
||||
let signatures: Vec<Signature> = vec![];
|
||||
let params = Parameters::new(2).unwrap();
|
||||
let attributes = params.n_random_scalars(2);
|
||||
random_scalars_refs!(attributes, params, 2);
|
||||
let keypairs = ttp_keygen(¶ms, 3, 5).unwrap();
|
||||
|
||||
let (_, vks): (Vec<_>, Vec<_>) = keypairs
|
||||
.into_iter()
|
||||
.map(|keypair| (keypair.secret_key(), keypair.verification_key()))
|
||||
.map(|keypair| {
|
||||
(
|
||||
keypair.secret_key().clone(),
|
||||
keypair.verification_key().clone(),
|
||||
)
|
||||
})
|
||||
.unzip();
|
||||
|
||||
let aggr_vk_all = aggregate_verification_keys(&vks, None).unwrap();
|
||||
@@ -330,11 +340,16 @@ mod tests {
|
||||
fn signature_aggregation_doesnt_work_if_indices_have_invalid_length() {
|
||||
let signatures = vec![random_signature()];
|
||||
let params = Parameters::new(2).unwrap();
|
||||
let attributes = params.n_random_scalars(2);
|
||||
random_scalars_refs!(attributes, params, 2);
|
||||
let keypairs = ttp_keygen(¶ms, 3, 5).unwrap();
|
||||
let (_, vks): (Vec<_>, Vec<_>) = keypairs
|
||||
.into_iter()
|
||||
.map(|keypair| (keypair.secret_key(), keypair.verification_key()))
|
||||
.map(|keypair| {
|
||||
(
|
||||
keypair.secret_key().clone(),
|
||||
keypair.verification_key().clone(),
|
||||
)
|
||||
})
|
||||
.unzip();
|
||||
let aggr_vk_all = aggregate_verification_keys(&vks, None).unwrap();
|
||||
|
||||
@@ -356,11 +371,16 @@ mod tests {
|
||||
fn signature_aggregation_doesnt_work_for_non_unique_indices() {
|
||||
let signatures = vec![random_signature(), random_signature()];
|
||||
let params = Parameters::new(2).unwrap();
|
||||
let attributes = params.n_random_scalars(2);
|
||||
random_scalars_refs!(attributes, params, 2);
|
||||
let keypairs = ttp_keygen(¶ms, 3, 5).unwrap();
|
||||
let (_, vks): (Vec<_>, Vec<_>) = keypairs
|
||||
.into_iter()
|
||||
.map(|keypair| (keypair.secret_key(), keypair.verification_key()))
|
||||
.map(|keypair| {
|
||||
(
|
||||
keypair.secret_key().clone(),
|
||||
keypair.verification_key().clone(),
|
||||
)
|
||||
})
|
||||
.unzip();
|
||||
let aggr_vk_all = aggregate_verification_keys(&vks, None).unwrap();
|
||||
|
||||
|
||||
@@ -25,6 +25,8 @@ impl TryFrom<&[u8]> for BlindedSerialNumber {
|
||||
));
|
||||
}
|
||||
|
||||
// safety: we've just made a check for 96 bytes
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let inner = try_deserialize_g2_projective(
|
||||
&bytes.try_into().unwrap(),
|
||||
CoconutError::Deserialization(
|
||||
|
||||
@@ -54,6 +54,8 @@ impl TryFrom<&[u8]> for BlindSignRequest {
|
||||
let commitment_bytes_len = 48;
|
||||
let commitment_hash_bytes_len = 48;
|
||||
|
||||
// safety: we made bound check and we're using constant offest
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let cm_bytes = bytes[..j + commitment_bytes_len].try_into().unwrap();
|
||||
let commitment = try_deserialize_g1_projective(
|
||||
&cm_bytes,
|
||||
@@ -63,6 +65,8 @@ impl TryFrom<&[u8]> for BlindSignRequest {
|
||||
)?;
|
||||
j += commitment_bytes_len;
|
||||
|
||||
// safety: we made bound check and we're using constant offest
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let cm_hash_bytes = bytes[j..j + commitment_hash_bytes_len].try_into().unwrap();
|
||||
let commitment_hash = try_deserialize_g1_projective(
|
||||
&cm_hash_bytes,
|
||||
@@ -72,6 +76,8 @@ impl TryFrom<&[u8]> for BlindSignRequest {
|
||||
)?;
|
||||
j += commitment_hash_bytes_len;
|
||||
|
||||
// safety: we made bound check and we're using constant offest
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let c_len = u64::from_le_bytes(bytes[j..j + 8].try_into().unwrap());
|
||||
j += 8;
|
||||
if bytes[j..].len() < c_len as usize * 48 {
|
||||
@@ -86,6 +92,14 @@ impl TryFrom<&[u8]> for BlindSignRequest {
|
||||
let start = j + i * 48;
|
||||
let end = start + 48;
|
||||
|
||||
if bytes.len() < end {
|
||||
return Err(CoconutError::Deserialization(
|
||||
"Failed to deserialize compressed commitment".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// safety: we made bound check and we're using constant offest
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let private_attributes_commitment_bytes = bytes[start..end].try_into().unwrap();
|
||||
let private_attributes_commitment = try_deserialize_g1_projective(
|
||||
&private_attributes_commitment_bytes,
|
||||
@@ -137,7 +151,7 @@ impl Bytable for BlindSignRequest {
|
||||
impl Base58 for BlindSignRequest {}
|
||||
|
||||
impl BlindSignRequest {
|
||||
fn verify_proof(&self, params: &Parameters, public_attributes: &[Attribute]) -> bool {
|
||||
fn verify_proof(&self, params: &Parameters, public_attributes: &[&Attribute]) -> bool {
|
||||
self.pi_s.verify(
|
||||
params,
|
||||
&self.commitment,
|
||||
@@ -150,8 +164,8 @@ impl BlindSignRequest {
|
||||
self.commitment_hash
|
||||
}
|
||||
|
||||
pub fn get_private_attributes_pedersen_commitments(&self) -> Vec<G1Projective> {
|
||||
self.private_attributes_commitments.clone()
|
||||
pub fn get_private_attributes_pedersen_commitments(&self) -> &[G1Projective] {
|
||||
&self.private_attributes_commitments
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
@@ -161,12 +175,16 @@ impl BlindSignRequest {
|
||||
pub fn from_bytes(bytes: &[u8]) -> Result<BlindSignRequest> {
|
||||
BlindSignRequest::try_from(bytes)
|
||||
}
|
||||
|
||||
pub fn num_private_attributes(&self) -> usize {
|
||||
self.private_attributes_commitments.len()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn compute_attributes_commitment(
|
||||
params: &Parameters,
|
||||
private_attributes: &[Attribute],
|
||||
public_attributes: &[Attribute],
|
||||
private_attributes: &[&Attribute],
|
||||
public_attributes: &[&Attribute],
|
||||
hs: &[G1Affine],
|
||||
) -> (Scalar, G1Projective) {
|
||||
let commitment_opening = params.random_scalar();
|
||||
@@ -187,7 +205,7 @@ pub fn compute_attributes_commitment(
|
||||
|
||||
pub fn compute_pedersen_commitments_for_private_attributes(
|
||||
params: &Parameters,
|
||||
private_attributes: &[Attribute],
|
||||
private_attributes: &[&Attribute],
|
||||
h: &G1Projective,
|
||||
) -> (Vec<Scalar>, Vec<G1Projective>) {
|
||||
// Generate openings for Pedersen commitment for each private attribute
|
||||
@@ -197,13 +215,13 @@ pub fn compute_pedersen_commitments_for_private_attributes(
|
||||
let pedersen_commitments = commitments_openings
|
||||
.iter()
|
||||
.zip(private_attributes.iter())
|
||||
.map(|(o_j, m_j)| params.gen1() * o_j + h * m_j)
|
||||
.map(|(o_j, &m_j)| params.gen1() * o_j + h * m_j)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
(commitments_openings, pedersen_commitments)
|
||||
}
|
||||
|
||||
pub fn compute_hash(commitment: G1Projective, public_attributes: &[Attribute]) -> G1Projective {
|
||||
pub fn compute_hash(commitment: G1Projective, public_attributes: &[&Attribute]) -> G1Projective {
|
||||
let mut buff = Vec::new();
|
||||
buff.extend_from_slice(commitment.to_bytes().as_ref());
|
||||
for attr in public_attributes {
|
||||
@@ -215,8 +233,8 @@ pub fn compute_hash(commitment: G1Projective, public_attributes: &[Attribute]) -
|
||||
/// Builds cryptographic material required for blind sign.
|
||||
pub fn prepare_blind_sign(
|
||||
params: &Parameters,
|
||||
private_attributes: &[Attribute],
|
||||
public_attributes: &[Attribute],
|
||||
private_attributes: &[&Attribute],
|
||||
public_attributes: &[&Attribute],
|
||||
) -> Result<(Vec<Scalar>, BlindSignRequest)> {
|
||||
if private_attributes.is_empty() {
|
||||
return Err(CoconutError::Issuance(
|
||||
@@ -271,7 +289,7 @@ pub fn blind_sign(
|
||||
params: &Parameters,
|
||||
signing_secret_key: &SecretKey,
|
||||
blind_sign_request: &BlindSignRequest,
|
||||
public_attributes: &[Attribute],
|
||||
public_attributes: &[&Attribute],
|
||||
) -> Result<BlindedSignature> {
|
||||
let num_private = blind_sign_request.private_attributes_commitments.len();
|
||||
let hs = params.gen_hs();
|
||||
@@ -304,7 +322,7 @@ pub fn blind_sign(
|
||||
let signed_public = h * public_attributes
|
||||
.iter()
|
||||
.zip(signing_secret_key.ys.iter().skip(num_private))
|
||||
.map(|(attr, yi)| attr * yi)
|
||||
.map(|(&attr, yi)| attr * yi)
|
||||
.sum::<Scalar>();
|
||||
|
||||
// h ^ x + c[0] ^ y[0] + ... c[m] ^ y[m] + h ^ (pub_m[0] * y[m + 1] + ... + pub_m[n] * y[m + n])
|
||||
@@ -345,7 +363,7 @@ pub fn blind_sign(
|
||||
pub fn verify_partial_blind_signature(
|
||||
params: &Parameters,
|
||||
blind_sign_request: &BlindSignRequest,
|
||||
public_attributes: &[Attribute],
|
||||
public_attributes: &[&Attribute],
|
||||
blind_sig: &BlindedSignature,
|
||||
partial_verification_key: &VerificationKey,
|
||||
) -> bool {
|
||||
@@ -383,7 +401,7 @@ pub fn verify_partial_blind_signature(
|
||||
}
|
||||
|
||||
// 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(
|
||||
for (&pub_attr, beta_g2) in public_attributes.iter().zip(
|
||||
partial_verification_key
|
||||
.beta_g2
|
||||
.iter()
|
||||
@@ -415,7 +433,7 @@ pub fn verify_partial_blind_signature(
|
||||
pub fn sign(
|
||||
params: &mut Parameters,
|
||||
secret_key: &SecretKey,
|
||||
public_attributes: &[Attribute],
|
||||
public_attributes: &[&Attribute],
|
||||
) -> Result<Signature> {
|
||||
if public_attributes.len() > secret_key.ys.len() {
|
||||
return Err(CoconutError::IssuanceMaxAttributes {
|
||||
@@ -429,7 +447,7 @@ pub fn sign(
|
||||
// (the python implementation hashes string representation of all attributes onto the curve,
|
||||
// but I think the same can be achieved by just summing the attributes thus avoiding the unnecessary
|
||||
// transformation. If I'm wrong, please correct me.)
|
||||
let attributes_sum = public_attributes.iter().sum::<Scalar>();
|
||||
let attributes_sum = public_attributes.iter().copied().sum::<Scalar>();
|
||||
let h = hash_g1((params.gen1() * attributes_sum).to_bytes());
|
||||
|
||||
// x + m0 * y0 + m1 * y1 + ... mn * yn
|
||||
@@ -437,7 +455,7 @@ pub fn sign(
|
||||
+ public_attributes
|
||||
.iter()
|
||||
.zip(secret_key.ys.iter())
|
||||
.map(|(m_i, y_i)| m_i * y_i)
|
||||
.map(|(&m_i, y_i)| m_i * y_i)
|
||||
.sum::<Scalar>();
|
||||
|
||||
let sig2 = h * exponent;
|
||||
@@ -448,13 +466,14 @@ pub fn sign(
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::scheme::keygen::keygen;
|
||||
use crate::tests::helpers::random_scalars_refs;
|
||||
|
||||
#[test]
|
||||
fn blind_sign_request_bytes_roundtrip() {
|
||||
// 0 public and 1 private attribute
|
||||
let params = Parameters::new(1).unwrap();
|
||||
let private_attributes = params.n_random_scalars(1);
|
||||
let public_attributes = params.n_random_scalars(0);
|
||||
random_scalars_refs!(private_attributes, params, 1);
|
||||
random_scalars_refs!(public_attributes, params, 0);
|
||||
|
||||
let (_commitments_openings, lambda) =
|
||||
prepare_blind_sign(¶ms, &private_attributes, &public_attributes).unwrap();
|
||||
@@ -467,8 +486,8 @@ mod tests {
|
||||
|
||||
// 2 public and 2 private attributes
|
||||
let params = Parameters::new(4).unwrap();
|
||||
let private_attributes = params.n_random_scalars(2);
|
||||
let public_attributes = params.n_random_scalars(2);
|
||||
random_scalars_refs!(private_attributes, params, 2);
|
||||
random_scalars_refs!(public_attributes, params, 2);
|
||||
|
||||
let (_commitments_openings, lambda) =
|
||||
prepare_blind_sign(¶ms, &private_attributes, &public_attributes).unwrap();
|
||||
@@ -483,8 +502,8 @@ mod tests {
|
||||
#[test]
|
||||
fn successful_verify_partial_blind_signature() {
|
||||
let params = Parameters::new(4).unwrap();
|
||||
let private_attributes = params.n_random_scalars(2);
|
||||
let public_attributes = params.n_random_scalars(2);
|
||||
random_scalars_refs!(private_attributes, params, 2);
|
||||
random_scalars_refs!(public_attributes, params, 2);
|
||||
|
||||
let (_commitments_openings, request) =
|
||||
prepare_blind_sign(¶ms, &private_attributes, &public_attributes).unwrap();
|
||||
@@ -492,7 +511,7 @@ mod tests {
|
||||
let validator_keypair = keygen(¶ms);
|
||||
let blind_sig = blind_sign(
|
||||
¶ms,
|
||||
&validator_keypair.secret_key(),
|
||||
validator_keypair.secret_key(),
|
||||
&request,
|
||||
&public_attributes,
|
||||
)
|
||||
@@ -503,36 +522,35 @@ mod tests {
|
||||
&request,
|
||||
&public_attributes,
|
||||
&blind_sig,
|
||||
&validator_keypair.verification_key()
|
||||
validator_keypair.verification_key()
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn successful_verify_partial_blind_signature_no_public_attributes() {
|
||||
let params = Parameters::new(4).unwrap();
|
||||
let private_attributes = params.n_random_scalars(2);
|
||||
random_scalars_refs!(private_attributes, params, 2);
|
||||
|
||||
let (_commitments_openings, request) =
|
||||
prepare_blind_sign(¶ms, &private_attributes, &[]).unwrap();
|
||||
|
||||
let validator_keypair = keygen(¶ms);
|
||||
let blind_sig =
|
||||
blind_sign(¶ms, &validator_keypair.secret_key(), &request, &[]).unwrap();
|
||||
let blind_sig = blind_sign(¶ms, validator_keypair.secret_key(), &request, &[]).unwrap();
|
||||
|
||||
assert!(verify_partial_blind_signature(
|
||||
¶ms,
|
||||
&request,
|
||||
&[],
|
||||
&blind_sig,
|
||||
&validator_keypair.verification_key()
|
||||
validator_keypair.verification_key()
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fail_verify_partial_blind_signature_with_wrong_key() {
|
||||
let params = Parameters::new(4).unwrap();
|
||||
let private_attributes = params.n_random_scalars(2);
|
||||
let public_attributes = params.n_random_scalars(2);
|
||||
random_scalars_refs!(private_attributes, params, 2);
|
||||
random_scalars_refs!(public_attributes, params, 2);
|
||||
|
||||
let (_commitments_openings, request) =
|
||||
prepare_blind_sign(¶ms, &private_attributes, &public_attributes).unwrap();
|
||||
@@ -541,7 +559,7 @@ mod tests {
|
||||
let validator2_keypair = keygen(¶ms);
|
||||
let blind_sig = blind_sign(
|
||||
¶ms,
|
||||
&validator_keypair.secret_key(),
|
||||
validator_keypair.secret_key(),
|
||||
&request,
|
||||
&public_attributes,
|
||||
)
|
||||
@@ -553,7 +571,7 @@ mod tests {
|
||||
&request,
|
||||
&public_attributes,
|
||||
&blind_sig,
|
||||
&validator2_keypair.verification_key()
|
||||
validator2_keypair.verification_key()
|
||||
),);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,8 +23,12 @@ use crate::utils::{
|
||||
};
|
||||
use crate::Base58;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(test, derive(PartialEq, Eq))]
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(test, derive(PartialEq, Eq, Clone))]
|
||||
#[cfg_attr(
|
||||
feature = "key-zeroize",
|
||||
derive(zeroize::Zeroize, zeroize::ZeroizeOnDrop)
|
||||
)]
|
||||
pub struct SecretKey {
|
||||
pub(crate) x: Scalar,
|
||||
pub(crate) ys: Vec<Scalar>,
|
||||
@@ -62,7 +66,9 @@ impl TryFrom<&[u8]> for SecretKey {
|
||||
}
|
||||
|
||||
// this conversion will not fail as we are taking the same length of data
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let x_bytes: [u8; 32] = bytes[..32].try_into().unwrap();
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let ys_len = u64::from_le_bytes(bytes[32..40].try_into().unwrap());
|
||||
let actual_ys_len = (bytes.len() - 40) / 32;
|
||||
|
||||
@@ -97,6 +103,10 @@ impl SecretKey {
|
||||
(self.x, self.ys.clone())
|
||||
}
|
||||
|
||||
pub fn size(&self) -> usize {
|
||||
self.ys.len()
|
||||
}
|
||||
|
||||
/// Derive verification key using this secret key.
|
||||
pub fn verification_key(&self, params: &Parameters) -> VerificationKey {
|
||||
let g1 = params.gen1();
|
||||
@@ -141,6 +151,10 @@ impl Base58 for SecretKey {}
|
||||
// TODO: perhaps change points to affine representation
|
||||
// to make verification slightly more efficient?
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
#[cfg_attr(
|
||||
feature = "key-zeroize",
|
||||
derive(zeroize::Zeroize, zeroize::ZeroizeOnDrop)
|
||||
)]
|
||||
pub struct VerificationKey {
|
||||
// TODO add gen2 as per the paper or imply it from the fact library is using bls381?
|
||||
pub(crate) alpha: G2Projective,
|
||||
@@ -180,7 +194,9 @@ impl TryFrom<&[u8]> for VerificationKey {
|
||||
}
|
||||
|
||||
// this conversion will not fail as we are taking the same length of data
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let alpha_bytes: [u8; 96] = bytes[..96].try_into().unwrap();
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let betas_len = u64::from_le_bytes(bytes[96..104].try_into().unwrap());
|
||||
|
||||
let actual_betas_len = (bytes.len() - 104) / (96 + 48);
|
||||
@@ -204,6 +220,8 @@ impl TryFrom<&[u8]> for VerificationKey {
|
||||
for i in 0..betas_len {
|
||||
let start = (104 + i * 48) as usize;
|
||||
let end = start + 48;
|
||||
// we're using a constant 48 byte offset (which is the size of G1 compressed) so unwrap is fine
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let beta_i_bytes = bytes[start..end].try_into().unwrap();
|
||||
let beta_i = try_deserialize_g1_projective(
|
||||
&beta_i_bytes,
|
||||
@@ -220,6 +238,8 @@ impl TryFrom<&[u8]> for VerificationKey {
|
||||
for i in 0..betas_len {
|
||||
let start = (beta_g1_end + i * 96) as usize;
|
||||
let end = start + 96;
|
||||
// we're using a constant 96 byte offset (which is the size of G2 compressed) so unwrap is fine
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let beta_i_bytes = bytes[start..end].try_into().unwrap();
|
||||
let beta_i = try_deserialize_g2_projective(
|
||||
&beta_i_bytes,
|
||||
@@ -392,7 +412,11 @@ impl Bytable for VerificationKey {
|
||||
impl Base58 for VerificationKey {}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(test, derive(PartialEq, Eq))]
|
||||
#[cfg_attr(test, derive(PartialEq, Eq, Clone))]
|
||||
#[cfg_attr(
|
||||
feature = "key-zeroize",
|
||||
derive(zeroize::Zeroize, zeroize::ZeroizeOnDrop)
|
||||
)]
|
||||
pub struct KeyPair {
|
||||
secret_key: SecretKey,
|
||||
verification_key: VerificationKey,
|
||||
@@ -429,12 +453,12 @@ impl KeyPair {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn secret_key(&self) -> SecretKey {
|
||||
self.secret_key.clone()
|
||||
pub fn secret_key(&self) -> &SecretKey {
|
||||
&self.secret_key
|
||||
}
|
||||
|
||||
pub fn verification_key(&self) -> VerificationKey {
|
||||
self.verification_key.clone()
|
||||
pub fn verification_key(&self) -> &VerificationKey {
|
||||
&self.verification_key
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
@@ -488,6 +512,8 @@ impl TryFrom<&[u8]> for KeyPair {
|
||||
});
|
||||
}
|
||||
|
||||
// safety: we made bound check and we're using constant offest
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let secret_key_len =
|
||||
u64::from_le_bytes(bytes[header_len..header_len + 8].try_into().unwrap()) as usize;
|
||||
let secret_key_start = header_len + 8;
|
||||
@@ -503,6 +529,8 @@ impl TryFrom<&[u8]> for KeyPair {
|
||||
});
|
||||
}
|
||||
|
||||
// safety: we made bound check
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let verification_key_len = u64::from_le_bytes(
|
||||
bytes[secret_key_start + secret_key_len..secret_key_start + secret_key_len + 8]
|
||||
.try_into()
|
||||
@@ -515,6 +543,7 @@ impl TryFrom<&[u8]> for KeyPair {
|
||||
)?;
|
||||
let consumed_bytes = verification_key_start + verification_key_len;
|
||||
let index = if consumed_bytes < bytes.len() && [consumed_bytes..].len() == 8 {
|
||||
#[allow(clippy::unwrap_used)]
|
||||
Some(u64::from_le_bytes(
|
||||
bytes[consumed_bytes..consumed_bytes + 8]
|
||||
.try_into()
|
||||
|
||||
@@ -44,7 +44,10 @@ impl TryFrom<&[u8]> for Signature {
|
||||
)));
|
||||
}
|
||||
|
||||
// safety: we just checked for the length so the unwraps are fine
|
||||
#[allow(clippy::expect_used)]
|
||||
let sig1_bytes: &[u8; 48] = &bytes[..48].try_into().expect("Slice size != 48");
|
||||
#[allow(clippy::expect_used)]
|
||||
let sig2_bytes: &[u8; 48] = &bytes[48..].try_into().expect("Slice size != 48");
|
||||
|
||||
let sig1 = try_deserialize_g1_projective(
|
||||
@@ -88,6 +91,45 @@ impl Signature {
|
||||
pub fn from_bytes(bytes: &[u8]) -> Result<Signature> {
|
||||
Signature::try_from(bytes)
|
||||
}
|
||||
|
||||
pub fn verify(
|
||||
&self,
|
||||
params: &Parameters,
|
||||
partial_verification_key: &VerificationKey,
|
||||
private_attributes: &[&Attribute],
|
||||
public_attributes: &[&Attribute],
|
||||
commitment_hash: &G1Projective,
|
||||
) -> Result<()> {
|
||||
// Verify the commitment hash
|
||||
if !(commitment_hash == &self.0) {
|
||||
return Err(CoconutError::Verification(
|
||||
"Verification of commitment hash from signature failed".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let alpha = partial_verification_key.alpha;
|
||||
|
||||
let signed_attributes = private_attributes
|
||||
.iter()
|
||||
.chain(public_attributes.iter())
|
||||
.zip(partial_verification_key.beta_g2.iter())
|
||||
.map(|(&attr, beta_i)| beta_i * attr)
|
||||
.sum::<G2Projective>();
|
||||
|
||||
// Verify the signature share
|
||||
if !check_bilinear_pairing(
|
||||
&self.0.to_affine(),
|
||||
&G2Prepared::from((alpha + signed_attributes).to_affine()),
|
||||
&self.1.to_affine(),
|
||||
params.prepared_miller_g2(),
|
||||
) {
|
||||
return Err(CoconutError::Unblind(
|
||||
"Verification of signature share failed".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Bytable for Signature {
|
||||
@@ -102,8 +144,7 @@ impl Bytable for Signature {
|
||||
|
||||
impl Base58 for Signature {}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(test, derive(PartialEq, Eq))]
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct BlindedSignature(G1Projective, G1Projective);
|
||||
|
||||
impl Bytable for BlindedSignature {
|
||||
@@ -129,7 +170,10 @@ impl TryFrom<&[u8]> for BlindedSignature {
|
||||
)));
|
||||
}
|
||||
|
||||
// safety: we just checked for the length so the unwraps are fine
|
||||
#[allow(clippy::expect_used)]
|
||||
let h_bytes: &[u8; 48] = &bytes[..48].try_into().expect("Slice size != 48");
|
||||
#[allow(clippy::expect_used)]
|
||||
let sig_bytes: &[u8; 48] = &bytes[48..].try_into().expect("Slice size != 48");
|
||||
|
||||
let h = try_deserialize_g1_projective(
|
||||
@@ -148,24 +192,12 @@ impl TryFrom<&[u8]> for BlindedSignature {
|
||||
impl BlindedSignature {
|
||||
pub fn unblind(
|
||||
&self,
|
||||
params: &Parameters,
|
||||
partial_verification_key: &VerificationKey,
|
||||
private_attributes: &[Attribute],
|
||||
public_attributes: &[Attribute],
|
||||
commitment_hash: &G1Projective,
|
||||
pedersen_commitments_openings: &[Scalar],
|
||||
) -> Result<Signature> {
|
||||
// parse the signature
|
||||
let h = &self.0;
|
||||
let c = &self.1;
|
||||
|
||||
// Verify the commitment hash
|
||||
if !(commitment_hash == h) {
|
||||
return Err(CoconutError::Unblind(
|
||||
"Verification of commitment hash from signature failed".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let blinding_removers = partial_verification_key
|
||||
.beta_g1
|
||||
.iter()
|
||||
@@ -175,30 +207,29 @@ impl BlindedSignature {
|
||||
|
||||
let unblinded_c = c - blinding_removers;
|
||||
|
||||
let alpha = partial_verification_key.alpha;
|
||||
|
||||
let signed_attributes = private_attributes
|
||||
.iter()
|
||||
.chain(public_attributes.iter())
|
||||
.zip(partial_verification_key.beta_g2.iter())
|
||||
.map(|(attr, beta_i)| beta_i * attr)
|
||||
.sum::<G2Projective>();
|
||||
|
||||
// Verify the signature share
|
||||
if !check_bilinear_pairing(
|
||||
&h.to_affine(),
|
||||
&G2Prepared::from((alpha + signed_attributes).to_affine()),
|
||||
&unblinded_c.to_affine(),
|
||||
params.prepared_miller_g2(),
|
||||
) {
|
||||
return Err(CoconutError::Unblind(
|
||||
"Verification of signature share failed".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(Signature(*h, unblinded_c))
|
||||
}
|
||||
|
||||
pub fn unblind_and_verify(
|
||||
&self,
|
||||
params: &Parameters,
|
||||
partial_verification_key: &VerificationKey,
|
||||
private_attributes: &[&Attribute],
|
||||
public_attributes: &[&Attribute],
|
||||
commitment_hash: &G1Projective,
|
||||
pedersen_commitments_openings: &[Scalar],
|
||||
) -> Result<Signature> {
|
||||
let unblinded = self.unblind(partial_verification_key, pedersen_commitments_openings)?;
|
||||
unblinded.verify(
|
||||
params,
|
||||
partial_verification_key,
|
||||
private_attributes,
|
||||
public_attributes,
|
||||
commitment_hash,
|
||||
)?;
|
||||
Ok(unblinded)
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> [u8; 96] {
|
||||
let mut bytes = [0u8; 96];
|
||||
bytes[..48].copy_from_slice(&self.0.to_affine().to_compressed());
|
||||
@@ -237,25 +268,25 @@ impl SignatureShare {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::hash_to_scalar;
|
||||
use crate::scheme::aggregation::{aggregate_signatures, aggregate_verification_keys};
|
||||
use crate::scheme::issuance::{blind_sign, compute_hash, prepare_blind_sign, sign};
|
||||
use crate::scheme::keygen::{keygen, ttp_keygen};
|
||||
use crate::scheme::verification::{prove_bandwidth_credential, verify, verify_credential};
|
||||
|
||||
use super::*;
|
||||
use crate::tests::helpers::random_scalars_refs;
|
||||
|
||||
#[test]
|
||||
fn unblind_returns_error_if_integrity_check_on_commitment_hash_fails() {
|
||||
let params = Parameters::new(2).unwrap();
|
||||
let private_attributes = params.n_random_scalars(2_usize);
|
||||
random_scalars_refs!(private_attributes, params, 2);
|
||||
|
||||
let (_commitments_openings, lambda) =
|
||||
prepare_blind_sign(¶ms, &private_attributes, &[]).unwrap();
|
||||
|
||||
let keypair1 = keygen(¶ms);
|
||||
|
||||
let sig1 = blind_sign(¶ms, &keypair1.secret_key(), &lambda, &[]).unwrap();
|
||||
let sig1 = blind_sign(¶ms, keypair1.secret_key(), &lambda, &[]).unwrap();
|
||||
|
||||
let wrong_commitment_opening = params.random_scalar();
|
||||
let wrong_commitment = params.gen1() * wrong_commitment_opening;
|
||||
@@ -263,9 +294,9 @@ mod tests {
|
||||
let wrong_commitments_openings = params.n_random_scalars(private_attributes.len());
|
||||
|
||||
assert!(sig1
|
||||
.unblind(
|
||||
.unblind_and_verify(
|
||||
¶ms,
|
||||
&keypair1.verification_key(),
|
||||
keypair1.verification_key(),
|
||||
&private_attributes,
|
||||
&[],
|
||||
&fake_commitment_hash,
|
||||
@@ -277,20 +308,23 @@ mod tests {
|
||||
#[test]
|
||||
fn unblind_returns_error_if_signature_verification_fails() {
|
||||
let params = Parameters::new(2).unwrap();
|
||||
let private_attributes = vec![hash_to_scalar("Attribute1"), hash_to_scalar("Attribute2")];
|
||||
let private_attributes2 = vec![hash_to_scalar("Attribute3"), hash_to_scalar("Attribute4")];
|
||||
let p = [hash_to_scalar("Attribute1"), hash_to_scalar("Attribute2")];
|
||||
let private_attributes = vec![&p[0], &p[1]];
|
||||
|
||||
let p2 = [hash_to_scalar("Attribute3"), hash_to_scalar("Attribute4")];
|
||||
let private_attributes2 = vec![&p2[0], &p2[1]];
|
||||
|
||||
let (commitments_openings, lambda) =
|
||||
prepare_blind_sign(¶ms, &private_attributes, &[]).unwrap();
|
||||
|
||||
let keypair1 = keygen(¶ms);
|
||||
|
||||
let sig1 = blind_sign(¶ms, &keypair1.secret_key(), &lambda, &[]).unwrap();
|
||||
let sig1 = blind_sign(¶ms, keypair1.secret_key(), &lambda, &[]).unwrap();
|
||||
|
||||
assert!(sig1
|
||||
.unblind(
|
||||
.unblind_and_verify(
|
||||
¶ms,
|
||||
&keypair1.verification_key(),
|
||||
keypair1.verification_key(),
|
||||
&private_attributes2,
|
||||
&[],
|
||||
&lambda.get_commitment_hash(),
|
||||
@@ -304,7 +338,7 @@ mod tests {
|
||||
let params = Parameters::new(2).unwrap();
|
||||
let serial_number = params.random_scalar();
|
||||
let binding_number = params.random_scalar();
|
||||
let private_attributes = vec![serial_number, binding_number];
|
||||
let private_attributes = vec![&serial_number, &binding_number];
|
||||
|
||||
let keypair1 = keygen(¶ms);
|
||||
let keypair2 = keygen(¶ms);
|
||||
@@ -312,11 +346,11 @@ mod tests {
|
||||
let (commitments_openings, lambda) =
|
||||
prepare_blind_sign(¶ms, &private_attributes, &[]).unwrap();
|
||||
|
||||
let sig1 = blind_sign(¶ms, &keypair1.secret_key(), &lambda, &[])
|
||||
let sig1 = blind_sign(¶ms, keypair1.secret_key(), &lambda, &[])
|
||||
.unwrap()
|
||||
.unblind(
|
||||
.unblind_and_verify(
|
||||
¶ms,
|
||||
&keypair1.verification_key(),
|
||||
keypair1.verification_key(),
|
||||
&private_attributes,
|
||||
&[],
|
||||
&lambda.get_commitment_hash(),
|
||||
@@ -324,11 +358,11 @@ mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let sig2 = blind_sign(¶ms, &keypair2.secret_key(), &lambda, &[])
|
||||
let sig2 = blind_sign(¶ms, keypair2.secret_key(), &lambda, &[])
|
||||
.unwrap()
|
||||
.unblind(
|
||||
.unblind_and_verify(
|
||||
¶ms,
|
||||
&keypair2.verification_key(),
|
||||
keypair2.verification_key(),
|
||||
&private_attributes,
|
||||
&[],
|
||||
&lambda.get_commitment_hash(),
|
||||
@@ -338,39 +372,39 @@ mod tests {
|
||||
|
||||
let theta1 = prove_bandwidth_credential(
|
||||
¶ms,
|
||||
&keypair1.verification_key(),
|
||||
keypair1.verification_key(),
|
||||
&sig1,
|
||||
serial_number,
|
||||
binding_number,
|
||||
&serial_number,
|
||||
&binding_number,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let theta2 = prove_bandwidth_credential(
|
||||
¶ms,
|
||||
&keypair2.verification_key(),
|
||||
keypair2.verification_key(),
|
||||
&sig2,
|
||||
serial_number,
|
||||
binding_number,
|
||||
&serial_number,
|
||||
&binding_number,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(verify_credential(
|
||||
¶ms,
|
||||
&keypair1.verification_key(),
|
||||
keypair1.verification_key(),
|
||||
&theta1,
|
||||
&[],
|
||||
));
|
||||
|
||||
assert!(verify_credential(
|
||||
¶ms,
|
||||
&keypair2.verification_key(),
|
||||
keypair2.verification_key(),
|
||||
&theta2,
|
||||
&[],
|
||||
));
|
||||
|
||||
assert!(!verify_credential(
|
||||
¶ms,
|
||||
&keypair1.verification_key(),
|
||||
keypair1.verification_key(),
|
||||
&theta2,
|
||||
&[],
|
||||
));
|
||||
@@ -379,30 +413,30 @@ mod tests {
|
||||
#[test]
|
||||
fn verification_on_two_public_attributes() {
|
||||
let mut params = Parameters::new(2).unwrap();
|
||||
let attributes = params.n_random_scalars(2);
|
||||
random_scalars_refs!(attributes, params, 2);
|
||||
|
||||
let keypair1 = keygen(¶ms);
|
||||
let keypair2 = keygen(¶ms);
|
||||
let sig1 = sign(&mut params, &keypair1.secret_key(), &attributes).unwrap();
|
||||
let sig2 = sign(&mut params, &keypair2.secret_key(), &attributes).unwrap();
|
||||
let sig1 = sign(&mut params, keypair1.secret_key(), &attributes).unwrap();
|
||||
let sig2 = sign(&mut params, keypair2.secret_key(), &attributes).unwrap();
|
||||
|
||||
assert!(verify(
|
||||
¶ms,
|
||||
&keypair1.verification_key(),
|
||||
keypair1.verification_key(),
|
||||
&attributes,
|
||||
&sig1,
|
||||
));
|
||||
|
||||
assert!(!verify(
|
||||
¶ms,
|
||||
&keypair2.verification_key(),
|
||||
keypair2.verification_key(),
|
||||
&attributes,
|
||||
&sig1,
|
||||
));
|
||||
|
||||
assert!(!verify(
|
||||
¶ms,
|
||||
&keypair1.verification_key(),
|
||||
keypair1.verification_key(),
|
||||
&attributes,
|
||||
&sig2,
|
||||
));
|
||||
@@ -411,10 +445,11 @@ mod tests {
|
||||
#[test]
|
||||
fn verification_on_two_public_and_two_private_attributes() {
|
||||
let params = Parameters::new(4).unwrap();
|
||||
let public_attributes = params.n_random_scalars(2);
|
||||
random_scalars_refs!(public_attributes, params, 2);
|
||||
|
||||
let serial_number = params.random_scalar();
|
||||
let binding_number = params.random_scalar();
|
||||
let private_attributes = vec![serial_number, binding_number];
|
||||
let private_attributes = vec![&serial_number, &binding_number];
|
||||
|
||||
let keypair1 = keygen(¶ms);
|
||||
let keypair2 = keygen(¶ms);
|
||||
@@ -422,11 +457,11 @@ mod tests {
|
||||
let (commitments_openings, lambda) =
|
||||
prepare_blind_sign(¶ms, &private_attributes, &public_attributes).unwrap();
|
||||
|
||||
let sig1 = blind_sign(¶ms, &keypair1.secret_key(), &lambda, &public_attributes)
|
||||
let sig1 = blind_sign(¶ms, keypair1.secret_key(), &lambda, &public_attributes)
|
||||
.unwrap()
|
||||
.unblind(
|
||||
.unblind_and_verify(
|
||||
¶ms,
|
||||
&keypair1.verification_key(),
|
||||
keypair1.verification_key(),
|
||||
&private_attributes,
|
||||
&public_attributes,
|
||||
&lambda.get_commitment_hash(),
|
||||
@@ -434,11 +469,11 @@ mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let sig2 = blind_sign(¶ms, &keypair2.secret_key(), &lambda, &public_attributes)
|
||||
let sig2 = blind_sign(¶ms, keypair2.secret_key(), &lambda, &public_attributes)
|
||||
.unwrap()
|
||||
.unblind(
|
||||
.unblind_and_verify(
|
||||
¶ms,
|
||||
&keypair2.verification_key(),
|
||||
keypair2.verification_key(),
|
||||
&private_attributes,
|
||||
&public_attributes,
|
||||
&lambda.get_commitment_hash(),
|
||||
@@ -448,39 +483,39 @@ mod tests {
|
||||
|
||||
let theta1 = prove_bandwidth_credential(
|
||||
¶ms,
|
||||
&keypair1.verification_key(),
|
||||
keypair1.verification_key(),
|
||||
&sig1,
|
||||
serial_number,
|
||||
binding_number,
|
||||
&serial_number,
|
||||
&binding_number,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let theta2 = prove_bandwidth_credential(
|
||||
¶ms,
|
||||
&keypair2.verification_key(),
|
||||
keypair2.verification_key(),
|
||||
&sig2,
|
||||
serial_number,
|
||||
binding_number,
|
||||
&serial_number,
|
||||
&binding_number,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(verify_credential(
|
||||
¶ms,
|
||||
&keypair1.verification_key(),
|
||||
keypair1.verification_key(),
|
||||
&theta1,
|
||||
&public_attributes,
|
||||
));
|
||||
|
||||
assert!(verify_credential(
|
||||
¶ms,
|
||||
&keypair2.verification_key(),
|
||||
keypair2.verification_key(),
|
||||
&theta2,
|
||||
&public_attributes,
|
||||
));
|
||||
|
||||
assert!(!verify_credential(
|
||||
¶ms,
|
||||
&keypair1.verification_key(),
|
||||
keypair1.verification_key(),
|
||||
&theta2,
|
||||
&public_attributes,
|
||||
));
|
||||
@@ -489,10 +524,11 @@ mod tests {
|
||||
#[test]
|
||||
fn verification_on_two_public_and_two_private_attributes_from_two_signers() {
|
||||
let params = Parameters::new(4).unwrap();
|
||||
let public_attributes = params.n_random_scalars(2);
|
||||
random_scalars_refs!(public_attributes, params, 2);
|
||||
|
||||
let serial_number = params.random_scalar();
|
||||
let binding_number = params.random_scalar();
|
||||
let private_attributes = vec![serial_number, binding_number];
|
||||
let private_attributes = vec![&serial_number, &binding_number];
|
||||
|
||||
let keypairs = ttp_keygen(¶ms, 2, 3).unwrap();
|
||||
|
||||
@@ -502,11 +538,11 @@ mod tests {
|
||||
let sigs = keypairs
|
||||
.iter()
|
||||
.map(|keypair| {
|
||||
blind_sign(¶ms, &keypair.secret_key(), &lambda, &public_attributes)
|
||||
blind_sign(¶ms, keypair.secret_key(), &lambda, &public_attributes)
|
||||
.unwrap()
|
||||
.unblind(
|
||||
.unblind_and_verify(
|
||||
¶ms,
|
||||
&keypair.verification_key(),
|
||||
keypair.verification_key(),
|
||||
&private_attributes,
|
||||
&public_attributes,
|
||||
&lambda.get_commitment_hash(),
|
||||
@@ -518,7 +554,7 @@ mod tests {
|
||||
|
||||
let vks = keypairs
|
||||
.into_iter()
|
||||
.map(|keypair| keypair.verification_key())
|
||||
.map(|keypair| keypair.verification_key().clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut attributes = Vec::with_capacity(private_attributes.len() + public_attributes.len());
|
||||
@@ -530,9 +566,14 @@ mod tests {
|
||||
aggregate_signatures(¶ms, &aggr_vk, &attributes, &sigs[..2], Some(&[1, 2]))
|
||||
.unwrap();
|
||||
|
||||
let theta =
|
||||
prove_bandwidth_credential(¶ms, &aggr_vk, &aggr_sig, serial_number, binding_number)
|
||||
.unwrap();
|
||||
let theta = prove_bandwidth_credential(
|
||||
¶ms,
|
||||
&aggr_vk,
|
||||
&aggr_sig,
|
||||
&serial_number,
|
||||
&binding_number,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(verify_credential(
|
||||
¶ms,
|
||||
@@ -547,9 +588,14 @@ mod tests {
|
||||
aggregate_signatures(¶ms, &aggr_vk, &attributes, &sigs[1..], Some(&[2, 3]))
|
||||
.unwrap();
|
||||
|
||||
let theta =
|
||||
prove_bandwidth_credential(¶ms, &aggr_vk, &aggr_sig, serial_number, binding_number)
|
||||
.unwrap();
|
||||
let theta = prove_bandwidth_credential(
|
||||
¶ms,
|
||||
&aggr_vk,
|
||||
&aggr_sig,
|
||||
&serial_number,
|
||||
&binding_number,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(verify_credential(
|
||||
¶ms,
|
||||
|
||||
@@ -43,6 +43,8 @@ impl TryFrom<&[u8]> for Theta {
|
||||
));
|
||||
}
|
||||
|
||||
// safety: we just checked for the length so the unwraps are fine
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let blinded_message_bytes = bytes[..96].try_into().unwrap();
|
||||
let blinded_message = try_deserialize_g2_projective(
|
||||
&blinded_message_bytes,
|
||||
@@ -51,6 +53,8 @@ impl TryFrom<&[u8]> for Theta {
|
||||
),
|
||||
)?;
|
||||
|
||||
// safety: we just checked for the length so the unwraps are fine
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let blinded_serial_number_bytes = bytes[96..192].try_into().unwrap();
|
||||
let blinded_serial_number = try_deserialize_g2_projective(
|
||||
&blinded_serial_number_bytes,
|
||||
@@ -130,7 +134,7 @@ impl Base58 for Theta {}
|
||||
pub fn compute_kappa(
|
||||
params: &Parameters,
|
||||
verification_key: &VerificationKey,
|
||||
private_attributes: &[Attribute],
|
||||
private_attributes: &[&Attribute],
|
||||
blinding_factor: Scalar,
|
||||
) -> G2Projective {
|
||||
params.gen2() * blinding_factor
|
||||
@@ -138,11 +142,11 @@ pub fn compute_kappa(
|
||||
+ private_attributes
|
||||
.iter()
|
||||
.zip(verification_key.beta_g2.iter())
|
||||
.map(|(priv_attr, beta_i)| beta_i * priv_attr)
|
||||
.map(|(&priv_attr, beta_i)| beta_i * priv_attr)
|
||||
.sum::<G2Projective>()
|
||||
}
|
||||
|
||||
pub fn compute_zeta(params: &Parameters, serial_number: Attribute) -> G2Projective {
|
||||
pub fn compute_zeta(params: &Parameters, serial_number: &Attribute) -> G2Projective {
|
||||
params.gen2() * serial_number
|
||||
}
|
||||
|
||||
@@ -150,8 +154,8 @@ pub fn prove_bandwidth_credential(
|
||||
params: &Parameters,
|
||||
verification_key: &VerificationKey,
|
||||
signature: &Signature,
|
||||
serial_number: Attribute,
|
||||
binding_number: Attribute,
|
||||
serial_number: &Attribute,
|
||||
binding_number: &Attribute,
|
||||
) -> Result<Theta> {
|
||||
if verification_key.beta_g2.len() < 2 {
|
||||
return Err(
|
||||
@@ -171,7 +175,7 @@ pub fn prove_bandwidth_credential(
|
||||
// Thus, we need kappa which allows us to verify sigma'. In particular,
|
||||
// kappa is computed on m as input, but thanks to the use or random value r,
|
||||
// it does not reveal any information about m.
|
||||
let private_attributes = vec![serial_number, binding_number];
|
||||
let private_attributes = [serial_number, binding_number];
|
||||
let blinded_message = compute_kappa(
|
||||
params,
|
||||
verification_key,
|
||||
@@ -185,8 +189,8 @@ pub fn prove_bandwidth_credential(
|
||||
let pi_v = ProofKappaZeta::construct(
|
||||
params,
|
||||
verification_key,
|
||||
&serial_number,
|
||||
&binding_number,
|
||||
serial_number,
|
||||
binding_number,
|
||||
&sign_blinding_factor,
|
||||
&blinded_message,
|
||||
&blinded_serial_number,
|
||||
@@ -221,7 +225,10 @@ pub fn check_vk_pairing(
|
||||
if values_len == 0 || values_len - 1 != vk.beta_g1.len() || values_len - 1 != vk.beta_g2.len() {
|
||||
return false;
|
||||
}
|
||||
if vk.alpha != *dkg_values.last().unwrap() {
|
||||
|
||||
// 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.last().as_ref().unwrap() {
|
||||
return false;
|
||||
}
|
||||
if dkg_values
|
||||
@@ -249,7 +256,7 @@ pub fn verify_credential(
|
||||
params: &Parameters,
|
||||
verification_key: &VerificationKey,
|
||||
theta: &Theta,
|
||||
public_attributes: &[Attribute],
|
||||
public_attributes: &[&Attribute],
|
||||
) -> bool {
|
||||
if public_attributes.len() + theta.pi_v.private_attributes_len()
|
||||
> verification_key.beta_g2.len()
|
||||
@@ -272,7 +279,7 @@ pub fn verify_credential(
|
||||
.iter()
|
||||
.skip(theta.pi_v.private_attributes_len()),
|
||||
)
|
||||
.map(|(pub_attr, beta_i)| beta_i * pub_attr)
|
||||
.map(|(&pub_attr, beta_i)| beta_i * pub_attr)
|
||||
.sum::<G2Projective>();
|
||||
|
||||
theta.blinded_message + signed_public_attributes
|
||||
@@ -291,14 +298,14 @@ pub fn verify_credential(
|
||||
pub fn verify(
|
||||
params: &Parameters,
|
||||
verification_key: &VerificationKey,
|
||||
public_attributes: &[Attribute],
|
||||
public_attributes: &[&Attribute],
|
||||
sig: &Signature,
|
||||
) -> bool {
|
||||
let kappa = (verification_key.alpha
|
||||
+ public_attributes
|
||||
.iter()
|
||||
.zip(verification_key.beta_g2.iter())
|
||||
.map(|(m_i, b_i)| b_i * m_i)
|
||||
.map(|(&m_i, b_i)| b_i * m_i)
|
||||
.sum::<G2Projective>())
|
||||
.to_affine();
|
||||
|
||||
@@ -320,10 +327,11 @@ mod tests {
|
||||
#[test]
|
||||
fn vk_pairing() {
|
||||
let params = setup(2).unwrap();
|
||||
let vk = keygen(¶ms).verification_key();
|
||||
let keypair = keygen(¶ms);
|
||||
let vk = keypair.verification_key();
|
||||
let mut dkg_values = vk.beta_g2.clone();
|
||||
dkg_values.push(vk.alpha);
|
||||
assert!(check_vk_pairing(¶ms, &dkg_values, &vk));
|
||||
assert!(check_vk_pairing(¶ms, &dkg_values, vk));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -340,10 +348,10 @@ mod tests {
|
||||
|
||||
let theta = prove_bandwidth_credential(
|
||||
¶ms,
|
||||
&keypair.verification_key(),
|
||||
keypair.verification_key(),
|
||||
&signature,
|
||||
serial_number,
|
||||
binding_number,
|
||||
&serial_number,
|
||||
&binding_number,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::random_scalars_refs;
|
||||
use crate::tests::helpers::tests::generate_dkg_keys;
|
||||
use crate::{
|
||||
aggregate_verification_keys, setup, tests::helpers::*, ttp_keygen, verify_credential,
|
||||
@@ -12,14 +13,14 @@ fn keygen() -> Result<(), CoconutError> {
|
||||
let params = setup(5)?;
|
||||
let node_indices = vec![15u64, 248, 33521];
|
||||
|
||||
let public_attributes = params.n_random_scalars(2);
|
||||
random_scalars_refs!(public_attributes, params, 2);
|
||||
|
||||
// generate_keys
|
||||
let coconut_keypairs = ttp_keygen(¶ms, 2, 3)?;
|
||||
|
||||
let verification_keys: Vec<VerificationKey> = coconut_keypairs
|
||||
.iter()
|
||||
.map(|keypair| keypair.verification_key())
|
||||
.map(|keypair| keypair.verification_key().clone())
|
||||
.collect();
|
||||
|
||||
// aggregate verification keys
|
||||
@@ -50,14 +51,14 @@ fn dkg() -> Result<(), CoconutError> {
|
||||
let params = setup(5)?;
|
||||
let node_indices = vec![15u64, 248, 33521];
|
||||
|
||||
let public_attributes = params.n_random_scalars(2);
|
||||
random_scalars_refs!(public_attributes, params, 2);
|
||||
|
||||
// generate_keys
|
||||
let coconut_keypairs = generate_dkg_keys(5, &node_indices);
|
||||
|
||||
let verification_keys: Vec<VerificationKey> = coconut_keypairs
|
||||
.iter()
|
||||
.map(|keypair| keypair.verification_key())
|
||||
.map(|keypair| keypair.verification_key().clone())
|
||||
.collect();
|
||||
|
||||
// aggregate verification keys
|
||||
|
||||
@@ -5,15 +5,17 @@ use crate::*;
|
||||
use itertools::izip;
|
||||
use std::fmt::Debug;
|
||||
|
||||
// unwraps are fine in the test code
|
||||
#[allow(clippy::unwrap_used)]
|
||||
pub fn theta_from_keys_and_attributes(
|
||||
params: &Parameters,
|
||||
coconut_keypairs: &Vec<KeyPair>,
|
||||
indices: &[scheme::SignerIndex],
|
||||
public_attributes: &Vec<PublicAttribute>,
|
||||
public_attributes: &[&PublicAttribute],
|
||||
) -> Result<Theta, CoconutError> {
|
||||
let serial_number = params.random_scalar();
|
||||
let binding_number = params.random_scalar();
|
||||
let private_attributes = vec![serial_number, binding_number];
|
||||
let private_attributes = vec![&serial_number, &binding_number];
|
||||
|
||||
// generate commitment
|
||||
let (commitments_openings, blind_sign_request) =
|
||||
@@ -21,7 +23,7 @@ pub fn theta_from_keys_and_attributes(
|
||||
|
||||
let verification_keys: Vec<VerificationKey> = coconut_keypairs
|
||||
.iter()
|
||||
.map(|keypair| keypair.verification_key())
|
||||
.map(|keypair| keypair.verification_key().clone())
|
||||
.collect();
|
||||
|
||||
// aggregate verification keys
|
||||
@@ -33,7 +35,7 @@ pub fn theta_from_keys_and_attributes(
|
||||
for keypair in coconut_keypairs {
|
||||
let blinded_signature = blind_sign(
|
||||
params,
|
||||
&keypair.secret_key(),
|
||||
keypair.secret_key(),
|
||||
&blind_sign_request,
|
||||
public_attributes,
|
||||
)?;
|
||||
@@ -49,7 +51,7 @@ pub fn theta_from_keys_and_attributes(
|
||||
.map(|(idx, s, vk)| {
|
||||
(
|
||||
*idx,
|
||||
s.unblind(
|
||||
s.unblind_and_verify(
|
||||
params,
|
||||
vk,
|
||||
&private_attributes,
|
||||
@@ -81,13 +83,15 @@ pub fn theta_from_keys_and_attributes(
|
||||
params,
|
||||
&verification_key,
|
||||
&signature,
|
||||
serial_number,
|
||||
binding_number,
|
||||
&serial_number,
|
||||
&binding_number,
|
||||
)?;
|
||||
|
||||
Ok(theta)
|
||||
}
|
||||
|
||||
// unwraps are fine in the test code
|
||||
#[allow(clippy::unwrap_used)]
|
||||
pub fn transpose_matrix<T: Debug>(matrix: Vec<Vec<T>>) -> Vec<Vec<T>> {
|
||||
if matrix.is_empty() {
|
||||
return vec![];
|
||||
@@ -166,3 +170,14 @@ pub mod tests {
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! random_scalars_refs {
|
||||
( $x: ident, $params: expr, $n: expr ) => {
|
||||
let _vec = $params.n_random_scalars($n);
|
||||
#[allow(clippy::map_identity)]
|
||||
let $x = _vec.iter().collect::<Vec<_>>();
|
||||
};
|
||||
}
|
||||
|
||||
pub use random_scalars_refs;
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
// Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#![warn(clippy::expect_used)]
|
||||
#![warn(clippy::unwrap_used)]
|
||||
|
||||
use crate::CoconutError;
|
||||
use bls12_381::{G1Affine, G1Projective, Scalar};
|
||||
use group::GroupEncoding;
|
||||
use std::convert::TryInto;
|
||||
|
||||
pub trait Bytable
|
||||
where
|
||||
@@ -14,9 +23,67 @@ where
|
||||
Self: Bytable,
|
||||
{
|
||||
fn try_from_bs58<S: AsRef<str>>(x: S) -> Result<Self, CoconutError> {
|
||||
Self::try_from_byte_slice(&bs58::decode(x.as_ref()).into_vec().unwrap())
|
||||
let bs58_decoded = &bs58::decode(x.as_ref()).into_vec()?;
|
||||
Self::try_from_byte_slice(bs58_decoded)
|
||||
}
|
||||
fn to_bs58(&self) -> String {
|
||||
bs58::encode(self.to_byte_vec()).into_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl Bytable for Scalar {
|
||||
fn to_byte_vec(&self) -> Vec<u8> {
|
||||
self.to_bytes().to_vec()
|
||||
}
|
||||
|
||||
fn try_from_byte_slice(slice: &[u8]) -> Result<Self, CoconutError> {
|
||||
let received = slice.len();
|
||||
let Ok(arr) = slice.try_into() else {
|
||||
return Err(CoconutError::UnexpectedArrayLength {
|
||||
typ: "Scalar".to_string(),
|
||||
received,
|
||||
expected: 32,
|
||||
});
|
||||
};
|
||||
|
||||
let maybe_scalar = Scalar::from_bytes(arr);
|
||||
if maybe_scalar.is_none().into() {
|
||||
Err(CoconutError::ScalarDeserializationFailure)
|
||||
} else {
|
||||
// safety: this unwrap is fine as we've just checked the element is not none
|
||||
#[allow(clippy::unwrap_used)]
|
||||
Ok(maybe_scalar.unwrap())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Base58 for Scalar {}
|
||||
|
||||
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, CoconutError> {
|
||||
let received = slice.len();
|
||||
let arr: Result<[u8; 48], _> = slice.try_into();
|
||||
let Ok(bytes) = arr else {
|
||||
return Err(CoconutError::UnexpectedArrayLength {
|
||||
typ: "G1Projective".to_string(),
|
||||
received,
|
||||
expected: 48,
|
||||
});
|
||||
};
|
||||
|
||||
let maybe_g1 = G1Affine::from_compressed(&bytes);
|
||||
if maybe_g1.is_none().into() {
|
||||
Err(CoconutError::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 {}
|
||||
|
||||
@@ -34,6 +34,7 @@ impl Polynomial {
|
||||
// just return the last term of the polynomial
|
||||
} else if x.is_zero().into() {
|
||||
// we checked that coefficients are not empty so unwrap here is fine
|
||||
#[allow(clippy::unwrap_used)]
|
||||
*self.coefficients.first().unwrap()
|
||||
} else {
|
||||
self.coefficients
|
||||
@@ -148,6 +149,8 @@ pub(crate) fn try_deserialize_scalar_vec(
|
||||
|
||||
let mut out = Vec::with_capacity(expected_len as usize);
|
||||
for i in 0..expected_len as usize {
|
||||
// we just checked we have exactly the amount of bytes we need and thus the unwrap is fine
|
||||
#[allow(clippy::unwrap_used)]
|
||||
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),
|
||||
|
||||
@@ -9,7 +9,7 @@ repository = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
bytes = "1.0"
|
||||
tokio-util = { version = "0.7.4", features = ["codec"] }
|
||||
tokio-util = { workspace = true, features = ["codec"] }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
nym-sphinx-types = { path = "../types", features = ["sphinx", "outfox"] }
|
||||
|
||||
@@ -8,8 +8,8 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
bytes = "1.0"
|
||||
tokio = { version = "1.24.1", features = [ "net", "io-util", "sync", "macros", "time", "rt-multi-thread" ] }
|
||||
tokio-util = { version = "0.7.4", features = [ "io" ] } # reason for getting this guy is to to able to port to tokio 1.X more quickly by being able to use
|
||||
tokio = { workspace = true, features = [ "net", "io-util", "sync", "macros", "time", "rt-multi-thread" ] }
|
||||
tokio-util = { workspace = true, features = [ "io" ] } # reason for getting this guy is to to able to port to tokio 1.X more quickly by being able to use
|
||||
# their `read_buf` [from the util crate] replacement rather than having to rethink/reimplement `AvailableReader` with the new AsyncRead trait definition.
|
||||
# In the long run, the dependency should probably get removed in favour of pure-tokio implementation, but for time being it's fine.
|
||||
futures = { workspace = true }
|
||||
|
||||
+1
-1
@@ -47,7 +47,7 @@ serde_json = "1.0.91"
|
||||
thiserror = { workspace = true }
|
||||
tokio = { version = "1", features = ["macros", "net","rt-multi-thread"] }
|
||||
tokio-tungstenite = { workspace = true }
|
||||
tokio-util = { version = "0.7.4", features = ["full"] }
|
||||
tokio-util = { workspace = true, features = ["full"] }
|
||||
toml = "0.7.0"
|
||||
unsigned-varint = "0.7.1"
|
||||
utoipa = { workspace = true, features = ["actix_extras"] }
|
||||
|
||||
+1
-1
@@ -53,7 +53,7 @@ tokio = { workspace = true, features = [
|
||||
] }
|
||||
tokio-stream = { version = "0.1.11", features = ["fs"] }
|
||||
tokio-tungstenite = { version = "0.20.1" }
|
||||
tokio-util = { version = "0.7.4", features = ["codec"] }
|
||||
tokio-util = { workspace = true, features = ["codec"] }
|
||||
url = { workspace = true, features = ["serde"] }
|
||||
zeroize = { workspace = true }
|
||||
|
||||
|
||||
+2
-2
@@ -33,8 +33,8 @@ rand = "0.7.3"
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
sysinfo = "0.27.7"
|
||||
tokio = { version = "1.21.2", features = ["rt-multi-thread", "net", "signal"] }
|
||||
tokio-util = { version = "0.7.3", features = ["codec"] }
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "net", "signal"] }
|
||||
tokio-util = { workspace = true, features = ["codec"] }
|
||||
toml = "0.5.8"
|
||||
url = { workspace = true, features = ["serde"] }
|
||||
cfg-if = "1.0.0"
|
||||
|
||||
+2
-1
@@ -24,6 +24,7 @@ clap = { workspace = true, features = ["cargo", "derive"] }
|
||||
console-subscriber = { version = "0.1.1", optional = true } # validator-api needs to be built with RUSTFLAGS="--cfg tokio_unstable"
|
||||
dirs = "4.0"
|
||||
futures = { workspace = true }
|
||||
itertools = "0.12.0"
|
||||
humantime-serde = "1.0"
|
||||
lazy_static = "1.4.0"
|
||||
log = { workspace = true }
|
||||
@@ -97,7 +98,7 @@ nym-contracts-common = { path = "../common/cosmwasm-smart-contracts/contracts-co
|
||||
nym-multisig-contract-common = { path = "../common/cosmwasm-smart-contracts/multisig-contract" }
|
||||
nym-service-provider-directory-common = { path = "../common/cosmwasm-smart-contracts/service-provider-directory" }
|
||||
nym-name-service-common = { path = "../common/cosmwasm-smart-contracts/name-service" }
|
||||
nym-coconut = { path = "../common/nymcoconut" }
|
||||
nym-coconut = { path = "../common/nymcoconut", features = ["key-zeroize"] }
|
||||
nym-sphinx = { path = "../common/nymsphinx" }
|
||||
nym-pemstore = { path = "../common/pemstore" }
|
||||
nym-task = { path = "../common/task" }
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
|
||||
DROP TABLE signed_deposit;
|
||||
|
||||
-- represents information about credentials issued in that epoch so that the other parties could make their appropriate queries
|
||||
-- note: before this information can be returned to a client (of the API), it needs to be signed first
|
||||
CREATE TABLE epoch_credentials
|
||||
(
|
||||
epoch_id INTEGER NOT NULL PRIMARY KEY UNIQUE,
|
||||
start_id INTEGER NOT NULL,
|
||||
total_issued INTEGER NOT NULL
|
||||
);
|
||||
|
||||
-- particular credential issued in this epoch
|
||||
CREATE TABLE issued_credential
|
||||
(
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
epoch_id INTEGER NOT NULL,
|
||||
tx_hash VARCHAR NOT NULL UNIQUE,
|
||||
bs58_partial_credential VARCHAR NOT NULL,
|
||||
bs58_signature VARCHAR NOT NULL,
|
||||
joined_private_commitments VARCHAR NOT NULL,
|
||||
joined_public_attributes VARCHAR NOT NULL
|
||||
);
|
||||
@@ -13,8 +13,11 @@ getset = "0.1.1"
|
||||
schemars = { workspace = true, features = ["preserve_order"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
ts-rs = { workspace = true, optional = true }
|
||||
tendermint = { workspace = true }
|
||||
|
||||
nym-coconut-interface = { path = "../../common/coconut-interface" }
|
||||
nym-crypto = { path = "../../common/crypto", features = ["serde", "asymmetric"]}
|
||||
|
||||
nym-mixnet-contract-common = { path= "../../common/cosmwasm-smart-contracts/mixnet-contract" }
|
||||
nym-node-requests = { path = "../../nym-node/nym-node-requests", default-features = false }
|
||||
|
||||
|
||||
@@ -1,158 +0,0 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use cosmrs::AccountId;
|
||||
use getset::{CopyGetters, Getters};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use nym_coconut_interface::{
|
||||
error::CoconutInterfaceError, Attribute, Base58, BlindSignRequest, Credential, VerificationKey,
|
||||
};
|
||||
|
||||
#[derive(Serialize, Deserialize, Getters, CopyGetters)]
|
||||
pub struct VerifyCredentialBody {
|
||||
#[getset(get = "pub")]
|
||||
credential: Credential,
|
||||
#[getset(get = "pub")]
|
||||
proposal_id: u64,
|
||||
#[getset(get = "pub")]
|
||||
gateway_cosmos_addr: AccountId,
|
||||
}
|
||||
|
||||
impl VerifyCredentialBody {
|
||||
pub fn new(
|
||||
credential: Credential,
|
||||
proposal_id: u64,
|
||||
gateway_cosmos_addr: AccountId,
|
||||
) -> VerifyCredentialBody {
|
||||
VerifyCredentialBody {
|
||||
credential,
|
||||
proposal_id,
|
||||
gateway_cosmos_addr,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct VerifyCredentialResponse {
|
||||
pub verification_result: bool,
|
||||
}
|
||||
|
||||
impl VerifyCredentialResponse {
|
||||
pub fn new(verification_result: bool) -> Self {
|
||||
VerifyCredentialResponse {
|
||||
verification_result,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// All strings are base58 encoded representations of structs
|
||||
#[derive(Clone, Serialize, Deserialize, Debug, Getters, CopyGetters)]
|
||||
pub struct BlindSignRequestBody {
|
||||
#[getset(get = "pub")]
|
||||
blind_sign_request: BlindSignRequest,
|
||||
#[getset(get = "pub")]
|
||||
tx_hash: String,
|
||||
#[getset(get = "pub")]
|
||||
signature: String,
|
||||
public_attributes: Vec<String>,
|
||||
#[getset(get = "pub")]
|
||||
public_attributes_plain: Vec<String>,
|
||||
#[getset(get = "pub")]
|
||||
total_params: u32,
|
||||
}
|
||||
|
||||
impl BlindSignRequestBody {
|
||||
pub fn new(
|
||||
blind_sign_request: &BlindSignRequest,
|
||||
tx_hash: String,
|
||||
signature: String,
|
||||
public_attributes: &[Attribute],
|
||||
public_attributes_plain: Vec<String>,
|
||||
total_params: u32,
|
||||
) -> BlindSignRequestBody {
|
||||
BlindSignRequestBody {
|
||||
blind_sign_request: blind_sign_request.clone(),
|
||||
tx_hash,
|
||||
signature,
|
||||
public_attributes: public_attributes
|
||||
.iter()
|
||||
.map(|attr| attr.to_bs58())
|
||||
.collect(),
|
||||
public_attributes_plain,
|
||||
total_params,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn public_attributes(&self) -> Vec<Attribute> {
|
||||
self.public_attributes
|
||||
.iter()
|
||||
.map(|x| Attribute::try_from_bs58(x).unwrap())
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct BlindedSignatureResponse {
|
||||
pub remote_key: [u8; 32],
|
||||
pub encrypted_signature: Vec<u8>,
|
||||
}
|
||||
|
||||
impl BlindedSignatureResponse {
|
||||
pub fn new(encrypted_signature: Vec<u8>, remote_key: [u8; 32]) -> BlindedSignatureResponse {
|
||||
BlindedSignatureResponse {
|
||||
encrypted_signature,
|
||||
remote_key,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_base58_string(&self) -> String {
|
||||
bs58::encode(&self.to_bytes()).into_string()
|
||||
}
|
||||
|
||||
pub fn from_base58_string<I: AsRef<[u8]>>(val: I) -> Result<Self, CoconutInterfaceError> {
|
||||
let bytes = bs58::decode(val).into_vec()?;
|
||||
Self::from_bytes(&bytes)
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
let mut bytes = self.remote_key.to_vec();
|
||||
bytes.extend_from_slice(&self.encrypted_signature);
|
||||
bytes
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: &[u8]) -> Result<Self, CoconutInterfaceError> {
|
||||
if bytes.len() < 32 {
|
||||
return Err(CoconutInterfaceError::InvalidByteLength(bytes.len(), 32));
|
||||
}
|
||||
let mut remote_key = [0u8; 32];
|
||||
remote_key.copy_from_slice(&bytes[..32]);
|
||||
let encrypted_signature = bytes[32..].to_vec();
|
||||
Ok(BlindedSignatureResponse {
|
||||
remote_key,
|
||||
encrypted_signature,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct VerificationKeyResponse {
|
||||
pub key: VerificationKey,
|
||||
}
|
||||
|
||||
impl VerificationKeyResponse {
|
||||
pub fn new(key: VerificationKey) -> VerificationKeyResponse {
|
||||
VerificationKeyResponse { key }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct CosmosAddressResponse {
|
||||
pub addr: AccountId,
|
||||
}
|
||||
|
||||
impl CosmosAddressResponse {
|
||||
pub fn new(addr: AccountId) -> CosmosAddressResponse {
|
||||
CosmosAddressResponse { addr }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use nym_coconut_interface::BlindedSignature;
|
||||
use tendermint::hash::Hash;
|
||||
|
||||
// recomputes plaintext on the credential nym-api has used for signing
|
||||
//
|
||||
// note: this method doesn't have to be reversible so just naively concatenate everything
|
||||
pub fn issued_credential_plaintext(
|
||||
epoch_id: u32,
|
||||
tx_hash: Hash,
|
||||
blinded_partial_credential: &BlindedSignature,
|
||||
bs58_encoded_private_attributes_commitments: &[String],
|
||||
public_attributes: &[String],
|
||||
) -> Vec<u8> {
|
||||
epoch_id
|
||||
.to_be_bytes()
|
||||
.into_iter()
|
||||
.chain(tx_hash.as_bytes().iter().copied())
|
||||
.chain(blinded_partial_credential.to_bytes())
|
||||
.chain(
|
||||
bs58_encoded_private_attributes_commitments
|
||||
.iter()
|
||||
.flat_map(|attr| attr.as_bytes().iter().copied()),
|
||||
)
|
||||
.chain(
|
||||
public_attributes
|
||||
.iter()
|
||||
.flat_map(|attr| attr.as_bytes().iter().copied()),
|
||||
)
|
||||
.collect()
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub mod helpers;
|
||||
pub mod models;
|
||||
|
||||
pub use models::{
|
||||
BlindSignRequestBody, BlindedSignatureResponse, CredentialsRequestBody,
|
||||
VerificationKeyResponse, VerifyCredentialBody, VerifyCredentialResponse,
|
||||
};
|
||||
@@ -0,0 +1,233 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::coconut::helpers::issued_credential_plaintext;
|
||||
use cosmrs::AccountId;
|
||||
use nym_coconut_interface::{
|
||||
error::CoconutInterfaceError, hash_to_scalar, Attribute, BlindSignRequest, BlindedSignature,
|
||||
Bytable, Credential, VerificationKey,
|
||||
};
|
||||
use nym_crypto::asymmetric::identity;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::BTreeMap;
|
||||
use tendermint::hash::Hash;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct VerifyCredentialBody {
|
||||
pub credential: Credential,
|
||||
|
||||
pub proposal_id: u64,
|
||||
|
||||
pub gateway_cosmos_addr: AccountId,
|
||||
}
|
||||
|
||||
impl VerifyCredentialBody {
|
||||
pub fn new(
|
||||
credential: Credential,
|
||||
proposal_id: u64,
|
||||
gateway_cosmos_addr: AccountId,
|
||||
) -> VerifyCredentialBody {
|
||||
VerifyCredentialBody {
|
||||
credential,
|
||||
proposal_id,
|
||||
gateway_cosmos_addr,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct VerifyCredentialResponse {
|
||||
pub verification_result: bool,
|
||||
}
|
||||
|
||||
impl VerifyCredentialResponse {
|
||||
pub fn new(verification_result: bool) -> Self {
|
||||
VerifyCredentialResponse {
|
||||
verification_result,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// All strings are base58 encoded representations of structs
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
pub struct BlindSignRequestBody {
|
||||
pub inner_sign_request: BlindSignRequest,
|
||||
|
||||
/// Hash of the deposit transaction
|
||||
pub tx_hash: Hash,
|
||||
|
||||
/// Signature on the inner sign request and the tx hash
|
||||
pub signature: identity::Signature,
|
||||
|
||||
// public_attributes: Vec<String>,
|
||||
pub public_attributes_plain: Vec<String>,
|
||||
}
|
||||
|
||||
impl BlindSignRequestBody {
|
||||
pub fn new(
|
||||
inner_sign_request: BlindSignRequest,
|
||||
tx_hash: Hash,
|
||||
signature: identity::Signature,
|
||||
public_attributes_plain: Vec<String>,
|
||||
) -> BlindSignRequestBody {
|
||||
BlindSignRequestBody {
|
||||
inner_sign_request,
|
||||
tx_hash,
|
||||
signature,
|
||||
public_attributes_plain,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn attributes(&self) -> u32 {
|
||||
(self.public_attributes_plain.len() + self.inner_sign_request.num_private_attributes())
|
||||
as u32
|
||||
}
|
||||
|
||||
pub fn public_attributes_hashed(&self) -> Vec<Attribute> {
|
||||
self.public_attributes_plain
|
||||
.iter()
|
||||
.map(hash_to_scalar)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn encode_commitments(&self) -> Vec<String> {
|
||||
use nym_coconut_interface::Base58;
|
||||
|
||||
self.inner_sign_request
|
||||
.get_private_attributes_pedersen_commitments()
|
||||
.iter()
|
||||
.map(|c| c.to_bs58())
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct BlindedSignatureResponse {
|
||||
pub blinded_signature: BlindedSignature,
|
||||
}
|
||||
|
||||
impl BlindedSignatureResponse {
|
||||
pub fn new(blinded_signature: BlindedSignature) -> BlindedSignatureResponse {
|
||||
BlindedSignatureResponse { blinded_signature }
|
||||
}
|
||||
|
||||
pub fn to_base58_string(&self) -> String {
|
||||
bs58::encode(&self.to_bytes()).into_string()
|
||||
}
|
||||
|
||||
pub fn from_base58_string<I: AsRef<[u8]>>(val: I) -> Result<Self, CoconutInterfaceError> {
|
||||
let bytes = bs58::decode(val).into_vec()?;
|
||||
Self::from_bytes(&bytes)
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
self.blinded_signature.to_byte_vec()
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: &[u8]) -> Result<Self, CoconutInterfaceError> {
|
||||
Ok(BlindedSignatureResponse {
|
||||
blinded_signature: BlindedSignature::from_bytes(bytes)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct VerificationKeyResponse {
|
||||
pub key: VerificationKey,
|
||||
}
|
||||
|
||||
impl VerificationKeyResponse {
|
||||
pub fn new(key: VerificationKey) -> VerificationKeyResponse {
|
||||
VerificationKeyResponse { key }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct CosmosAddressResponse {
|
||||
pub addr: AccountId,
|
||||
}
|
||||
|
||||
impl CosmosAddressResponse {
|
||||
pub fn new(addr: AccountId) -> CosmosAddressResponse {
|
||||
CosmosAddressResponse { addr }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
pub struct Pagination<T> {
|
||||
/// last_key is the last value returned in the previous query.
|
||||
/// it's used to indicate the start of the next (this) page.
|
||||
/// the value itself is not included in the response.
|
||||
pub last_key: Option<T>,
|
||||
|
||||
/// limit is the total number of results to be returned in the result page.
|
||||
/// If left empty it will default to a value to be set by each app.
|
||||
pub limit: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CredentialsRequestBody {
|
||||
/// Explicit ids of the credentials to retrieve. Note: it can't be set alongside pagination.
|
||||
pub credential_ids: Vec<i64>,
|
||||
|
||||
/// Pagination settings for retrieving credentials. Note: it can't be set alongside explicit ids.
|
||||
pub pagination: Option<Pagination<i64>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EpochCredentialsResponse {
|
||||
pub epoch_id: u64,
|
||||
pub first_epoch_credential_id: Option<i64>,
|
||||
pub total_issued: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct IssuedCredentialsResponse {
|
||||
// note: BTreeMap returns ordered results so it's fine to use it with pagination
|
||||
pub credentials: BTreeMap<i64, IssuedCredentialInner>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct IssuedCredentialResponse {
|
||||
pub credential: Option<IssuedCredentialInner>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct IssuedCredentialInner {
|
||||
pub credential: IssuedCredential,
|
||||
|
||||
pub signature: identity::Signature,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct IssuedCredential {
|
||||
pub id: i64,
|
||||
pub epoch_id: u32,
|
||||
pub tx_hash: Hash,
|
||||
|
||||
// NOTE: if we find creation of this guy takes too long,
|
||||
// change `BlindedSignature` to `BlindedSignatureBytes`
|
||||
// so that nym-api wouldn't need to parse the value out of its storage
|
||||
pub blinded_partial_credential: BlindedSignature,
|
||||
pub bs58_encoded_private_attributes_commitments: Vec<String>,
|
||||
pub public_attributes: Vec<String>,
|
||||
}
|
||||
|
||||
impl IssuedCredential {
|
||||
// this method doesn't have to be reversible so just naively concatenate everything
|
||||
pub fn signable_plaintext(&self) -> Vec<u8> {
|
||||
issued_credential_plaintext(
|
||||
self.epoch_id,
|
||||
self.tx_hash,
|
||||
&self.blinded_partial_credential,
|
||||
&self.bs58_encoded_private_attributes_commitments,
|
||||
&self.public_attributes,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::coconut::error::Result;
|
||||
use crate::coconut::storage::models::IssuedCredential;
|
||||
use nym_api_requests::coconut::models::IssuedCredentialInner;
|
||||
use nym_api_requests::coconut::models::IssuedCredentialsResponse;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
pub(crate) fn build_credentials_response(
|
||||
raw: Vec<IssuedCredential>,
|
||||
) -> Result<IssuedCredentialsResponse> {
|
||||
let mut credentials = BTreeMap::new();
|
||||
|
||||
for raw_credential in raw {
|
||||
let id = raw_credential.id;
|
||||
let api_issued = IssuedCredentialInner::try_from(raw_credential)?;
|
||||
let old = credentials.insert(id, api_issued);
|
||||
if old.is_some() {
|
||||
// why do we panic here rather than return an error? because it's a critical failure because
|
||||
// since the raw values came directly from the database with the PRIMARY KEY constraint
|
||||
// it should be IMPOSSIBLE to have duplicate values here
|
||||
panic!("somehow retrieved multiple credentials with id {id} from the database!")
|
||||
}
|
||||
}
|
||||
|
||||
Ok(IssuedCredentialsResponse { credentials })
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::coconut::api_routes::helpers::build_credentials_response;
|
||||
use crate::coconut::error::{CoconutError, Result};
|
||||
use crate::coconut::helpers::{accepted_vote_err, blind_sign};
|
||||
use crate::coconut::state::State;
|
||||
use crate::coconut::storage::CoconutStorageExt;
|
||||
use nym_api_requests::coconut::models::{
|
||||
CredentialsRequestBody, EpochCredentialsResponse, IssuedCredentialResponse,
|
||||
IssuedCredentialsResponse,
|
||||
};
|
||||
use nym_api_requests::coconut::{
|
||||
BlindSignRequestBody, BlindedSignatureResponse, VerifyCredentialBody, VerifyCredentialResponse,
|
||||
};
|
||||
use nym_coconut_bandwidth_contract_common::spend_credential::{
|
||||
funds_from_cosmos_msgs, SpendCredentialStatus,
|
||||
};
|
||||
use nym_coconut_dkg_common::types::EpochId;
|
||||
use nym_credentials::coconut::bandwidth::BandwidthVoucher;
|
||||
use nym_validator_client::nyxd::{Coin, Fee};
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::State as RocketState;
|
||||
|
||||
mod helpers;
|
||||
|
||||
#[post("/blind-sign", data = "<blind_sign_request_body>")]
|
||||
// Until we have serialization and deserialization traits we'll be using a crutch
|
||||
pub async fn post_blind_sign(
|
||||
blind_sign_request_body: Json<BlindSignRequestBody>,
|
||||
state: &RocketState<State>,
|
||||
) -> Result<Json<BlindedSignatureResponse>> {
|
||||
debug!("Received blind sign request");
|
||||
trace!("body: {:?}", blind_sign_request_body);
|
||||
|
||||
// early check: does the request have the expected number of public attributes?
|
||||
debug!("performing basic request validation");
|
||||
if blind_sign_request_body.public_attributes_plain.len()
|
||||
!= BandwidthVoucher::PUBLIC_ATTRIBUTES as usize
|
||||
{
|
||||
return Err(CoconutError::InconsistentPublicAttributes);
|
||||
}
|
||||
|
||||
// check if we already issued a credential for this tx hash
|
||||
debug!(
|
||||
"checking if we have already issued credential for this tx_hash (hash: {})",
|
||||
blind_sign_request_body.tx_hash
|
||||
);
|
||||
if let Some(blinded_signature) = state
|
||||
.already_issued(blind_sign_request_body.tx_hash)
|
||||
.await?
|
||||
{
|
||||
return Ok(Json(BlindedSignatureResponse { blinded_signature }));
|
||||
}
|
||||
|
||||
// check if we have the signing key available
|
||||
debug!("checking if we actually have coconut keys derived...");
|
||||
let keypair_guard = state.coconut_keypair.get().await;
|
||||
let Some(signing_key) = keypair_guard.as_ref() else {
|
||||
return Err(CoconutError::KeyPairNotDerivedYet);
|
||||
};
|
||||
|
||||
// get the transaction details of the claimed deposit
|
||||
debug!("getting transaction details from the chain");
|
||||
let tx = state
|
||||
.get_transaction(blind_sign_request_body.tx_hash)
|
||||
.await?;
|
||||
|
||||
// check validity of the request
|
||||
debug!("fully validating received request");
|
||||
state.validate_request(&blind_sign_request_body, tx).await?;
|
||||
|
||||
// produce the partial signature
|
||||
debug!("producing the partial credential");
|
||||
let blinded_signature = blind_sign(&blind_sign_request_body, signing_key.secret_key())?;
|
||||
|
||||
// store the information locally
|
||||
debug!("storing the issued credential in the database");
|
||||
state
|
||||
.store_issued_credential(blind_sign_request_body.into_inner(), &blinded_signature)
|
||||
.await?;
|
||||
|
||||
// finally return the credential to the client
|
||||
Ok(Json(BlindedSignatureResponse { blinded_signature }))
|
||||
}
|
||||
|
||||
#[post("/verify-bandwidth-credential", data = "<verify_credential_body>")]
|
||||
pub async fn verify_bandwidth_credential(
|
||||
verify_credential_body: Json<VerifyCredentialBody>,
|
||||
state: &RocketState<State>,
|
||||
) -> Result<Json<VerifyCredentialResponse>> {
|
||||
let proposal_id = verify_credential_body.proposal_id;
|
||||
let proposal = state.client.get_proposal(proposal_id).await?;
|
||||
// Proposal description is the blinded serial number
|
||||
if !verify_credential_body
|
||||
.credential
|
||||
.has_blinded_serial_number(&proposal.description)?
|
||||
{
|
||||
return Err(CoconutError::IncorrectProposal {
|
||||
reason: String::from("incorrect blinded serial number in description"),
|
||||
});
|
||||
}
|
||||
let proposed_release_funds =
|
||||
funds_from_cosmos_msgs(proposal.msgs).ok_or(CoconutError::IncorrectProposal {
|
||||
reason: String::from("action is not to release funds"),
|
||||
})?;
|
||||
// Credential has not been spent before, and is on its way of being spent
|
||||
let credential_status = state
|
||||
.client
|
||||
.get_spent_credential(verify_credential_body.credential.blinded_serial_number())
|
||||
.await?
|
||||
.spend_credential
|
||||
.ok_or(CoconutError::InvalidCredentialStatus {
|
||||
status: String::from("Inexistent"),
|
||||
})?
|
||||
.status();
|
||||
if credential_status != SpendCredentialStatus::InProgress {
|
||||
return Err(CoconutError::InvalidCredentialStatus {
|
||||
status: format!("{:?}", credential_status),
|
||||
});
|
||||
}
|
||||
let verification_key = state
|
||||
.verification_key(*verify_credential_body.credential.epoch_id())
|
||||
.await?;
|
||||
let mut vote_yes = verify_credential_body.credential.verify(&verification_key);
|
||||
|
||||
vote_yes &= Coin::from(proposed_release_funds)
|
||||
== Coin::new(
|
||||
verify_credential_body.credential.voucher_value() as u128,
|
||||
state.mix_denom.clone(),
|
||||
);
|
||||
|
||||
// Vote yes or no on the proposal based on the verification result
|
||||
let ret = state
|
||||
.client
|
||||
.vote_proposal(
|
||||
proposal_id,
|
||||
vote_yes,
|
||||
Some(Fee::new_payer_granter_auto(
|
||||
None,
|
||||
None,
|
||||
Some(verify_credential_body.gateway_cosmos_addr.clone()),
|
||||
)),
|
||||
)
|
||||
.await;
|
||||
accepted_vote_err(ret)?;
|
||||
|
||||
Ok(Json(VerifyCredentialResponse::new(vote_yes)))
|
||||
}
|
||||
|
||||
#[get("/epoch-credentials/<epoch>")]
|
||||
pub async fn epoch_credentials(
|
||||
epoch: EpochId,
|
||||
state: &RocketState<State>,
|
||||
) -> Result<Json<EpochCredentialsResponse>> {
|
||||
let issued = state.storage.get_epoch_credentials(epoch).await?;
|
||||
|
||||
let response = if let Some(issued) = issued {
|
||||
issued.into()
|
||||
} else {
|
||||
EpochCredentialsResponse {
|
||||
epoch_id: epoch,
|
||||
first_epoch_credential_id: None,
|
||||
total_issued: 0,
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Json(response))
|
||||
}
|
||||
|
||||
#[get("/issued-credential/<id>")]
|
||||
pub async fn issued_credential(
|
||||
id: i64,
|
||||
state: &RocketState<State>,
|
||||
) -> Result<Json<IssuedCredentialResponse>> {
|
||||
let issued = state.storage.get_issued_credential(id).await?;
|
||||
|
||||
let credential = if let Some(issued) = issued {
|
||||
Some(issued.try_into()?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(Json(IssuedCredentialResponse { credential }))
|
||||
}
|
||||
|
||||
#[post("/issued-credentials", data = "<params>")]
|
||||
pub async fn issued_credentials(
|
||||
params: Json<CredentialsRequestBody>,
|
||||
state: &RocketState<State>,
|
||||
) -> Result<Json<IssuedCredentialsResponse>> {
|
||||
let params = params.into_inner();
|
||||
|
||||
if params.pagination.is_some() && !params.credential_ids.is_empty() {
|
||||
return Err(CoconutError::InvalidQueryArguments);
|
||||
}
|
||||
|
||||
let credentials = if let Some(pagination) = params.pagination {
|
||||
state
|
||||
.storage
|
||||
.get_issued_credentials_paged(pagination)
|
||||
.await?
|
||||
} else {
|
||||
state
|
||||
.storage
|
||||
.get_issued_credentials(params.credential_ids)
|
||||
.await?
|
||||
};
|
||||
|
||||
build_credentials_response(credentials).map(Json)
|
||||
}
|
||||
@@ -13,12 +13,12 @@ use nym_coconut_dkg_common::verification_key::{ContractVKShare, VerificationKeyS
|
||||
use nym_contracts_common::dealings::ContractSafeBytes;
|
||||
use nym_dkg::Threshold;
|
||||
use nym_validator_client::nyxd::cosmwasm_client::types::ExecuteResult;
|
||||
use nym_validator_client::nyxd::{AccountId, Fee, TxResponse};
|
||||
use nym_validator_client::nyxd::{AccountId, Fee, Hash, TxResponse};
|
||||
|
||||
#[async_trait]
|
||||
pub trait Client {
|
||||
async fn address(&self) -> AccountId;
|
||||
async fn get_tx(&self, tx_hash: &str) -> Result<TxResponse>;
|
||||
async fn get_tx(&self, tx_hash: Hash) -> Result<TxResponse>;
|
||||
async fn get_proposal(&self, proposal_id: u64) -> Result<ProposalResponse>;
|
||||
async fn list_proposals(&self) -> Result<Vec<ProposalResponse>>;
|
||||
async fn get_spent_credential(
|
||||
|
||||
@@ -7,29 +7,101 @@ use nym_coconut_dkg_common::types::EpochId;
|
||||
use nym_coconut_interface::VerificationKey;
|
||||
use nym_credentials::coconut::utils::obtain_aggregate_verification_key;
|
||||
use nym_validator_client::coconut::all_coconut_api_clients;
|
||||
use nym_validator_client::nyxd::contract_traits::DkgQueryClient;
|
||||
use nym_validator_client::DirectSigningHttpRpcNyxdClient;
|
||||
use std::cmp::min;
|
||||
use std::collections::HashMap;
|
||||
use std::ops::Deref;
|
||||
use time::OffsetDateTime;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
#[async_trait]
|
||||
pub trait APICommunicationChannel {
|
||||
async fn current_epoch(&self) -> Result<EpochId>;
|
||||
async fn aggregated_verification_key(&self, epoch_id: EpochId) -> Result<VerificationKey>;
|
||||
}
|
||||
|
||||
struct CachedEpoch {
|
||||
valid_until: OffsetDateTime,
|
||||
current_epoch_id: EpochId,
|
||||
}
|
||||
|
||||
impl Default for CachedEpoch {
|
||||
fn default() -> Self {
|
||||
CachedEpoch {
|
||||
valid_until: OffsetDateTime::UNIX_EPOCH,
|
||||
current_epoch_id: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CachedEpoch {
|
||||
fn is_valid(&self) -> bool {
|
||||
self.valid_until > OffsetDateTime::now_utc()
|
||||
}
|
||||
|
||||
async fn update(&mut self, client: &DirectSigningHttpRpcNyxdClient) -> Result<()> {
|
||||
let epoch = client.get_current_epoch().await?;
|
||||
|
||||
let now = OffsetDateTime::now_utc();
|
||||
let state_end =
|
||||
OffsetDateTime::from_unix_timestamp(epoch.finish_timestamp.seconds() as i64).unwrap();
|
||||
let until_epoch_state_end = state_end - now;
|
||||
|
||||
// make it valid until the next epoch transition or next 5min, whichever is smaller
|
||||
self.valid_until = now + min(until_epoch_state_end, 5 * time::Duration::MINUTE);
|
||||
self.current_epoch_id = epoch.epoch_id;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct QueryCommunicationChannel {
|
||||
nyxd_client: nyxd::Client,
|
||||
|
||||
epoch_keys: RwLock<HashMap<EpochId, VerificationKey>>,
|
||||
cached_epoch: RwLock<CachedEpoch>,
|
||||
}
|
||||
|
||||
impl QueryCommunicationChannel {
|
||||
pub fn new(nyxd_client: nyxd::Client) -> Self {
|
||||
QueryCommunicationChannel { nyxd_client }
|
||||
QueryCommunicationChannel {
|
||||
nyxd_client,
|
||||
epoch_keys: Default::default(),
|
||||
cached_epoch: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl APICommunicationChannel for QueryCommunicationChannel {
|
||||
async fn current_epoch(&self) -> Result<EpochId> {
|
||||
let guard = self.cached_epoch.read().await;
|
||||
if guard.is_valid() {
|
||||
return Ok(guard.current_epoch_id);
|
||||
}
|
||||
|
||||
// update cache
|
||||
drop(guard);
|
||||
let mut guard = self.cached_epoch.write().await;
|
||||
let client = self.nyxd_client.0.read().await;
|
||||
guard.update(&client).await?;
|
||||
|
||||
return Ok(guard.current_epoch_id);
|
||||
}
|
||||
|
||||
async fn aggregated_verification_key(&self, epoch_id: EpochId) -> Result<VerificationKey> {
|
||||
if let Some(vk) = self.epoch_keys.read().await.get(&epoch_id) {
|
||||
return Ok(vk.clone());
|
||||
}
|
||||
|
||||
let mut guard = self.epoch_keys.write().await;
|
||||
let client = self.nyxd_client.0.read().await;
|
||||
let coconut_api_clients = all_coconut_api_clients(client.deref(), epoch_id).await?;
|
||||
let vk = obtain_aggregate_verification_key(&coconut_api_clients).await?;
|
||||
|
||||
guard.insert(epoch_id, vk.clone());
|
||||
|
||||
Ok(vk)
|
||||
}
|
||||
}
|
||||
|
||||
+133
-256
@@ -1,189 +1,112 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::coconut::error::{CoconutError, Result};
|
||||
use nym_api_requests::coconut::BlindSignRequestBody;
|
||||
use nym_coconut_bandwidth_contract_common::events::{
|
||||
DEPOSITED_FUNDS_EVENT_TYPE, DEPOSIT_ENCRYPTION_KEY, DEPOSIT_IDENTITY_KEY, DEPOSIT_INFO,
|
||||
DEPOSIT_VALUE,
|
||||
COSMWASM_DEPOSITED_FUNDS_EVENT_TYPE, DEPOSIT_ENCRYPTION_KEY, DEPOSIT_IDENTITY_KEY,
|
||||
DEPOSIT_INFO, DEPOSIT_VALUE,
|
||||
};
|
||||
use nym_credentials::coconut::bandwidth::BandwidthVoucher;
|
||||
use nym_crypto::asymmetric::encryption;
|
||||
use nym_crypto::asymmetric::identity::{self, Signature};
|
||||
use nym_crypto::asymmetric::identity;
|
||||
use nym_validator_client::nyxd::helpers::find_tx_attribute;
|
||||
use nym_validator_client::nyxd::TxResponse;
|
||||
|
||||
use super::error::{CoconutError, Result};
|
||||
|
||||
pub async fn extract_encryption_key(
|
||||
blind_sign_request_body: &BlindSignRequestBody,
|
||||
tx: TxResponse,
|
||||
) -> Result<encryption::PublicKey> {
|
||||
let blind_sign_request = blind_sign_request_body.blind_sign_request();
|
||||
let public_attributes = blind_sign_request_body.public_attributes();
|
||||
let public_attributes_plain = blind_sign_request_body.public_attributes_plain();
|
||||
|
||||
if !BandwidthVoucher::verify_against_plain(&public_attributes, public_attributes_plain) {
|
||||
pub async fn validate_deposit_tx(request: &BlindSignRequestBody, tx: TxResponse) -> Result<()> {
|
||||
if request.public_attributes_plain.len() != BandwidthVoucher::PUBLIC_ATTRIBUTES as usize {
|
||||
return Err(CoconutError::InconsistentPublicAttributes);
|
||||
}
|
||||
|
||||
let tx_hash_str = blind_sign_request_body.tx_hash();
|
||||
let mut message = blind_sign_request.to_bytes();
|
||||
message.extend_from_slice(tx_hash_str.as_bytes());
|
||||
// extract actual public attributes + associated x25519 public key
|
||||
let deposit_value = find_tx_attribute(&tx, COSMWASM_DEPOSITED_FUNDS_EVENT_TYPE, DEPOSIT_VALUE)
|
||||
.ok_or(CoconutError::DepositValueNotFound)?;
|
||||
|
||||
let signature = Signature::from_base58_string(blind_sign_request_body.signature())?;
|
||||
let deposit_info = find_tx_attribute(&tx, COSMWASM_DEPOSITED_FUNDS_EVENT_TYPE, DEPOSIT_INFO)
|
||||
.ok_or(CoconutError::DepositInfoNotFound)?;
|
||||
|
||||
let attributes: &Vec<_> = tx
|
||||
.tx_result
|
||||
.events
|
||||
.iter()
|
||||
.find(|event| event.kind == format!("wasm-{}", DEPOSITED_FUNDS_EVENT_TYPE))
|
||||
.ok_or(CoconutError::DepositEventNotFound)?
|
||||
.attributes
|
||||
.as_ref();
|
||||
let x25519_raw = find_tx_attribute(
|
||||
&tx,
|
||||
COSMWASM_DEPOSITED_FUNDS_EVENT_TYPE,
|
||||
DEPOSIT_IDENTITY_KEY,
|
||||
)
|
||||
.ok_or(CoconutError::DepositVerifKeyNotFound)?;
|
||||
|
||||
let deposit_value: &str = attributes
|
||||
.iter()
|
||||
.find(|tag| tag.key == DEPOSIT_VALUE)
|
||||
.ok_or(CoconutError::DepositValueNotFound)?
|
||||
.value
|
||||
.as_ref();
|
||||
let deposit_value_plain = public_attributes_plain.first().cloned().unwrap_or_default();
|
||||
if deposit_value != deposit_value_plain {
|
||||
return Err(CoconutError::DifferentPublicAttributes(
|
||||
deposit_value.to_string(),
|
||||
deposit_value_plain,
|
||||
));
|
||||
// we're not using it anymore, but for legacy reasons it must be present
|
||||
let _ed25519_raw = find_tx_attribute(
|
||||
&tx,
|
||||
COSMWASM_DEPOSITED_FUNDS_EVENT_TYPE,
|
||||
DEPOSIT_ENCRYPTION_KEY,
|
||||
)
|
||||
.ok_or(CoconutError::DepositEncrKeyNotFound)?;
|
||||
|
||||
// check public attributes against request data
|
||||
// (thinking about it attaching that data might be redundant since we have the source of truth on the chain)
|
||||
// safety: we won't read data out of bounds since we just checked we have BandwidthVoucher::PUBLIC_ATTRIBUTES values in the vec
|
||||
if deposit_value != request.public_attributes_plain[0] {
|
||||
return Err(CoconutError::InconsistentDepositValue {
|
||||
request: request.public_attributes_plain[0].clone(),
|
||||
on_chain: deposit_value,
|
||||
});
|
||||
}
|
||||
|
||||
let deposit_info: &str = attributes
|
||||
.iter()
|
||||
.find(|tag| tag.key == DEPOSIT_INFO)
|
||||
.ok_or(CoconutError::DepositInfoNotFound)?
|
||||
.value
|
||||
.as_ref();
|
||||
let deposit_info_plain = public_attributes_plain.get(1).cloned().unwrap_or_default();
|
||||
if deposit_info != deposit_info_plain {
|
||||
return Err(CoconutError::DifferentPublicAttributes(
|
||||
deposit_info.to_string(),
|
||||
deposit_info_plain,
|
||||
));
|
||||
if deposit_info != request.public_attributes_plain[1] {
|
||||
return Err(CoconutError::InconsistentDepositInfo {
|
||||
request: request.public_attributes_plain[1].clone(),
|
||||
on_chain: deposit_info,
|
||||
});
|
||||
}
|
||||
|
||||
let verification_key = identity::PublicKey::from_base58_string(
|
||||
&attributes
|
||||
.iter()
|
||||
.find(|tag| tag.key == DEPOSIT_IDENTITY_KEY)
|
||||
.ok_or(CoconutError::DepositVerifKeyNotFound)?
|
||||
.value,
|
||||
)?;
|
||||
// verify signature
|
||||
let x25519 = identity::PublicKey::from_base58_string(x25519_raw)?;
|
||||
let plaintext =
|
||||
BandwidthVoucher::signable_plaintext(&request.inner_sign_request, request.tx_hash);
|
||||
x25519.verify(plaintext, &request.signature)?;
|
||||
|
||||
let encryption_key = encryption::PublicKey::from_base58_string(
|
||||
&attributes
|
||||
.iter()
|
||||
.find(|tag| tag.key == DEPOSIT_ENCRYPTION_KEY)
|
||||
.ok_or(CoconutError::DepositEncrKeyNotFound)?
|
||||
.value,
|
||||
)?;
|
||||
|
||||
verification_key.verify(&message, &signature)?;
|
||||
|
||||
Ok(encryption_key)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::coconut::tests::tx_entry_fixture;
|
||||
use nym_coconut::{prepare_blind_sign, BlindSignRequest, Parameters};
|
||||
use crate::coconut::tests::{tx_entry_fixture, voucher_request_fixture};
|
||||
use cosmwasm_std::coin;
|
||||
use nym_api_requests::coconut::BlindSignRequestBody;
|
||||
use nym_coconut::BlindSignRequest;
|
||||
use nym_coconut_bandwidth_contract_common::events::DEPOSITED_FUNDS_EVENT_TYPE;
|
||||
use nym_config::defaults::VOUCHER_INFO;
|
||||
use nym_validator_client::nyxd::Hash;
|
||||
use nym_validator_client::nyxd::{Event, EventAttribute};
|
||||
use rand_07::rngs::OsRng;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[tokio::test]
|
||||
async fn extract_encryption_key_test() {
|
||||
let tx_hash =
|
||||
Hash::from_str("6B27412050B823E58BB38447D7870BBC8CBE3C51C905BEA89D459ACCDA80A00E")
|
||||
.unwrap();
|
||||
let mut tx_entry = tx_entry_fixture(&tx_hash.to_string());
|
||||
let params = Parameters::new(4).unwrap();
|
||||
let mut rng = OsRng;
|
||||
let voucher = BandwidthVoucher::new(
|
||||
¶ms,
|
||||
"1234".to_string(),
|
||||
VOUCHER_INFO.to_string(),
|
||||
tx_hash,
|
||||
identity::PrivateKey::from_base58_string(
|
||||
identity::KeyPair::new(&mut rng)
|
||||
.private_key()
|
||||
.to_base58_string(),
|
||||
)
|
||||
.unwrap(),
|
||||
encryption::PrivateKey::from_bytes(
|
||||
&encryption::KeyPair::new(&mut rng).private_key().to_bytes(),
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
let (_, blind_sign_req) = prepare_blind_sign(
|
||||
¶ms,
|
||||
&voucher.get_private_attributes(),
|
||||
&voucher.get_public_attributes(),
|
||||
)
|
||||
.unwrap();
|
||||
let signature = "2DHbEZ6pzToGpsAXJrqJi7Wj1pAXeT18283q2YEEyNH5gTymwRozWBdja6SMAVt1dyYmUnM4ZNhsJ4wxZyGh4Z6J".to_string();
|
||||
async fn validate_deposit_tx_test() {
|
||||
let (voucher, correct_request) = voucher_request_fixture(coin(1234, "unym"), None);
|
||||
|
||||
let req = BlindSignRequestBody::new(
|
||||
&blind_sign_req,
|
||||
tx_hash.to_string(),
|
||||
signature.clone(),
|
||||
&voucher.get_public_attributes(),
|
||||
vec![
|
||||
String::from("First wrong plain"),
|
||||
String::from("Second wrong plain"),
|
||||
],
|
||||
4,
|
||||
);
|
||||
let err = extract_encryption_key(&req, tx_entry.clone())
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
CoconutError::InconsistentPublicAttributes.to_string()
|
||||
);
|
||||
|
||||
let req = BlindSignRequestBody::new(
|
||||
&blind_sign_req,
|
||||
tx_hash.to_string(),
|
||||
String::from("Invalid signature"),
|
||||
&voucher.get_public_attributes(),
|
||||
voucher.get_public_attributes_plain(),
|
||||
4,
|
||||
);
|
||||
let err = extract_encryption_key(&req, tx_entry.clone())
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
assert!(matches!(
|
||||
err,
|
||||
CoconutError::Ed25519ParseError(
|
||||
nym_crypto::asymmetric::identity::Ed25519RecoveryError::MalformedSignatureString { .. }
|
||||
)
|
||||
));
|
||||
|
||||
let correct_request = BlindSignRequestBody::new(
|
||||
&blind_sign_req,
|
||||
tx_hash.to_string(),
|
||||
signature.clone(),
|
||||
&voucher.get_public_attributes(),
|
||||
voucher.get_public_attributes_plain(),
|
||||
4,
|
||||
);
|
||||
let mut tx_entry = tx_entry_fixture(correct_request.tx_hash);
|
||||
let good_deposit_attribute = EventAttribute {
|
||||
key: DEPOSIT_VALUE.to_string(),
|
||||
value: correct_request.public_attributes_plain[0].clone(),
|
||||
index: false,
|
||||
};
|
||||
let good_info_attribute = EventAttribute {
|
||||
key: DEPOSIT_INFO.to_string(),
|
||||
value: correct_request.public_attributes_plain[1].clone(),
|
||||
index: false,
|
||||
};
|
||||
let good_identity_key_attribute = EventAttribute {
|
||||
key: DEPOSIT_IDENTITY_KEY.to_string(),
|
||||
value: "2eSxwquNJb2nZTEW5p4rbqjHfBaz9UaNhjHHiexPN4He".to_string(),
|
||||
index: false,
|
||||
};
|
||||
let good_encryption_key_attribute = EventAttribute {
|
||||
key: DEPOSIT_ENCRYPTION_KEY.to_string(),
|
||||
value: "2eSxwquNJb2nZTEW5p4rbqjHfBaz9UaNhjHHiexPN4He".to_string(),
|
||||
index: false,
|
||||
};
|
||||
|
||||
tx_entry.tx_result.events.push(Event {
|
||||
kind: format!("wasm-{}", DEPOSITED_FUNDS_EVENT_TYPE),
|
||||
attributes: vec![],
|
||||
});
|
||||
let err = extract_encryption_key(&correct_request, tx_entry.clone())
|
||||
let err = validate_deposit_tx(&correct_request, tx_entry.clone())
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert_eq!(
|
||||
@@ -191,26 +114,34 @@ mod test {
|
||||
CoconutError::DepositValueNotFound.to_string(),
|
||||
);
|
||||
|
||||
tx_entry.tx_result.events.get_mut(0).unwrap().attributes = vec![EventAttribute {
|
||||
key: DEPOSIT_VALUE.parse().unwrap(),
|
||||
value: "10".parse().unwrap(),
|
||||
index: false,
|
||||
}];
|
||||
let err = extract_encryption_key(&correct_request, tx_entry.clone())
|
||||
tx_entry.tx_result.events.get_mut(0).unwrap().attributes = vec![
|
||||
EventAttribute {
|
||||
key: DEPOSIT_VALUE.parse().unwrap(),
|
||||
value: "10".parse().unwrap(),
|
||||
index: false,
|
||||
},
|
||||
good_info_attribute.clone(),
|
||||
good_identity_key_attribute.clone(),
|
||||
good_encryption_key_attribute.clone(),
|
||||
];
|
||||
let err = validate_deposit_tx(&correct_request, tx_entry.clone())
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
CoconutError::DifferentPublicAttributes("10".to_string(), "1234".to_string())
|
||||
.to_string(),
|
||||
CoconutError::InconsistentDepositValue {
|
||||
request: "1234".to_string(),
|
||||
on_chain: "10".to_string()
|
||||
}
|
||||
.to_string()
|
||||
);
|
||||
|
||||
tx_entry.tx_result.events.get_mut(0).unwrap().attributes = vec![EventAttribute {
|
||||
key: DEPOSIT_VALUE.parse().unwrap(),
|
||||
value: "1234".parse().unwrap(),
|
||||
index: false,
|
||||
}];
|
||||
let err = extract_encryption_key(&correct_request, tx_entry.clone())
|
||||
tx_entry.tx_result.events.get_mut(0).unwrap().attributes = vec![
|
||||
good_deposit_attribute.clone(),
|
||||
good_identity_key_attribute.clone(),
|
||||
good_encryption_key_attribute.clone(),
|
||||
];
|
||||
let err = validate_deposit_tx(&correct_request, tx_entry.clone())
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert_eq!(
|
||||
@@ -219,42 +150,33 @@ mod test {
|
||||
);
|
||||
|
||||
tx_entry.tx_result.events.get_mut(0).unwrap().attributes = vec![
|
||||
EventAttribute {
|
||||
key: DEPOSIT_VALUE.parse().unwrap(),
|
||||
value: "1234".parse().unwrap(),
|
||||
index: false,
|
||||
},
|
||||
good_deposit_attribute.clone(),
|
||||
EventAttribute {
|
||||
key: DEPOSIT_INFO.parse().unwrap(),
|
||||
value: "bandwidth deposit info".parse().unwrap(),
|
||||
index: false,
|
||||
},
|
||||
good_identity_key_attribute.clone(),
|
||||
good_encryption_key_attribute.clone(),
|
||||
];
|
||||
let err = extract_encryption_key(&correct_request, tx_entry.clone())
|
||||
let err = validate_deposit_tx(&correct_request, tx_entry.clone())
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
CoconutError::DifferentPublicAttributes(
|
||||
"bandwidth deposit info".to_string(),
|
||||
VOUCHER_INFO.to_string(),
|
||||
)
|
||||
CoconutError::InconsistentDepositInfo {
|
||||
on_chain: "bandwidth deposit info".to_string(),
|
||||
request: VOUCHER_INFO.to_string(),
|
||||
}
|
||||
.to_string(),
|
||||
);
|
||||
|
||||
tx_entry.tx_result.events.get_mut(0).unwrap().attributes = vec![
|
||||
EventAttribute {
|
||||
key: DEPOSIT_VALUE.parse().unwrap(),
|
||||
value: "1234".parse().unwrap(),
|
||||
index: false,
|
||||
},
|
||||
EventAttribute {
|
||||
key: DEPOSIT_INFO.parse().unwrap(),
|
||||
value: VOUCHER_INFO.parse().unwrap(),
|
||||
index: false,
|
||||
},
|
||||
good_deposit_attribute.clone(),
|
||||
good_info_attribute.clone(),
|
||||
good_encryption_key_attribute.clone(),
|
||||
];
|
||||
let err = extract_encryption_key(&correct_request, tx_entry.clone())
|
||||
let err = validate_deposit_tx(&correct_request, tx_entry.clone())
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert_eq!(
|
||||
@@ -263,23 +185,16 @@ mod test {
|
||||
);
|
||||
|
||||
tx_entry.tx_result.events.get_mut(0).unwrap().attributes = vec![
|
||||
EventAttribute {
|
||||
key: DEPOSIT_VALUE.parse().unwrap(),
|
||||
value: "1234".parse().unwrap(),
|
||||
index: false,
|
||||
},
|
||||
EventAttribute {
|
||||
key: DEPOSIT_INFO.parse().unwrap(),
|
||||
value: VOUCHER_INFO.parse().unwrap(),
|
||||
index: false,
|
||||
},
|
||||
good_deposit_attribute.clone(),
|
||||
good_info_attribute.clone(),
|
||||
EventAttribute {
|
||||
key: DEPOSIT_IDENTITY_KEY.parse().unwrap(),
|
||||
value: "verification key".parse().unwrap(),
|
||||
index: false,
|
||||
},
|
||||
good_encryption_key_attribute.clone(),
|
||||
];
|
||||
let err = extract_encryption_key(&correct_request, tx_entry.clone())
|
||||
let err = validate_deposit_tx(&correct_request, tx_entry.clone())
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
@@ -291,16 +206,8 @@ mod test {
|
||||
));
|
||||
|
||||
tx_entry.tx_result.events.get_mut(0).unwrap().attributes = vec![
|
||||
EventAttribute {
|
||||
key: DEPOSIT_VALUE.parse().unwrap(),
|
||||
value: "1234".parse().unwrap(),
|
||||
index: false,
|
||||
},
|
||||
EventAttribute {
|
||||
key: DEPOSIT_INFO.parse().unwrap(),
|
||||
value: VOUCHER_INFO.parse().unwrap(),
|
||||
index: false,
|
||||
},
|
||||
good_deposit_attribute.clone(),
|
||||
good_info_attribute.clone(),
|
||||
EventAttribute {
|
||||
key: DEPOSIT_IDENTITY_KEY.parse().unwrap(),
|
||||
value: "2eSxwquNJb2nZTEW5p4rbqjHfBaz9UaNhjHHiexPN4He"
|
||||
@@ -309,7 +216,7 @@ mod test {
|
||||
index: false,
|
||||
},
|
||||
];
|
||||
let err = extract_encryption_key(&correct_request, tx_entry.clone())
|
||||
let err = validate_deposit_tx(&correct_request, tx_entry.clone())
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert_eq!(
|
||||
@@ -318,16 +225,8 @@ mod test {
|
||||
);
|
||||
|
||||
tx_entry.tx_result.events.get_mut(0).unwrap().attributes = vec![
|
||||
EventAttribute {
|
||||
key: DEPOSIT_VALUE.parse().unwrap(),
|
||||
value: "1234".parse().unwrap(),
|
||||
index: false,
|
||||
},
|
||||
EventAttribute {
|
||||
key: DEPOSIT_INFO.parse().unwrap(),
|
||||
value: VOUCHER_INFO.parse().unwrap(),
|
||||
index: false,
|
||||
},
|
||||
good_deposit_attribute.clone(),
|
||||
good_info_attribute.clone(),
|
||||
EventAttribute {
|
||||
key: DEPOSIT_IDENTITY_KEY.parse().unwrap(),
|
||||
value: "6EJGMdEq7t8Npz54uPkftGsdmj7DKntLVputAnDfVZB2"
|
||||
@@ -341,29 +240,16 @@ mod test {
|
||||
index: false,
|
||||
},
|
||||
];
|
||||
let err = extract_encryption_key(&correct_request, tx_entry.clone())
|
||||
let err = validate_deposit_tx(&correct_request, tx_entry.clone())
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
assert!(matches!(
|
||||
err,
|
||||
CoconutError::X25519ParseError(
|
||||
nym_crypto::asymmetric::encryption::KeyRecoveryError::MalformedPublicKeyString { .. }
|
||||
)
|
||||
));
|
||||
assert!(matches!(err, CoconutError::SignatureVerificationError(..)));
|
||||
|
||||
let expected_encryption_key = "HxnTpWTkgigSTAysVKLE8pEiUULHdTT1BxFfzfJvQRi6";
|
||||
tx_entry.tx_result.events.get_mut(0).unwrap().attributes = vec![
|
||||
EventAttribute {
|
||||
key: DEPOSIT_VALUE.parse().unwrap(),
|
||||
value: "1234".parse().unwrap(),
|
||||
index: false,
|
||||
},
|
||||
EventAttribute {
|
||||
key: DEPOSIT_INFO.parse().unwrap(),
|
||||
value: VOUCHER_INFO.parse().unwrap(),
|
||||
index: false,
|
||||
},
|
||||
good_deposit_attribute.clone(),
|
||||
good_info_attribute.clone(),
|
||||
EventAttribute {
|
||||
key: DEPOSIT_IDENTITY_KEY.parse().unwrap(),
|
||||
value: "6EJGMdEq7t8Npz54uPkftGsdmj7DKntLVputAnDfVZB2"
|
||||
@@ -377,7 +263,7 @@ mod test {
|
||||
index: false,
|
||||
},
|
||||
];
|
||||
let err = extract_encryption_key(&correct_request, tx_entry.clone())
|
||||
let err = validate_deposit_tx(&correct_request, tx_entry.clone())
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert_eq!(
|
||||
@@ -414,28 +300,21 @@ mod test {
|
||||
66, 137, 17, 32,
|
||||
])
|
||||
.unwrap();
|
||||
|
||||
let correct_request = BlindSignRequestBody::new(
|
||||
&blind_sign_req,
|
||||
"7C41AF8266D91DE55E1C8F4712E6A952A165ED3D8C27C7B00428CBD0DE00A52B".to_string(),
|
||||
"gSFgpma5GAVMcsmZwKieqGNHNd3dPzcfa8eT2Qn2LoBccSeyiJdphREbNrkuh5XWxMe2hUsranaYzLro48L9Qhd".to_string(),
|
||||
&voucher.get_public_attributes(),
|
||||
blind_sign_req,
|
||||
"7C41AF8266D91DE55E1C8F4712E6A952A165ED3D8C27C7B00428CBD0DE00A52B"
|
||||
.parse()
|
||||
.unwrap(),
|
||||
"3vUCc6MCN5AC2LNgDYjRB1QeErZSN1S8f6K14JHjpUcKWXbjGYFExA8DbwQQBki9gyUqrpBF94Drttb4eMcGQXkp".parse().unwrap(),
|
||||
voucher.get_public_attributes_plain(),
|
||||
4,
|
||||
);
|
||||
tx_entry.tx_result.events.get_mut(0).unwrap().attributes = vec![
|
||||
EventAttribute {
|
||||
key: DEPOSIT_VALUE.parse().unwrap(),
|
||||
value: "1234".parse().unwrap(),
|
||||
index: false,
|
||||
},
|
||||
EventAttribute {
|
||||
key: DEPOSIT_INFO.parse().unwrap(),
|
||||
value: VOUCHER_INFO.parse().unwrap(),
|
||||
index: false,
|
||||
},
|
||||
good_deposit_attribute.clone(),
|
||||
good_info_attribute.clone(),
|
||||
EventAttribute {
|
||||
key: DEPOSIT_IDENTITY_KEY.parse().unwrap(),
|
||||
value: "64auwDkWan7R8yH1Mwe9dS4qXgrDBCUNDg3Q4KFnd2P5"
|
||||
value: "3xoM5GmUSq7YW4YNQrax1fEFLw1GbZozxe6UUoJcrqLG"
|
||||
.parse()
|
||||
.unwrap(),
|
||||
index: false,
|
||||
@@ -448,9 +327,7 @@ mod test {
|
||||
index: false,
|
||||
},
|
||||
];
|
||||
let encryption_key = extract_encryption_key(&correct_request, tx_entry.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(encryption_key.to_base58_string(), expected_encryption_key);
|
||||
let res = validate_deposit_tx(&correct_request, tx_entry.clone()).await;
|
||||
assert!(res.is_ok())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ use nym_dkg::bte::setup;
|
||||
use nym_dkg::Dealing;
|
||||
use rand::RngCore;
|
||||
use std::collections::VecDeque;
|
||||
use zeroize::Zeroize;
|
||||
|
||||
pub(crate) async fn dealing_exchange(
|
||||
dkg_client: &DkgClient,
|
||||
@@ -39,16 +40,19 @@ pub(crate) async fn dealing_exchange(
|
||||
.keys()
|
||||
.position(|node_index| *node_index == dealer_index);
|
||||
|
||||
let prior_resharing_secrets = if let Some(sk) = state.coconut_secret_key().await {
|
||||
let prior_resharing_secrets = if let Some(mut keypair) = state.take_coconut_keypair().await {
|
||||
// Double check that we are in resharing mode
|
||||
if resharing {
|
||||
let (x, mut scalars) = sk.into_raw();
|
||||
if scalars.len() + 1 != TOTAL_DEALINGS {
|
||||
let sk = keypair.secret_key();
|
||||
if sk.size() + 1 != TOTAL_DEALINGS {
|
||||
return Err(CoconutError::CorruptedCoconutKeyPair);
|
||||
}
|
||||
|
||||
let (x, mut scalars) = sk.into_raw();
|
||||
|
||||
// We can now erase the keypair from memory
|
||||
debug!("Removing coconut keypair from memory");
|
||||
state.set_coconut_keypair(None).await;
|
||||
keypair.zeroize();
|
||||
scalars.push(x);
|
||||
scalars
|
||||
} else {
|
||||
|
||||
@@ -6,7 +6,6 @@ use crate::coconut::error::CoconutError;
|
||||
use crate::coconut::keypair::KeyPair as CoconutKeyPair;
|
||||
use cosmwasm_std::Addr;
|
||||
use log::debug;
|
||||
use nym_coconut::SecretKey;
|
||||
use nym_coconut_dkg_common::dealer::DealerDetails;
|
||||
use nym_coconut_dkg_common::types::EpochState;
|
||||
use nym_dkg::bte::{keys::KeyPair as DkgKeyPair, PublicKey, PublicKeyWithProof};
|
||||
@@ -274,12 +273,15 @@ impl State {
|
||||
self.coconut_keypair.get().await.is_some()
|
||||
}
|
||||
|
||||
pub async fn coconut_secret_key(&self) -> Option<SecretKey> {
|
||||
self.coconut_keypair
|
||||
.get()
|
||||
.await
|
||||
.as_ref()
|
||||
.map(|kp| kp.secret_key())
|
||||
pub async fn take_coconut_keypair(&self) -> Option<nym_coconut::KeyPair> {
|
||||
self.coconut_keypair.take().await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub async fn coconut_keypair(
|
||||
&self,
|
||||
) -> tokio::sync::RwLockReadGuard<'_, Option<nym_coconut::KeyPair>> {
|
||||
self.coconut_keypair.get().await
|
||||
}
|
||||
|
||||
pub fn node_index(&self) -> Option<NodeIndex> {
|
||||
|
||||
@@ -6,16 +6,16 @@ use crate::coconut::dkg::complaints::ComplaintReason;
|
||||
use crate::coconut::dkg::state::{ConsistentState, State};
|
||||
use crate::coconut::error::CoconutError;
|
||||
use crate::coconut::helpers::accepted_vote_err;
|
||||
use crate::coconut::state::BANDWIDTH_CREDENTIAL_PARAMS;
|
||||
use cosmwasm_std::Addr;
|
||||
use cw3::{ProposalResponse, Status};
|
||||
use log::debug;
|
||||
use nym_coconut::tests::helpers::transpose_matrix;
|
||||
use nym_coconut::{check_vk_pairing, Base58, KeyPair, Parameters, SecretKey, VerificationKey};
|
||||
use nym_coconut::{check_vk_pairing, Base58, KeyPair, SecretKey, VerificationKey};
|
||||
use nym_coconut_dkg_common::event_attributes::DKG_PROPOSAL_ID;
|
||||
use nym_coconut_dkg_common::types::{NodeIndex, TOTAL_DEALINGS};
|
||||
use nym_coconut_dkg_common::verification_key::owner_from_cosmos_msgs;
|
||||
use nym_coconut_interface::KeyPair as CoconutKeyPair;
|
||||
use nym_credentials::coconut::bandwidth::{PRIVATE_ATTRIBUTES, PUBLIC_ATTRIBUTES};
|
||||
use nym_dkg::bte::{decrypt_share, setup};
|
||||
use nym_dkg::error::DkgError;
|
||||
use nym_dkg::{combine_shares, try_recover_verification_keys, Dealing, Threshold};
|
||||
@@ -140,7 +140,6 @@ fn derive_partial_keypair(
|
||||
}
|
||||
state.set_recovered_vks(recovered_vks);
|
||||
|
||||
let params = Parameters::new(PUBLIC_ATTRIBUTES + PRIVATE_ATTRIBUTES)?;
|
||||
let x = scalars.pop().ok_or(CoconutError::DkgError(
|
||||
DkgError::NotEnoughDealingsAvailable {
|
||||
available: 0,
|
||||
@@ -148,7 +147,7 @@ fn derive_partial_keypair(
|
||||
},
|
||||
))?;
|
||||
let sk = SecretKey::create_from_raw(x, scalars);
|
||||
let vk = sk.verification_key(¶ms);
|
||||
let vk = sk.verification_key(&BANDWIDTH_CREDENTIAL_PARAMS);
|
||||
|
||||
Ok(CoconutKeyPair::from_keys(sk, vk))
|
||||
}
|
||||
@@ -234,7 +233,7 @@ pub(crate) async fn verification_key_validation(
|
||||
.map(|recovered_vk| recovered_vk.recovered_partials.clone())
|
||||
.collect();
|
||||
let recovered_partials = transpose_matrix(recovered_partials);
|
||||
let params = Parameters::new(PUBLIC_ATTRIBUTES + PRIVATE_ATTRIBUTES)?;
|
||||
let params = &BANDWIDTH_CREDENTIAL_PARAMS;
|
||||
for contract_share in vk_shares {
|
||||
if let Some(proposal_id) = proposal_ids.get(&contract_share.owner).copied() {
|
||||
match VerificationKey::try_from_bs58(contract_share.share) {
|
||||
@@ -243,7 +242,7 @@ pub(crate) async fn verification_key_validation(
|
||||
.iter()
|
||||
.position(|node_index| contract_share.node_index == *node_index)
|
||||
{
|
||||
let ret = if !check_vk_pairing(¶ms, &recovered_partials[idx], &vk) {
|
||||
let ret = if !check_vk_pairing(params, &recovered_partials[idx], &vk) {
|
||||
debug!(
|
||||
"Voting NO to proposal {} because of failed VK pairing",
|
||||
proposal_id
|
||||
@@ -836,16 +835,17 @@ pub(crate) mod tests {
|
||||
for (_, state) in clients_and_states.iter_mut() {
|
||||
state.set_was_in_progress();
|
||||
}
|
||||
let params = Parameters::new(4).unwrap();
|
||||
|
||||
let mut vks = vec![];
|
||||
let mut indices = vec![];
|
||||
for (_, state) in clients_and_states.iter() {
|
||||
let vk = state
|
||||
.coconut_secret_key()
|
||||
.coconut_keypair()
|
||||
.await
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.verification_key(¶ms);
|
||||
.verification_key()
|
||||
.clone();
|
||||
let index = state.node_index().unwrap();
|
||||
vks.push(vk);
|
||||
indices.push(index);
|
||||
@@ -933,10 +933,12 @@ pub(crate) mod tests {
|
||||
let mut indices = vec![];
|
||||
for (_, state) in clients_and_states.iter() {
|
||||
let vk = state
|
||||
.coconut_secret_key()
|
||||
.coconut_keypair()
|
||||
.await
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.verification_key(¶ms);
|
||||
.verification_key()
|
||||
.clone();
|
||||
let index = state.node_index().unwrap();
|
||||
vks.push(vk);
|
||||
indices.push(index);
|
||||
@@ -1047,16 +1049,16 @@ pub(crate) mod tests {
|
||||
}
|
||||
|
||||
// DKG in reshare mode
|
||||
let params = Parameters::new(4).unwrap();
|
||||
|
||||
let mut vks = vec![];
|
||||
let mut indices = vec![];
|
||||
for (_, state) in clients_and_states.iter() {
|
||||
let vk = state
|
||||
.coconut_secret_key()
|
||||
.coconut_keypair()
|
||||
.await
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.verification_key(¶ms);
|
||||
.verification_key()
|
||||
.clone();
|
||||
let index = state.node_index().unwrap();
|
||||
vks.push(vk);
|
||||
indices.push(index);
|
||||
@@ -1124,10 +1126,12 @@ pub(crate) mod tests {
|
||||
let mut indices = vec![];
|
||||
for (_, state) in clients_and_states.iter() {
|
||||
let vk = state
|
||||
.coconut_secret_key()
|
||||
.coconut_keypair()
|
||||
.await
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.verification_key(¶ms);
|
||||
.verification_key()
|
||||
.clone();
|
||||
let index = state.node_index().unwrap();
|
||||
vks.push(vk);
|
||||
indices.push(index);
|
||||
|
||||
@@ -13,7 +13,7 @@ use nym_crypto::asymmetric::{
|
||||
};
|
||||
use nym_dkg::error::DkgError;
|
||||
use nym_validator_client::coconut::CoconutApiError;
|
||||
use nym_validator_client::nyxd::error::NyxdError;
|
||||
use nym_validator_client::nyxd::error::{NyxdError, TendermintError};
|
||||
|
||||
use crate::node_status_api::models::NymApiStorageError;
|
||||
|
||||
@@ -30,82 +30,100 @@ pub enum CoconutError {
|
||||
#[error(transparent)]
|
||||
SerdeJsonError(#[from] serde_json::Error),
|
||||
|
||||
#[error("Could not parse Ed25519 data - {0}")]
|
||||
#[error("could not parse Ed25519 data: {0}")]
|
||||
Ed25519ParseError(#[from] Ed25519RecoveryError),
|
||||
|
||||
#[error("Could not parse X25519 data - {0}")]
|
||||
#[error("could not parse X25519 data: {0}")]
|
||||
X25519ParseError(#[from] KeyRecoveryError),
|
||||
|
||||
#[error("Could not parse tx hash in request body")]
|
||||
TxHashParseError,
|
||||
#[error("could not parse tx hash in request body: {source}")]
|
||||
TxHashParseError {
|
||||
#[source]
|
||||
source: TendermintError,
|
||||
},
|
||||
|
||||
#[error("Nyxd error - {0}")]
|
||||
#[error("could not get transaction details for '{tx_hash}': {source}")]
|
||||
TxRetrievalFailure {
|
||||
tx_hash: String,
|
||||
#[source]
|
||||
source: NyxdError,
|
||||
},
|
||||
|
||||
#[error("nyxd error: {0}")]
|
||||
NyxdError(#[from] NyxdError),
|
||||
|
||||
#[error("Validator client error - {0}")]
|
||||
#[error("validator client error: {0}")]
|
||||
ValidatorClientError(#[from] nym_validator_client::ValidatorClientError),
|
||||
|
||||
#[error("Coconut internal error - {0}")]
|
||||
#[error("coconut internal error: {0}")]
|
||||
CoconutInternalError(#[from] nym_coconut::CoconutError),
|
||||
|
||||
#[error("Could not find a deposit event in the transaction provided")]
|
||||
#[error("could not find a deposit event in the transaction provided")]
|
||||
DepositEventNotFound,
|
||||
|
||||
#[error("Could not find the deposit value in the event")]
|
||||
#[error("could not find the deposit value in the event")]
|
||||
DepositValueNotFound,
|
||||
|
||||
#[error("Could not find the deposit info in the event")]
|
||||
#[error("could not find the deposit info in the event")]
|
||||
DepositInfoNotFound,
|
||||
|
||||
#[error("Could not find the verification key in the event")]
|
||||
#[error("could not find the verification key in the event")]
|
||||
DepositVerifKeyNotFound,
|
||||
|
||||
#[error("Could not find the encryption key in the event")]
|
||||
#[error("could not find the encryption key in the event")]
|
||||
DepositEncrKeyNotFound,
|
||||
|
||||
#[error("Signature didn't verify correctly")]
|
||||
#[error("signature didn't verify correctly")]
|
||||
SignatureVerificationError(#[from] SignatureError),
|
||||
|
||||
#[error("Inconsistent public attributes")]
|
||||
#[error("inconsistent public attributes")]
|
||||
InconsistentPublicAttributes,
|
||||
|
||||
#[error(
|
||||
"Public attributes in request differ from the ones in deposit - Expected {0}, got {1}"
|
||||
)]
|
||||
#[error("the provided deposit value is inconsistent. got '{request}' while the value on chain is '{on_chain}'")]
|
||||
InconsistentDepositValue { request: String, on_chain: String },
|
||||
|
||||
#[error("the provided deposit info is inconsistent. got '{request}' while the value on chain is '{on_chain}'")]
|
||||
InconsistentDepositInfo { request: String, on_chain: String },
|
||||
|
||||
#[error("public attributes in request differ from the ones in deposit: Expected {0}, got {1}")]
|
||||
DifferentPublicAttributes(String, String),
|
||||
|
||||
#[error("Error in coconut interface - {0}")]
|
||||
#[error("error in coconut interface: {0}")]
|
||||
CoconutInterfaceError(#[from] nym_coconut_interface::error::CoconutInterfaceError),
|
||||
|
||||
#[error("Storage error - {0}")]
|
||||
#[error("storage error: {0}")]
|
||||
StorageError(#[from] NymApiStorageError),
|
||||
|
||||
#[error("Credentials error - {0}")]
|
||||
#[error("credentials error: {0}")]
|
||||
CredentialsError(#[from] nym_credentials::error::Error),
|
||||
|
||||
#[error("Incorrect credential proposal description: {reason}")]
|
||||
#[error("incorrect credential proposal description: {reason}")]
|
||||
IncorrectProposal { reason: String },
|
||||
|
||||
#[error("Invalid status of credential: {status}")]
|
||||
#[error("invalid status of credential: {status}")]
|
||||
InvalidCredentialStatus { status: String },
|
||||
|
||||
#[error("DKG error: {0}")]
|
||||
DkgError(#[from] DkgError),
|
||||
|
||||
#[error("Failed to recover assigned node index: {reason}")]
|
||||
#[error("failed to recover assigned node index: {reason}")]
|
||||
NodeIndexRecoveryError { reason: String },
|
||||
|
||||
#[error("Unrecoverable state: {reason}")]
|
||||
#[error("unrecoverable state: {reason}")]
|
||||
UnrecoverableState { reason: String },
|
||||
|
||||
#[error("DKG has not finished yet in order to derive the coconut key")]
|
||||
KeyPairNotDerivedYet,
|
||||
|
||||
#[error("The coconut keypair is corrupted")]
|
||||
#[error("the coconut keypair is corrupted")]
|
||||
CorruptedCoconutKeyPair,
|
||||
|
||||
#[error("There was a problem with the proposal id: {reason}")]
|
||||
#[error("there was a problem with the proposal id: {reason}")]
|
||||
ProposalIdError { reason: String },
|
||||
|
||||
// I guess we should make this one a bit more detailed
|
||||
#[error("the provided query arguments were invalid")]
|
||||
InvalidQueryArguments,
|
||||
}
|
||||
|
||||
impl<'r, 'o: 'r> Responder<'r, 'o> for CoconutError {
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::coconut::error::CoconutError;
|
||||
use crate::coconut::state::BANDWIDTH_CREDENTIAL_PARAMS;
|
||||
use nym_api_requests::coconut::BlindSignRequestBody;
|
||||
use nym_coconut::{BlindedSignature, SecretKey};
|
||||
use nym_validator_client::nyxd::error::NyxdError::AbciError;
|
||||
|
||||
// If the result is already established, the vote might be redundant and
|
||||
@@ -17,3 +20,18 @@ pub(crate) fn accepted_vote_err(ret: Result<(), CoconutError>) -> Result<(), Coc
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn blind_sign(
|
||||
request: &BlindSignRequestBody,
|
||||
signing_key: &SecretKey,
|
||||
) -> Result<BlindedSignature, CoconutError> {
|
||||
let public_attributes = request.public_attributes_hashed();
|
||||
let attributes_ref = public_attributes.iter().collect::<Vec<_>>();
|
||||
|
||||
Ok(nym_coconut_interface::blind_sign(
|
||||
&BANDWIDTH_CREDENTIAL_PARAMS,
|
||||
signing_key,
|
||||
&request.inner_sign_request,
|
||||
&attributes_ref,
|
||||
)?)
|
||||
}
|
||||
|
||||
@@ -16,6 +16,10 @@ impl KeyPair {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn take(&self) -> Option<nym_coconut::KeyPair> {
|
||||
self.inner.write().await.take()
|
||||
}
|
||||
|
||||
pub async fn get(&self) -> RwLockReadGuard<'_, Option<nym_coconut::KeyPair>> {
|
||||
self.inner.read().await
|
||||
}
|
||||
|
||||
+33
-278
@@ -3,39 +3,15 @@
|
||||
|
||||
use self::comm::APICommunicationChannel;
|
||||
use crate::coconut::client::Client as LocalClient;
|
||||
use crate::coconut::deposit::extract_encryption_key;
|
||||
use crate::coconut::error::{CoconutError, Result};
|
||||
use crate::coconut::helpers::accepted_vote_err;
|
||||
use crate::coconut::state::State;
|
||||
use crate::support::storage::NymApiStorage;
|
||||
use getset::{CopyGetters, Getters};
|
||||
use keypair::KeyPair;
|
||||
use nym_api_requests::coconut::{
|
||||
BlindSignRequestBody, BlindedSignatureResponse, VerifyCredentialBody, VerifyCredentialResponse,
|
||||
};
|
||||
use nym_coconut_bandwidth_contract_common::spend_credential::{
|
||||
funds_from_cosmos_msgs, SpendCredentialStatus,
|
||||
};
|
||||
use nym_coconut_dkg_common::types::EpochId;
|
||||
use nym_coconut_interface::KeyPair as CoconutKeyPair;
|
||||
use nym_coconut_interface::{
|
||||
Attribute, BlindSignRequest, BlindedSignature, Parameters, VerificationKey,
|
||||
};
|
||||
use nym_config::defaults::NYM_API_VERSION;
|
||||
use nym_credentials::coconut::params::{
|
||||
NymApiCredentialEncryptionAlgorithm, NymApiCredentialHkdfAlgorithm,
|
||||
};
|
||||
use nym_crypto::asymmetric::encryption;
|
||||
use nym_crypto::shared_key::new_ephemeral_shared_key;
|
||||
use nym_crypto::symmetric::stream_cipher;
|
||||
use nym_crypto::asymmetric::identity;
|
||||
use nym_validator_client::nym_api::routes::{BANDWIDTH, COCONUT_ROUTES};
|
||||
use nym_validator_client::nyxd::{Coin, Fee};
|
||||
use rand_07::rngs::OsRng;
|
||||
use rocket::fairing::AdHoc;
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::State as RocketState;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
pub(crate) mod api_routes;
|
||||
pub(crate) mod client;
|
||||
pub(crate) mod comm;
|
||||
mod deposit;
|
||||
@@ -43,263 +19,42 @@ pub(crate) mod dkg;
|
||||
pub(crate) mod error;
|
||||
pub(crate) mod helpers;
|
||||
pub(crate) mod keypair;
|
||||
pub(crate) mod state;
|
||||
pub(crate) mod storage;
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests;
|
||||
|
||||
pub struct State {
|
||||
client: Arc<dyn LocalClient + Send + Sync>,
|
||||
pub fn stage<C, D>(
|
||||
client: C,
|
||||
mix_denom: String,
|
||||
identity_keypair: identity::KeyPair,
|
||||
key_pair: KeyPair,
|
||||
comm_channel: Arc<dyn APICommunicationChannel + Send + Sync>,
|
||||
comm_channel: D,
|
||||
storage: NymApiStorage,
|
||||
rng: Arc<Mutex<OsRng>>,
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub(crate) fn new<C, D>(
|
||||
client: C,
|
||||
mix_denom: String,
|
||||
key_pair: KeyPair,
|
||||
comm_channel: D,
|
||||
storage: NymApiStorage,
|
||||
) -> Self
|
||||
where
|
||||
C: LocalClient + Send + Sync + 'static,
|
||||
D: APICommunicationChannel + Send + Sync + 'static,
|
||||
{
|
||||
let client = Arc::new(client);
|
||||
let comm_channel = Arc::new(comm_channel);
|
||||
let rng = Arc::new(Mutex::new(OsRng));
|
||||
Self {
|
||||
client,
|
||||
mix_denom,
|
||||
key_pair,
|
||||
comm_channel,
|
||||
storage,
|
||||
rng,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn signed_before(&self, tx_hash: &str) -> Result<Option<BlindedSignatureResponse>> {
|
||||
let ret = self.storage.get_blinded_signature_response(tx_hash).await?;
|
||||
if let Some(blinded_signature_reponse) = ret {
|
||||
Ok(Some(BlindedSignatureResponse::from_base58_string(
|
||||
blinded_signature_reponse,
|
||||
)?))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn encrypt_and_store(
|
||||
&self,
|
||||
tx_hash: &str,
|
||||
remote_key: &encryption::PublicKey,
|
||||
signature: &BlindedSignature,
|
||||
) -> Result<BlindedSignatureResponse> {
|
||||
let (keypair, shared_key) = {
|
||||
let mut rng = *self.rng.lock().await;
|
||||
new_ephemeral_shared_key::<
|
||||
NymApiCredentialEncryptionAlgorithm,
|
||||
NymApiCredentialHkdfAlgorithm,
|
||||
_,
|
||||
>(&mut rng, remote_key)
|
||||
};
|
||||
|
||||
let chunk_data = signature.to_bytes();
|
||||
|
||||
let zero_iv = stream_cipher::zero_iv::<NymApiCredentialEncryptionAlgorithm>();
|
||||
let encrypted_data = stream_cipher::encrypt::<NymApiCredentialEncryptionAlgorithm>(
|
||||
&shared_key,
|
||||
&zero_iv,
|
||||
&chunk_data,
|
||||
);
|
||||
|
||||
let response =
|
||||
BlindedSignatureResponse::new(encrypted_data, keypair.public_key().to_bytes());
|
||||
|
||||
// Atomically insert data, only if there is no signature stored in the meantime
|
||||
// This prevents race conditions on storing two signatures for the same deposit transaction
|
||||
if self
|
||||
.storage
|
||||
.insert_blinded_signature_response(tx_hash, &response.to_base58_string())
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
Ok(self
|
||||
.signed_before(tx_hash)
|
||||
.await?
|
||||
.expect("The signature was expected to be there"))
|
||||
} else {
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn verification_key(&self, epoch_id: EpochId) -> Result<VerificationKey> {
|
||||
self.comm_channel
|
||||
.aggregated_verification_key(epoch_id)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Getters, CopyGetters, Debug)]
|
||||
pub(crate) struct InternalSignRequest {
|
||||
// Total number of parameters to generate for
|
||||
#[getset(get_copy)]
|
||||
total_params: u32,
|
||||
#[getset(get)]
|
||||
public_attributes: Vec<Attribute>,
|
||||
#[getset(get)]
|
||||
blind_sign_request: BlindSignRequest,
|
||||
}
|
||||
|
||||
impl InternalSignRequest {
|
||||
pub fn new(
|
||||
total_params: u32,
|
||||
public_attributes: Vec<Attribute>,
|
||||
blind_sign_request: BlindSignRequest,
|
||||
) -> InternalSignRequest {
|
||||
InternalSignRequest {
|
||||
total_params,
|
||||
public_attributes,
|
||||
blind_sign_request,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stage<C, D>(
|
||||
client: C,
|
||||
mix_denom: String,
|
||||
key_pair: KeyPair,
|
||||
comm_channel: D,
|
||||
storage: NymApiStorage,
|
||||
) -> AdHoc
|
||||
where
|
||||
C: LocalClient + Send + Sync + 'static,
|
||||
D: APICommunicationChannel + Send + Sync + 'static,
|
||||
{
|
||||
let state = State::new(client, mix_denom, key_pair, comm_channel, storage);
|
||||
AdHoc::on_ignite("Internal Sign Request Stage", |rocket| async {
|
||||
rocket.manage(state).mount(
|
||||
// this format! is so ugly...
|
||||
format!("/{}/{}/{}", NYM_API_VERSION, COCONUT_ROUTES, BANDWIDTH),
|
||||
routes![post_blind_sign, verify_bandwidth_credential],
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn blind_sign(request: InternalSignRequest, key_pair: &CoconutKeyPair) -> Result<BlindedSignature> {
|
||||
let params = Parameters::new(request.total_params())?;
|
||||
Ok(nym_coconut_interface::blind_sign(
|
||||
¶ms,
|
||||
&key_pair.secret_key(),
|
||||
request.blind_sign_request(),
|
||||
request.public_attributes(),
|
||||
)?)
|
||||
}
|
||||
|
||||
#[post("/blind-sign", data = "<blind_sign_request_body>")]
|
||||
// Until we have serialization and deserialization traits we'll be using a crutch
|
||||
pub async fn post_blind_sign(
|
||||
blind_sign_request_body: Json<BlindSignRequestBody>,
|
||||
state: &RocketState<State>,
|
||||
) -> Result<Json<BlindedSignatureResponse>> {
|
||||
debug!("{:?}", blind_sign_request_body);
|
||||
if let Some(response) = state
|
||||
.signed_before(blind_sign_request_body.tx_hash())
|
||||
.await?
|
||||
{
|
||||
return Ok(Json(response));
|
||||
}
|
||||
let tx = state
|
||||
.client
|
||||
.get_tx(blind_sign_request_body.tx_hash())
|
||||
.await?;
|
||||
let encryption_key = extract_encryption_key(&blind_sign_request_body, tx).await?;
|
||||
let internal_request = InternalSignRequest::new(
|
||||
*blind_sign_request_body.total_params(),
|
||||
blind_sign_request_body.public_attributes(),
|
||||
blind_sign_request_body.blind_sign_request().clone(),
|
||||
) -> AdHoc
|
||||
where
|
||||
C: LocalClient + Send + Sync + 'static,
|
||||
D: APICommunicationChannel + Send + Sync + 'static,
|
||||
{
|
||||
let state = State::new(
|
||||
client,
|
||||
mix_denom,
|
||||
identity_keypair,
|
||||
key_pair,
|
||||
comm_channel,
|
||||
storage,
|
||||
);
|
||||
let blinded_signature = if let Some(keypair) = state.key_pair.get().await.as_ref() {
|
||||
blind_sign(internal_request, keypair)?
|
||||
} else {
|
||||
return Err(CoconutError::KeyPairNotDerivedYet);
|
||||
};
|
||||
|
||||
let response = state
|
||||
.encrypt_and_store(
|
||||
blind_sign_request_body.tx_hash(),
|
||||
&encryption_key,
|
||||
&blinded_signature,
|
||||
AdHoc::on_ignite("Internal Sign Request Stage", |rocket| async {
|
||||
rocket.manage(state).mount(
|
||||
// this format! is so ugly...
|
||||
format!("/{NYM_API_VERSION}/{COCONUT_ROUTES}/{BANDWIDTH}"),
|
||||
routes![
|
||||
api_routes::post_blind_sign,
|
||||
api_routes::verify_bandwidth_credential,
|
||||
api_routes::epoch_credentials,
|
||||
api_routes::issued_credential,
|
||||
api_routes::issued_credentials,
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(Json(response))
|
||||
}
|
||||
|
||||
#[post("/verify-bandwidth-credential", data = "<verify_credential_body>")]
|
||||
pub async fn verify_bandwidth_credential(
|
||||
verify_credential_body: Json<VerifyCredentialBody>,
|
||||
state: &RocketState<State>,
|
||||
) -> Result<Json<VerifyCredentialResponse>> {
|
||||
let proposal_id = *verify_credential_body.proposal_id();
|
||||
let proposal = state.client.get_proposal(proposal_id).await?;
|
||||
// Proposal description is the blinded serial number
|
||||
if !verify_credential_body
|
||||
.credential()
|
||||
.has_blinded_serial_number(&proposal.description)?
|
||||
{
|
||||
return Err(CoconutError::IncorrectProposal {
|
||||
reason: String::from("incorrect blinded serial number in description"),
|
||||
});
|
||||
}
|
||||
let proposed_release_funds =
|
||||
funds_from_cosmos_msgs(proposal.msgs).ok_or(CoconutError::IncorrectProposal {
|
||||
reason: String::from("action is not to release funds"),
|
||||
})?;
|
||||
// Credential has not been spent before, and is on its way of being spent
|
||||
let credential_status = state
|
||||
.client
|
||||
.get_spent_credential(verify_credential_body.credential().blinded_serial_number())
|
||||
.await?
|
||||
.spend_credential
|
||||
.ok_or(CoconutError::InvalidCredentialStatus {
|
||||
status: String::from("Inexistent"),
|
||||
})?
|
||||
.status();
|
||||
if credential_status != SpendCredentialStatus::InProgress {
|
||||
return Err(CoconutError::InvalidCredentialStatus {
|
||||
status: format!("{:?}", credential_status),
|
||||
});
|
||||
}
|
||||
let verification_key = state
|
||||
.verification_key(*verify_credential_body.credential().epoch_id())
|
||||
.await?;
|
||||
let mut vote_yes = verify_credential_body
|
||||
.credential()
|
||||
.verify(&verification_key);
|
||||
|
||||
vote_yes &= Coin::from(proposed_release_funds)
|
||||
== Coin::new(
|
||||
verify_credential_body.credential().voucher_value() as u128,
|
||||
state.mix_denom.clone(),
|
||||
);
|
||||
|
||||
// Vote yes or no on the proposal based on the verification result
|
||||
let ret = state
|
||||
.client
|
||||
.vote_proposal(
|
||||
proposal_id,
|
||||
vote_yes,
|
||||
Some(Fee::new_payer_granter_auto(
|
||||
None,
|
||||
None,
|
||||
Some(verify_credential_body.gateway_cosmos_addr().to_owned()),
|
||||
)),
|
||||
)
|
||||
.await;
|
||||
accepted_vote_err(ret)?;
|
||||
|
||||
Ok(Json(VerifyCredentialResponse::new(vote_yes)))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::coconut::client::Client as LocalClient;
|
||||
use crate::coconut::comm::APICommunicationChannel;
|
||||
use crate::coconut::deposit::validate_deposit_tx;
|
||||
use crate::coconut::error::Result;
|
||||
use crate::coconut::keypair::KeyPair;
|
||||
use crate::coconut::storage::CoconutStorageExt;
|
||||
use crate::support::storage::NymApiStorage;
|
||||
use lazy_static::lazy_static;
|
||||
use nym_api_requests::coconut::helpers::issued_credential_plaintext;
|
||||
use nym_api_requests::coconut::BlindSignRequestBody;
|
||||
use nym_coconut::Parameters;
|
||||
use nym_coconut_dkg_common::types::EpochId;
|
||||
use nym_coconut_interface::{BlindedSignature, VerificationKey};
|
||||
use nym_credentials::coconut::bandwidth::BandwidthVoucher;
|
||||
use nym_crypto::asymmetric::identity;
|
||||
use nym_validator_client::nyxd::{Hash, TxResponse};
|
||||
use std::sync::Arc;
|
||||
|
||||
// keep it as a global static due to relatively high cost of computing the curve points;
|
||||
// plus we expect all clients to use the same set of parameters
|
||||
//
|
||||
// future note: once we allow for credentials with variable number of attributes, just create Parameters(max_allowed_attributes)
|
||||
// and take as many hs elements as required (since they will match for all variants)
|
||||
lazy_static! {
|
||||
pub(crate) static ref BANDWIDTH_CREDENTIAL_PARAMS: Parameters =
|
||||
BandwidthVoucher::default_parameters();
|
||||
}
|
||||
|
||||
pub struct State {
|
||||
pub(crate) client: Arc<dyn LocalClient + Send + Sync>,
|
||||
pub(crate) mix_denom: String,
|
||||
pub(crate) coconut_keypair: KeyPair,
|
||||
pub(crate) identity_keypair: identity::KeyPair,
|
||||
pub(crate) comm_channel: Arc<dyn APICommunicationChannel + Send + Sync>,
|
||||
pub(crate) storage: NymApiStorage,
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub(crate) fn new<C, D>(
|
||||
client: C,
|
||||
mix_denom: String,
|
||||
identity_keypair: identity::KeyPair,
|
||||
key_pair: KeyPair,
|
||||
comm_channel: D,
|
||||
storage: NymApiStorage,
|
||||
) -> Self
|
||||
where
|
||||
C: LocalClient + Send + Sync + 'static,
|
||||
D: APICommunicationChannel + Send + Sync + 'static,
|
||||
{
|
||||
let client = Arc::new(client);
|
||||
let comm_channel = Arc::new(comm_channel);
|
||||
|
||||
Self {
|
||||
client,
|
||||
mix_denom,
|
||||
coconut_keypair: key_pair,
|
||||
identity_keypair,
|
||||
comm_channel,
|
||||
storage,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if this nym-api has already issued a credential for the provided deposit hash.
|
||||
/// If so, return it.
|
||||
pub async fn already_issued(&self, tx_hash: Hash) -> Result<Option<BlindedSignature>> {
|
||||
self.storage
|
||||
.get_issued_bandwidth_credential_by_hash(&tx_hash.to_string())
|
||||
.await?
|
||||
.map(|cred| cred.try_into())
|
||||
.transpose()
|
||||
}
|
||||
|
||||
pub async fn get_transaction(&self, tx_hash: Hash) -> Result<TxResponse> {
|
||||
self.client.get_tx(tx_hash).await
|
||||
}
|
||||
|
||||
pub async fn validate_request(
|
||||
&self,
|
||||
request: &BlindSignRequestBody,
|
||||
tx: TxResponse,
|
||||
) -> Result<()> {
|
||||
validate_deposit_tx(request, tx).await
|
||||
}
|
||||
|
||||
pub(crate) async fn sign_and_store_credential(
|
||||
&self,
|
||||
current_epoch: EpochId,
|
||||
request_body: BlindSignRequestBody,
|
||||
blinded_signature: &BlindedSignature,
|
||||
) -> Result<i64> {
|
||||
let encoded_commitments = request_body.encode_commitments();
|
||||
|
||||
let plaintext = issued_credential_plaintext(
|
||||
current_epoch as u32,
|
||||
request_body.tx_hash,
|
||||
blinded_signature,
|
||||
&encoded_commitments,
|
||||
&request_body.public_attributes_plain,
|
||||
);
|
||||
|
||||
let signature = self.identity_keypair.private_key().sign(plaintext);
|
||||
|
||||
// note: we have a UNIQUE constraint on the tx_hash column of the credential
|
||||
// and so if the api is processing request for the same hash at the same time,
|
||||
// only one of them will be successfully inserted to the database
|
||||
let credential_id = self
|
||||
.storage
|
||||
.store_issued_credential(
|
||||
current_epoch as u32,
|
||||
request_body.tx_hash,
|
||||
blinded_signature,
|
||||
signature,
|
||||
encoded_commitments,
|
||||
request_body.public_attributes_plain,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(credential_id)
|
||||
}
|
||||
|
||||
pub async fn store_issued_credential(
|
||||
&self,
|
||||
request_body: BlindSignRequestBody,
|
||||
blinded_signature: &BlindedSignature,
|
||||
) -> Result<()> {
|
||||
let current_epoch = self.comm_channel.current_epoch().await?;
|
||||
|
||||
// note: we have a UNIQUE constraint on the tx_hash column of the credential
|
||||
// and so if the api is processing request for the same hash at the same time,
|
||||
// only one of them will be successfully inserted to the database
|
||||
let credential_id = self
|
||||
.sign_and_store_credential(current_epoch, request_body, blinded_signature)
|
||||
.await?;
|
||||
self.storage
|
||||
.update_epoch_credentials_entry(current_epoch, credential_id)
|
||||
.await?;
|
||||
debug!("the stored credential has id {credential_id}");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn verification_key(&self, epoch_id: EpochId) -> Result<VerificationKey> {
|
||||
self.comm_channel
|
||||
.aggregated_verification_key(epoch_id)
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,381 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::coconut::storage::models::{EpochCredentials, IssuedCredential};
|
||||
use crate::support::storage::manager::StorageManager;
|
||||
use nym_coconut_dkg_common::types::EpochId;
|
||||
|
||||
#[async_trait]
|
||||
pub trait CoconutStorageManagerExt {
|
||||
/// Creates new encrypted blinded signature response entry for a given deposit tx hash.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `tx_hash`: hash of the deposit transaction.
|
||||
/// * `blinded_signature_response`: the encrypted blinded signature response.
|
||||
#[deprecated]
|
||||
async fn insert_blinded_signature_response(
|
||||
&self,
|
||||
_tx_hash: &str,
|
||||
_blinded_signature_response: &str,
|
||||
) -> Result<(), sqlx::Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tries to obtain encrypted blinded signature response for a given transaction hash.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `tx_hash`: transaction hash of the deposit.
|
||||
#[deprecated]
|
||||
async fn get_blinded_signature_response(
|
||||
&self,
|
||||
_tx_hash: &str,
|
||||
) -> Result<Option<String>, sqlx::Error> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Gets the information about all issued partial credentials in this (coconut) epoch.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `epoch_id`: Id of the (coconut) epoch in question.
|
||||
async fn get_epoch_credentials(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
) -> Result<Option<EpochCredentials>, sqlx::Error>;
|
||||
|
||||
/// Creates new entry for EpochCredentials for this (coconut) epoch.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `epoch_id`: Id of the (coconut) epoch in question.
|
||||
async fn create_epoch_credentials_entry(&self, epoch_id: EpochId) -> Result<(), sqlx::Error>;
|
||||
|
||||
/// Update the EpochCredentials by incrementing the total number of issued credentials,
|
||||
/// and setting `start_id` if unset (i.e. this is the first credential issued this epoch)
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `epoch_id`: Id of the (coconut) epoch in question.
|
||||
/// * `credential_id`: (database) Id of the coconut credential that triggered the update.
|
||||
async fn update_epoch_credentials_entry(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
credential_id: i64,
|
||||
) -> Result<(), sqlx::Error>;
|
||||
|
||||
/// Attempts to retrieve an issued credential from the data store.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `credential_id`: (database) id of the issued credential
|
||||
async fn get_issued_credential(
|
||||
&self,
|
||||
credential_id: i64,
|
||||
) -> Result<Option<IssuedCredential>, sqlx::Error>;
|
||||
|
||||
/// Attempts to retrieve an issued credential from the data store.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `tx_hash`: transaction hash of the deposit used in the issued bandwidth credential
|
||||
async fn get_issued_bandwidth_credential_by_hash(
|
||||
&self,
|
||||
tx_hash: &str,
|
||||
) -> Result<Option<IssuedCredential>, sqlx::Error>;
|
||||
|
||||
/// Store the provided issued credential information and return its (database) id.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `credential`: partial credential, alongside any data required for verification.
|
||||
async fn store_issued_credential(
|
||||
&self,
|
||||
epoch_id: u32,
|
||||
tx_hash: String,
|
||||
bs58_partial_credential: String,
|
||||
bs58_signature: String,
|
||||
joined_private_commitments: String,
|
||||
joined_public_attributes: String,
|
||||
) -> Result<i64, sqlx::Error>;
|
||||
|
||||
/// Attempts to retrieve issued credentials from the data store using provided ids.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `credential_ids`: (database) ids of the issued credentials
|
||||
async fn get_issued_credentials(
|
||||
&self,
|
||||
credential_ids: Vec<i64>,
|
||||
) -> Result<Vec<IssuedCredential>, sqlx::Error>;
|
||||
|
||||
/// Attempts to retrieve issued credentials from the data store using pagination specification.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `start_after`: the value preceding the first retrieved result
|
||||
/// * `limit`: the maximum number of entries to retrieve
|
||||
async fn get_issued_credentials_paged(
|
||||
&self,
|
||||
start_after: i64,
|
||||
limit: u32,
|
||||
) -> Result<Vec<IssuedCredential>, sqlx::Error>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl CoconutStorageManagerExt for StorageManager {
|
||||
/// Gets the information about all issued partial credentials in this (coconut) epoch.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `epoch_id`: Id of the (coconut) epoch in question.
|
||||
async fn get_epoch_credentials(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
) -> Result<Option<EpochCredentials>, sqlx::Error> {
|
||||
// even if we were changing epochs every second, it's rather impossible to overflow here
|
||||
// within any sane amount of time
|
||||
assert!(epoch_id <= u32::MAX as u64);
|
||||
let epoch_id_downcasted = epoch_id as u32;
|
||||
|
||||
sqlx::query_as!(
|
||||
EpochCredentials,
|
||||
r#"
|
||||
SELECT epoch_id as "epoch_id: u32", start_id, total_issued as "total_issued: u32"
|
||||
FROM epoch_credentials
|
||||
WHERE epoch_id = ?
|
||||
"#,
|
||||
epoch_id_downcasted
|
||||
)
|
||||
.fetch_optional(&self.connection_pool)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Creates new entry for EpochCredentials for this (coconut) epoch.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `epoch_id`: Id of the (coconut) epoch in question.
|
||||
async fn create_epoch_credentials_entry(&self, epoch_id: EpochId) -> Result<(), sqlx::Error> {
|
||||
// even if we were changing epochs every second, it's rather impossible to overflow here
|
||||
// within any sane amount of time
|
||||
assert!(epoch_id <= u32::MAX as u64);
|
||||
let epoch_id_downcasted = epoch_id as u32;
|
||||
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO epoch_credentials
|
||||
(epoch_id, start_id, total_issued)
|
||||
VALUES (?, ?, ?);
|
||||
"#,
|
||||
epoch_id_downcasted,
|
||||
-1,
|
||||
0
|
||||
)
|
||||
.execute(&self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// the logic in this function can be summarised with:
|
||||
// 1. get the current EpochCredentials for this epoch
|
||||
// 2. if it exists -> increment `total_issued`
|
||||
// 3. it has invalid (i.e. -1) `start_id` set it to the provided value
|
||||
// 4. if it didn't exist, create new entry
|
||||
/// Update the EpochCredentials by incrementing the total number of issued credentials,
|
||||
/// and setting `start_id` if unset (i.e. this is the first credential issued this epoch)
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `epoch_id`: Id of the (coconut) epoch in question.
|
||||
/// * `credential_id`: (database) Id of the coconut credential that triggered the update.
|
||||
async fn update_epoch_credentials_entry(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
credential_id: i64,
|
||||
) -> Result<(), sqlx::Error> {
|
||||
// even if we were changing epochs every second, it's rather impossible to overflow here
|
||||
// within any sane amount of time
|
||||
assert!(epoch_id <= u32::MAX as u64);
|
||||
let epoch_id_downcasted = epoch_id as u32;
|
||||
|
||||
// make the atomic transaction in case other tasks are attempting to use the pool
|
||||
let mut tx = self.connection_pool.begin().await?;
|
||||
|
||||
if let Some(existing) = sqlx::query_as!(
|
||||
EpochCredentials,
|
||||
r#"
|
||||
SELECT epoch_id as "epoch_id: u32", start_id, total_issued as "total_issued: u32"
|
||||
FROM epoch_credentials
|
||||
WHERE epoch_id = ?
|
||||
"#,
|
||||
epoch_id_downcasted
|
||||
)
|
||||
.fetch_optional(&mut tx)
|
||||
.await?
|
||||
{
|
||||
// the entry has existed before -> update it
|
||||
if existing.total_issued == 0 {
|
||||
// no credentials has been issued -> we have to set the `start_id`
|
||||
sqlx::query!(
|
||||
r#"
|
||||
UPDATE epoch_credentials
|
||||
SET total_issued = 1, start_id = ?
|
||||
WHERE epoch_id = ?
|
||||
"#,
|
||||
credential_id,
|
||||
epoch_id_downcasted
|
||||
)
|
||||
.execute(&mut tx)
|
||||
.await?;
|
||||
} else {
|
||||
// we have issued credentials in this epoch before -> just increment `total_issued`
|
||||
sqlx::query!(
|
||||
r#"
|
||||
UPDATE epoch_credentials
|
||||
SET total_issued = total_issued + 1
|
||||
WHERE epoch_id = ?
|
||||
"#,
|
||||
epoch_id_downcasted
|
||||
)
|
||||
.execute(&mut tx)
|
||||
.await?;
|
||||
}
|
||||
} else {
|
||||
// the entry has never been created -> probably some race condition; create it instead
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO epoch_credentials
|
||||
(epoch_id, start_id, total_issued)
|
||||
VALUES (?, ?, ?);
|
||||
"#,
|
||||
epoch_id_downcasted,
|
||||
credential_id,
|
||||
1
|
||||
)
|
||||
.execute(&mut tx)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// finally commit the transaction
|
||||
tx.commit().await
|
||||
}
|
||||
|
||||
/// Attempts to retrieve an issued credential from the data store.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `credential_id`: (database) id of the issued credential
|
||||
async fn get_issued_credential(
|
||||
&self,
|
||||
credential_id: i64,
|
||||
) -> Result<Option<IssuedCredential>, sqlx::Error> {
|
||||
sqlx::query_as!(
|
||||
IssuedCredential,
|
||||
r#"
|
||||
SELECT id, epoch_id as "epoch_id: u32", tx_hash, bs58_partial_credential, bs58_signature,joined_private_commitments, joined_public_attributes
|
||||
FROM issued_credential
|
||||
WHERE id = ?
|
||||
"#,
|
||||
credential_id
|
||||
)
|
||||
.fetch_optional(&self.connection_pool)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Attempts to retrieve an issued credential from the data store.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `tx_hash`: transaction hash of the deposit used in the issued bandwidth credential
|
||||
async fn get_issued_bandwidth_credential_by_hash(
|
||||
&self,
|
||||
tx_hash: &str,
|
||||
) -> Result<Option<IssuedCredential>, sqlx::Error> {
|
||||
sqlx::query_as!(
|
||||
IssuedCredential,
|
||||
r#"
|
||||
SELECT id, epoch_id as "epoch_id: u32", tx_hash, bs58_partial_credential, bs58_signature,joined_private_commitments, joined_public_attributes
|
||||
FROM issued_credential
|
||||
WHERE tx_hash = ?
|
||||
"#,
|
||||
tx_hash
|
||||
)
|
||||
.fetch_optional(&self.connection_pool)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Store the provided issued credential information and return its (database) id.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `credential`: partial credential, alongside any data required for verification.
|
||||
async fn store_issued_credential(
|
||||
&self,
|
||||
epoch_id: u32,
|
||||
tx_hash: String,
|
||||
bs58_partial_credential: String,
|
||||
bs58_signature: String,
|
||||
joined_private_commitments: String,
|
||||
joined_public_attributes: String,
|
||||
) -> Result<i64, sqlx::Error> {
|
||||
let row_id = sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO issued_credential
|
||||
(epoch_id, tx_hash, bs58_partial_credential, bs58_signature, joined_private_commitments, joined_public_attributes)
|
||||
VALUES
|
||||
(?, ?, ?, ?, ?, ?)
|
||||
"#,
|
||||
epoch_id, tx_hash, bs58_partial_credential, bs58_signature, joined_private_commitments, joined_public_attributes
|
||||
).execute(&self.connection_pool).await?.last_insert_rowid();
|
||||
|
||||
Ok(row_id)
|
||||
}
|
||||
|
||||
/// Attempts to retrieve issued credentials from the data store using provided ids.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `credential_ids`: (database) ids of the issued credentials
|
||||
async fn get_issued_credentials(
|
||||
&self,
|
||||
credential_ids: Vec<i64>,
|
||||
) -> Result<Vec<IssuedCredential>, sqlx::Error> {
|
||||
// that sucks : (
|
||||
// https://stackoverflow.com/a/70032524
|
||||
let params = format!("?{}", ", ?".repeat(credential_ids.len() - 1));
|
||||
let query_str = format!("SELECT * FROM issued_credential WHERE id IN ( {params} )");
|
||||
let mut query = sqlx::query_as(&query_str);
|
||||
for id in credential_ids {
|
||||
query = query.bind(id)
|
||||
}
|
||||
|
||||
query.fetch_all(&self.connection_pool).await
|
||||
}
|
||||
|
||||
/// Attempts to retrieve issued credentials from the data store using pagination specification.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `start_after`: the value preceding the first retrieved result
|
||||
/// * `limit`: the maximum number of entries to retrieve
|
||||
async fn get_issued_credentials_paged(
|
||||
&self,
|
||||
start_after: i64,
|
||||
limit: u32,
|
||||
) -> Result<Vec<IssuedCredential>, sqlx::Error> {
|
||||
sqlx::query_as!(
|
||||
IssuedCredential,
|
||||
r#"
|
||||
SELECT id, epoch_id as "epoch_id: u32", tx_hash, bs58_partial_credential, bs58_signature,joined_private_commitments, joined_public_attributes
|
||||
FROM issued_credential
|
||||
WHERE id > ?
|
||||
ORDER BY id
|
||||
LIMIT ?
|
||||
"#,
|
||||
start_after,
|
||||
limit
|
||||
)
|
||||
.fetch_all(&self.connection_pool)
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::coconut::storage::manager::CoconutStorageManagerExt;
|
||||
use crate::coconut::storage::models::{join_attributes, EpochCredentials, IssuedCredential};
|
||||
use crate::node_status_api::models::NymApiStorageError;
|
||||
use crate::support::storage::NymApiStorage;
|
||||
use nym_api_requests::coconut::models::Pagination;
|
||||
use nym_coconut::{Base58, BlindedSignature};
|
||||
use nym_coconut_dkg_common::types::EpochId;
|
||||
use nym_crypto::asymmetric::identity;
|
||||
use nym_validator_client::nyxd::Hash;
|
||||
|
||||
pub(crate) mod manager;
|
||||
pub(crate) mod models;
|
||||
|
||||
const DEFAULT_CREDENTIALS_PAGE_LIMIT: u32 = 100;
|
||||
|
||||
#[async_trait]
|
||||
pub trait CoconutStorageExt {
|
||||
async fn get_epoch_credentials(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
) -> Result<Option<EpochCredentials>, NymApiStorageError>;
|
||||
|
||||
async fn create_epoch_credentials_entry(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
) -> Result<(), NymApiStorageError>;
|
||||
|
||||
async fn update_epoch_credentials_entry(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
credential_id: i64,
|
||||
) -> Result<(), NymApiStorageError>;
|
||||
|
||||
async fn get_issued_credential(
|
||||
&self,
|
||||
credential_id: i64,
|
||||
) -> Result<Option<IssuedCredential>, NymApiStorageError>;
|
||||
|
||||
async fn get_issued_bandwidth_credential_by_hash(
|
||||
&self,
|
||||
tx_hash: &str,
|
||||
) -> Result<Option<IssuedCredential>, NymApiStorageError>;
|
||||
|
||||
async fn store_issued_credential(
|
||||
&self,
|
||||
epoch_id: u32,
|
||||
tx_hash: Hash,
|
||||
partial_credential: &BlindedSignature,
|
||||
signature: identity::Signature,
|
||||
private_commitments: Vec<String>,
|
||||
public_attributes: Vec<String>,
|
||||
) -> Result<i64, NymApiStorageError>;
|
||||
|
||||
async fn get_issued_credentials(
|
||||
&self,
|
||||
credential_ids: Vec<i64>,
|
||||
) -> Result<Vec<IssuedCredential>, NymApiStorageError>;
|
||||
|
||||
async fn get_issued_credentials_paged(
|
||||
&self,
|
||||
pagination: Pagination<i64>,
|
||||
) -> Result<Vec<IssuedCredential>, NymApiStorageError>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl CoconutStorageExt for NymApiStorage {
|
||||
async fn get_epoch_credentials(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
) -> Result<Option<EpochCredentials>, NymApiStorageError> {
|
||||
Ok(self.manager.get_epoch_credentials(epoch_id).await?)
|
||||
}
|
||||
|
||||
async fn create_epoch_credentials_entry(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
) -> Result<(), NymApiStorageError> {
|
||||
Ok(self
|
||||
.manager
|
||||
.create_epoch_credentials_entry(epoch_id)
|
||||
.await?)
|
||||
}
|
||||
|
||||
async fn update_epoch_credentials_entry(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
credential_id: i64,
|
||||
) -> Result<(), NymApiStorageError> {
|
||||
Ok(self
|
||||
.manager
|
||||
.update_epoch_credentials_entry(epoch_id, credential_id)
|
||||
.await?)
|
||||
}
|
||||
|
||||
async fn get_issued_credential(
|
||||
&self,
|
||||
credential_id: i64,
|
||||
) -> Result<Option<IssuedCredential>, NymApiStorageError> {
|
||||
Ok(self.manager.get_issued_credential(credential_id).await?)
|
||||
}
|
||||
|
||||
async fn get_issued_bandwidth_credential_by_hash(
|
||||
&self,
|
||||
tx_hash: &str,
|
||||
) -> Result<Option<IssuedCredential>, NymApiStorageError> {
|
||||
Ok(self
|
||||
.manager
|
||||
.get_issued_bandwidth_credential_by_hash(tx_hash)
|
||||
.await?)
|
||||
}
|
||||
|
||||
async fn store_issued_credential(
|
||||
&self,
|
||||
epoch_id: u32,
|
||||
tx_hash: Hash,
|
||||
partial_credential: &BlindedSignature,
|
||||
signature: identity::Signature,
|
||||
private_commitments: Vec<String>,
|
||||
public_attributes: Vec<String>,
|
||||
) -> Result<i64, NymApiStorageError> {
|
||||
Ok(self
|
||||
.manager
|
||||
.store_issued_credential(
|
||||
epoch_id,
|
||||
tx_hash.to_string(),
|
||||
partial_credential.to_bs58(),
|
||||
signature.to_base58_string(),
|
||||
join_attributes(private_commitments),
|
||||
join_attributes(public_attributes),
|
||||
)
|
||||
.await?)
|
||||
}
|
||||
|
||||
async fn get_issued_credentials(
|
||||
&self,
|
||||
credential_ids: Vec<i64>,
|
||||
) -> Result<Vec<IssuedCredential>, NymApiStorageError> {
|
||||
Ok(self.manager.get_issued_credentials(credential_ids).await?)
|
||||
}
|
||||
|
||||
async fn get_issued_credentials_paged(
|
||||
&self,
|
||||
pagination: Pagination<i64>,
|
||||
) -> Result<Vec<IssuedCredential>, NymApiStorageError> {
|
||||
// rows start at 1
|
||||
let start_after = pagination.last_key.unwrap_or(0);
|
||||
let limit = match pagination.limit {
|
||||
Some(v) => {
|
||||
if v == 0 || v > DEFAULT_CREDENTIALS_PAGE_LIMIT {
|
||||
DEFAULT_CREDENTIALS_PAGE_LIMIT
|
||||
} else {
|
||||
v
|
||||
}
|
||||
}
|
||||
None => DEFAULT_CREDENTIALS_PAGE_LIMIT,
|
||||
};
|
||||
|
||||
Ok(self
|
||||
.manager
|
||||
.get_issued_credentials_paged(start_after, limit)
|
||||
.await?)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::coconut::error::CoconutError;
|
||||
use nym_api_requests::coconut::models::{
|
||||
EpochCredentialsResponse, IssuedCredential as ApiIssuedCredential,
|
||||
IssuedCredentialInner as ApiIssuedCredentialInner,
|
||||
};
|
||||
use nym_api_requests::coconut::BlindedSignatureResponse;
|
||||
use nym_coconut::{Base58, BlindedSignature};
|
||||
use sqlx::FromRow;
|
||||
use std::fmt::Display;
|
||||
|
||||
pub struct EpochCredentials {
|
||||
pub epoch_id: u32,
|
||||
pub start_id: i64,
|
||||
pub total_issued: u32,
|
||||
}
|
||||
|
||||
impl From<EpochCredentials> for EpochCredentialsResponse {
|
||||
fn from(value: EpochCredentials) -> Self {
|
||||
let first_epoch_credential_id = if value.start_id == -1 {
|
||||
None
|
||||
} else {
|
||||
Some(value.start_id)
|
||||
};
|
||||
|
||||
EpochCredentialsResponse {
|
||||
epoch_id: value.epoch_id as u64,
|
||||
first_epoch_credential_id,
|
||||
total_issued: value.total_issued,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(FromRow)]
|
||||
pub struct IssuedCredential {
|
||||
pub id: i64,
|
||||
pub epoch_id: u32,
|
||||
pub tx_hash: String,
|
||||
|
||||
/// base58-encoded issued credential
|
||||
pub bs58_partial_credential: String,
|
||||
|
||||
/// base58-encoded signature on the issued credential (and the attributes)
|
||||
pub bs58_signature: String,
|
||||
|
||||
// i.e. "'attr1','attr2',..."
|
||||
pub joined_private_commitments: String,
|
||||
|
||||
// i.e. "'attr1','attr2',..."
|
||||
pub joined_public_attributes: String,
|
||||
}
|
||||
|
||||
impl TryFrom<IssuedCredential> for ApiIssuedCredentialInner {
|
||||
type Error = CoconutError;
|
||||
|
||||
fn try_from(value: IssuedCredential) -> Result<Self, Self::Error> {
|
||||
Ok(ApiIssuedCredentialInner {
|
||||
credential: ApiIssuedCredential {
|
||||
id: value.id,
|
||||
epoch_id: value.epoch_id,
|
||||
tx_hash: value
|
||||
.tx_hash
|
||||
.parse()
|
||||
.map_err(|source| CoconutError::TxHashParseError { source })?,
|
||||
blinded_partial_credential: BlindedSignature::try_from_bs58(
|
||||
value.bs58_partial_credential,
|
||||
)?,
|
||||
bs58_encoded_private_attributes_commitments: split_attributes(
|
||||
&value.joined_private_commitments,
|
||||
),
|
||||
public_attributes: split_attributes(&value.joined_public_attributes),
|
||||
},
|
||||
signature: value.bs58_signature.parse()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<IssuedCredential> for BlindedSignatureResponse {
|
||||
type Error = CoconutError;
|
||||
|
||||
fn try_from(value: IssuedCredential) -> Result<Self, Self::Error> {
|
||||
Ok(BlindedSignatureResponse {
|
||||
blinded_signature: BlindedSignature::try_from_bs58(value.bs58_partial_credential)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<IssuedCredential> for BlindedSignature {
|
||||
type Error = CoconutError;
|
||||
|
||||
fn try_from(value: IssuedCredential) -> Result<Self, Self::Error> {
|
||||
Ok(BlindedSignature::try_from_bs58(
|
||||
value.bs58_partial_credential,
|
||||
)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl IssuedCredential {
|
||||
// safety: this should only ever be called on sanitized data from the database,
|
||||
// thus the unwraps are fine (if somebody manually entered their db file and modified it, it's on them)
|
||||
// pub fn private_attribute_commitments(&self) -> Vec<Scalar>
|
||||
// pub fn public_attributes(&self)
|
||||
}
|
||||
|
||||
pub fn join_attributes<I, M>(attrs: I) -> String
|
||||
where
|
||||
I: IntoIterator<Item = M>,
|
||||
M: Display,
|
||||
{
|
||||
// I could have used `attrs.into_iter().join(",")`,
|
||||
// but my IDE didn't like it (compiler was fine)
|
||||
itertools::Itertools::join(&mut attrs.into_iter(), ",")
|
||||
}
|
||||
|
||||
pub fn split_attributes(attrs: &str) -> Vec<String> {
|
||||
attrs.split(',').map(|s| s.to_string()).collect()
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::coconut::tests::{voucher_request_fixture, TestFixture};
|
||||
use cosmwasm_std::coin;
|
||||
use nym_api_requests::coconut::models::{
|
||||
EpochCredentialsResponse, IssuedCredentialResponse, IssuedCredentialsResponse, Pagination,
|
||||
};
|
||||
use nym_api_requests::coconut::CredentialsRequestBody;
|
||||
use nym_coconut::Base58;
|
||||
use nym_validator_client::nym_api::routes::{API_VERSION, BANDWIDTH, COCONUT_ROUTES};
|
||||
use rocket::http::Status;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
#[tokio::test]
|
||||
async fn epoch_credentials() {
|
||||
let route_epoch1 = format!("/{API_VERSION}/{COCONUT_ROUTES}/{BANDWIDTH}/epoch-credentials/1");
|
||||
let route_epoch2 = format!("/{API_VERSION}/{COCONUT_ROUTES}/{BANDWIDTH}/epoch-credentials/2");
|
||||
let route_epoch42 = format!("/{API_VERSION}/{COCONUT_ROUTES}/{BANDWIDTH}/epoch-credentials/42");
|
||||
|
||||
let test_fixture = TestFixture::new().await;
|
||||
|
||||
// initially we expect 0 issued
|
||||
let response = test_fixture.rocket.get(&route_epoch1).dispatch().await;
|
||||
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
let parsed_response: EpochCredentialsResponse =
|
||||
serde_json::from_str(&response.into_string().await.unwrap()).unwrap();
|
||||
|
||||
assert_eq!(parsed_response.epoch_id, 1);
|
||||
assert_eq!(parsed_response.total_issued, 0);
|
||||
assert_eq!(parsed_response.first_epoch_credential_id, None);
|
||||
|
||||
// get credential
|
||||
test_fixture.issue_dummy_credential().await;
|
||||
|
||||
// now there should be one
|
||||
let response = test_fixture.rocket.get(&route_epoch1).dispatch().await;
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
let parsed_response: EpochCredentialsResponse =
|
||||
serde_json::from_str(&response.into_string().await.unwrap()).unwrap();
|
||||
|
||||
assert_eq!(parsed_response.epoch_id, 1);
|
||||
assert_eq!(parsed_response.total_issued, 1);
|
||||
assert_eq!(parsed_response.first_epoch_credential_id, Some(1));
|
||||
|
||||
// and another
|
||||
test_fixture.issue_dummy_credential().await;
|
||||
|
||||
let response = test_fixture.rocket.get(&route_epoch1).dispatch().await;
|
||||
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
let parsed_response: EpochCredentialsResponse =
|
||||
serde_json::from_str(&response.into_string().await.unwrap()).unwrap();
|
||||
|
||||
// note that first epoch credential didn't change
|
||||
assert_eq!(parsed_response.epoch_id, 1);
|
||||
assert_eq!(parsed_response.total_issued, 2);
|
||||
assert_eq!(parsed_response.first_epoch_credential_id, Some(1));
|
||||
|
||||
test_fixture.set_epoch(2);
|
||||
|
||||
let response = test_fixture.rocket.get(&route_epoch2).dispatch().await;
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
let parsed_response: EpochCredentialsResponse =
|
||||
serde_json::from_str(&response.into_string().await.unwrap()).unwrap();
|
||||
|
||||
// note the epoch change
|
||||
assert_eq!(parsed_response.epoch_id, 2);
|
||||
assert_eq!(parsed_response.total_issued, 0);
|
||||
assert_eq!(parsed_response.first_epoch_credential_id, None);
|
||||
|
||||
test_fixture.issue_dummy_credential().await;
|
||||
|
||||
let response = test_fixture.rocket.get(&route_epoch2).dispatch().await;
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
let parsed_response: EpochCredentialsResponse =
|
||||
serde_json::from_str(&response.into_string().await.unwrap()).unwrap();
|
||||
|
||||
// note the epoch change
|
||||
assert_eq!(parsed_response.epoch_id, 2);
|
||||
assert_eq!(parsed_response.total_issued, 1);
|
||||
assert_eq!(parsed_response.first_epoch_credential_id, Some(3));
|
||||
|
||||
// random epoch in the future
|
||||
let response = test_fixture.rocket.get(&route_epoch42).dispatch().await;
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
let parsed_response: EpochCredentialsResponse =
|
||||
serde_json::from_str(&response.into_string().await.unwrap()).unwrap();
|
||||
assert_eq!(parsed_response.epoch_id, 42);
|
||||
assert_eq!(parsed_response.total_issued, 0);
|
||||
assert_eq!(parsed_response.first_epoch_credential_id, None);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn issued_credential() {
|
||||
fn route(id: i64) -> String {
|
||||
format!("/{API_VERSION}/{COCONUT_ROUTES}/{BANDWIDTH}/issued-credential/{id}")
|
||||
}
|
||||
|
||||
// let test_fixture = TestFixture::new()
|
||||
let hash1 = "6B27412050B823E58BB38447D7870BBC8CBE3C51C905BEA89D459ACCDA80A00E".to_string();
|
||||
let hash2 = "9F4DF28B36189B4410BC23D97FD757FC74B919122E80534CC2CA6F3D646F6518".to_string();
|
||||
|
||||
let (voucher1, req1) = voucher_request_fixture(coin(1234, "unym"), Some(hash1.clone()));
|
||||
let (voucher2, req2) = voucher_request_fixture(coin(1234, "unym"), Some(hash2.clone()));
|
||||
|
||||
let test_fixture = TestFixture::new().await;
|
||||
test_fixture.add_deposit_tx(&voucher1);
|
||||
test_fixture.add_deposit_tx(&voucher2);
|
||||
|
||||
// random credential that was never issued
|
||||
let response = test_fixture.rocket.get(route(42)).dispatch().await;
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
let parsed_response: IssuedCredentialResponse =
|
||||
serde_json::from_str(&response.into_string().await.unwrap()).unwrap();
|
||||
assert!(parsed_response.credential.is_none());
|
||||
|
||||
let cred1 = test_fixture.issue_credential(req1).await;
|
||||
|
||||
test_fixture.set_epoch(3);
|
||||
let cred2 = test_fixture.issue_credential(req2).await;
|
||||
|
||||
let response = test_fixture.rocket.get(route(1)).dispatch().await;
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
let parsed_response: IssuedCredentialResponse =
|
||||
serde_json::from_str(&response.into_string().await.unwrap()).unwrap();
|
||||
let issued1 = parsed_response.credential.unwrap();
|
||||
|
||||
let response = test_fixture.rocket.get(route(2)).dispatch().await;
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
let parsed_response: IssuedCredentialResponse =
|
||||
serde_json::from_str(&response.into_string().await.unwrap()).unwrap();
|
||||
let issued2 = parsed_response.credential.unwrap();
|
||||
|
||||
// TODO: currently we have no signature checks
|
||||
assert_eq!(1, issued1.credential.id);
|
||||
assert_eq!(1, issued1.credential.epoch_id);
|
||||
assert_eq!(voucher1.tx_hash(), issued1.credential.tx_hash);
|
||||
assert_eq!(
|
||||
cred1.to_bytes(),
|
||||
issued1.credential.blinded_partial_credential.to_bytes()
|
||||
);
|
||||
let cms: Vec<_> = voucher1
|
||||
.blind_sign_request()
|
||||
.get_private_attributes_pedersen_commitments()
|
||||
.iter()
|
||||
.map(|c| c.to_bs58())
|
||||
.collect();
|
||||
assert_eq!(
|
||||
cms,
|
||||
issued1
|
||||
.credential
|
||||
.bs58_encoded_private_attributes_commitments
|
||||
);
|
||||
assert_eq!(
|
||||
voucher1.get_public_attributes_plain(),
|
||||
issued1.credential.public_attributes
|
||||
);
|
||||
|
||||
assert_eq!(2, issued2.credential.id);
|
||||
assert_eq!(3, issued2.credential.epoch_id);
|
||||
assert_eq!(voucher2.tx_hash(), issued2.credential.tx_hash);
|
||||
assert_eq!(
|
||||
cred2.to_bytes(),
|
||||
issued2.credential.blinded_partial_credential.to_bytes()
|
||||
);
|
||||
let cms: Vec<_> = voucher2
|
||||
.blind_sign_request()
|
||||
.get_private_attributes_pedersen_commitments()
|
||||
.iter()
|
||||
.map(|c| c.to_bs58())
|
||||
.collect();
|
||||
assert_eq!(
|
||||
cms,
|
||||
issued2
|
||||
.credential
|
||||
.bs58_encoded_private_attributes_commitments
|
||||
);
|
||||
assert_eq!(
|
||||
voucher2.get_public_attributes_plain(),
|
||||
issued2.credential.public_attributes
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn issued_credentials() {
|
||||
let route = format!("/{API_VERSION}/{COCONUT_ROUTES}/{BANDWIDTH}/issued-credentials");
|
||||
|
||||
let test_fixture = TestFixture::new().await;
|
||||
|
||||
// issue some credentials
|
||||
for _ in 0..20 {
|
||||
test_fixture.issue_dummy_credential().await;
|
||||
}
|
||||
|
||||
let issued1 = test_fixture.issued_unchecked(1).await;
|
||||
let issued2 = test_fixture.issued_unchecked(2).await;
|
||||
let issued3 = test_fixture.issued_unchecked(3).await;
|
||||
let issued4 = test_fixture.issued_unchecked(4).await;
|
||||
let issued5 = test_fixture.issued_unchecked(5).await;
|
||||
let issued13 = test_fixture.issued_unchecked(13).await;
|
||||
|
||||
let response = test_fixture
|
||||
.rocket
|
||||
.post(&route)
|
||||
.json(&CredentialsRequestBody {
|
||||
credential_ids: vec![5],
|
||||
pagination: None,
|
||||
})
|
||||
.dispatch()
|
||||
.await;
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
let parsed_response: IssuedCredentialsResponse =
|
||||
serde_json::from_str(&response.into_string().await.unwrap()).unwrap();
|
||||
assert_eq!(parsed_response.credentials[&5], issued5);
|
||||
assert!(parsed_response.credentials.get(&13).is_none());
|
||||
|
||||
let response = test_fixture
|
||||
.rocket
|
||||
.post(&route)
|
||||
.json(&CredentialsRequestBody {
|
||||
credential_ids: vec![5, 13],
|
||||
pagination: None,
|
||||
})
|
||||
.dispatch()
|
||||
.await;
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
let parsed_response: IssuedCredentialsResponse =
|
||||
serde_json::from_str(&response.into_string().await.unwrap()).unwrap();
|
||||
assert_eq!(parsed_response.credentials[&5], issued5);
|
||||
assert_eq!(parsed_response.credentials[&13], issued13);
|
||||
|
||||
let response_paginated = test_fixture
|
||||
.rocket
|
||||
.post(&route)
|
||||
.json(&CredentialsRequestBody {
|
||||
credential_ids: vec![],
|
||||
pagination: Some(Pagination {
|
||||
last_key: None,
|
||||
limit: Some(2),
|
||||
}),
|
||||
})
|
||||
.dispatch()
|
||||
.await;
|
||||
assert_eq!(response_paginated.status(), Status::Ok);
|
||||
let parsed_response: IssuedCredentialsResponse =
|
||||
serde_json::from_str(&response_paginated.into_string().await.unwrap()).unwrap();
|
||||
|
||||
let mut expected = BTreeMap::new();
|
||||
expected.insert(1, issued1);
|
||||
expected.insert(2, issued2);
|
||||
assert_eq!(expected, parsed_response.credentials);
|
||||
|
||||
let response_paginated = test_fixture
|
||||
.rocket
|
||||
.post(&route)
|
||||
.json(&CredentialsRequestBody {
|
||||
credential_ids: vec![],
|
||||
pagination: Some(Pagination {
|
||||
last_key: Some(2),
|
||||
limit: Some(3),
|
||||
}),
|
||||
})
|
||||
.dispatch()
|
||||
.await;
|
||||
assert_eq!(response_paginated.status(), Status::Ok);
|
||||
let parsed_response: IssuedCredentialsResponse =
|
||||
serde_json::from_str(&response_paginated.into_string().await.unwrap()).unwrap();
|
||||
|
||||
let mut expected = BTreeMap::new();
|
||||
expected.insert(3, issued3);
|
||||
expected.insert(4, issued4);
|
||||
expected.insert(5, issued5);
|
||||
assert_eq!(expected, parsed_response.credentials);
|
||||
}
|
||||
@@ -1,14 +1,20 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use super::InternalSignRequest;
|
||||
use crate::coconut::error::{CoconutError, Result};
|
||||
use cosmwasm_std::{to_binary, Addr, CosmosMsg, Decimal, WasmMsg};
|
||||
use crate::coconut::storage::CoconutStorageExt;
|
||||
use crate::coconut::{self, State};
|
||||
use crate::support::storage::NymApiStorage;
|
||||
use async_trait::async_trait;
|
||||
use cosmwasm_std::{coin, to_binary, Addr, CosmosMsg, Decimal, WasmMsg};
|
||||
use cw3::ProposalResponse;
|
||||
use cw4::MemberResponse;
|
||||
use nym_api_requests::coconut::models::{IssuedCredentialInner, IssuedCredentialResponse};
|
||||
use nym_api_requests::coconut::{
|
||||
BlindSignRequestBody, BlindedSignatureResponse, VerifyCredentialBody, VerifyCredentialResponse,
|
||||
};
|
||||
use nym_coconut::tests::helpers::theta_from_keys_and_attributes;
|
||||
use nym_coconut::{prepare_blind_sign, ttp_keygen, Base58, BlindedSignature, Parameters};
|
||||
use nym_coconut::{ttp_keygen, Base58, BlindedSignature, Parameters};
|
||||
use nym_coconut_bandwidth_contract_common::events::{
|
||||
DEPOSITED_FUNDS_EVENT_TYPE, DEPOSIT_ENCRYPTION_KEY, DEPOSIT_IDENTITY_KEY, DEPOSIT_INFO,
|
||||
DEPOSIT_VALUE,
|
||||
@@ -16,27 +22,6 @@ use nym_coconut_bandwidth_contract_common::events::{
|
||||
use nym_coconut_bandwidth_contract_common::spend_credential::{
|
||||
SpendCredential, SpendCredentialResponse,
|
||||
};
|
||||
use nym_coconut_interface::{hash_to_scalar, Credential, VerificationKey};
|
||||
use nym_config::defaults::VOUCHER_INFO;
|
||||
use nym_credentials::coconut::bandwidth::BandwidthVoucher;
|
||||
use nym_credentials::coconut::params::{
|
||||
NymApiCredentialEncryptionAlgorithm, NymApiCredentialHkdfAlgorithm,
|
||||
};
|
||||
use nym_crypto::shared_key::recompute_shared_key;
|
||||
use nym_crypto::symmetric::stream_cipher;
|
||||
use nym_validator_client::nym_api::routes::{
|
||||
API_VERSION, BANDWIDTH, COCONUT_BLIND_SIGN, COCONUT_ROUTES, COCONUT_VERIFY_BANDWIDTH_CREDENTIAL,
|
||||
};
|
||||
use nym_validator_client::nyxd::Coin;
|
||||
use nym_validator_client::nyxd::{
|
||||
AccountId, Algorithm, Event, EventAttribute, ExecTxResult, Fee, Hash, TxResponse,
|
||||
};
|
||||
|
||||
use crate::coconut::State;
|
||||
use crate::support::storage::NymApiStorage;
|
||||
use async_trait::async_trait;
|
||||
use cw3::ProposalResponse;
|
||||
use cw4::MemberResponse;
|
||||
use nym_coconut_dkg_common::dealer::{
|
||||
ContractDealing, DealerDetails, DealerDetailsResponse, DealerType,
|
||||
};
|
||||
@@ -45,26 +30,39 @@ use nym_coconut_dkg_common::types::{
|
||||
EncodedBTEPublicKeyWithProof, Epoch, EpochId, InitialReplacementData, TOTAL_DEALINGS,
|
||||
};
|
||||
use nym_coconut_dkg_common::verification_key::{ContractVKShare, VerificationKeyShare};
|
||||
use nym_coconut_interface::{hash_to_scalar, Credential, VerificationKey};
|
||||
use nym_config::defaults::VOUCHER_INFO;
|
||||
use nym_contracts_common::dealings::ContractSafeBytes;
|
||||
use nym_credentials::coconut::bandwidth::BandwidthVoucher;
|
||||
use nym_crypto::asymmetric::{encryption, identity};
|
||||
use nym_dkg::Threshold;
|
||||
use nym_validator_client::nym_api::routes::{
|
||||
API_VERSION, BANDWIDTH, COCONUT_BLIND_SIGN, COCONUT_ROUTES, COCONUT_VERIFY_BANDWIDTH_CREDENTIAL,
|
||||
};
|
||||
use nym_validator_client::nyxd::cosmwasm_client::logs::Log;
|
||||
use nym_validator_client::nyxd::cosmwasm_client::types::ExecuteResult;
|
||||
use nym_validator_client::nyxd::Coin;
|
||||
use nym_validator_client::nyxd::{
|
||||
AccountId, Algorithm, Event, EventAttribute, ExecTxResult, Fee, Hash, TxResponse,
|
||||
};
|
||||
use rand_07::rngs::OsRng;
|
||||
use rand_07::Rng;
|
||||
use rand_07::{Rng, RngCore};
|
||||
use rocket::http::Status;
|
||||
use rocket::local::asynchronous::Client;
|
||||
use std::collections::HashMap;
|
||||
use std::str::FromStr;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
mod issued_credentials;
|
||||
|
||||
const TEST_COIN_DENOM: &str = "unym";
|
||||
const TEST_REWARDING_VALIDATOR_ADDRESS: &str = "n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0";
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct DummyClient {
|
||||
validator_address: AccountId,
|
||||
tx_db: Arc<RwLock<HashMap<String, TxResponse>>>,
|
||||
tx_db: Arc<RwLock<HashMap<Hash, TxResponse>>>,
|
||||
proposal_db: Arc<RwLock<HashMap<u64, ProposalResponse>>>,
|
||||
spent_credential_db: Arc<RwLock<HashMap<String, SpendCredentialResponse>>>,
|
||||
|
||||
@@ -94,7 +92,7 @@ impl DummyClient {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_tx_db(mut self, tx_db: &Arc<RwLock<HashMap<String, TxResponse>>>) -> Self {
|
||||
pub fn with_tx_db(mut self, tx_db: &Arc<RwLock<HashMap<Hash, TxResponse>>>) -> Self {
|
||||
self.tx_db = Arc::clone(tx_db);
|
||||
self
|
||||
}
|
||||
@@ -172,13 +170,8 @@ impl super::client::Client for DummyClient {
|
||||
self.validator_address.clone()
|
||||
}
|
||||
|
||||
async fn get_tx(&self, tx_hash: &str) -> Result<TxResponse> {
|
||||
self.tx_db
|
||||
.read()
|
||||
.unwrap()
|
||||
.get(tx_hash)
|
||||
.cloned()
|
||||
.ok_or(CoconutError::TxHashParseError)
|
||||
async fn get_tx(&self, tx_hash: Hash) -> Result<TxResponse> {
|
||||
Ok(self.tx_db.read().unwrap().get(&tx_hash).cloned().unwrap())
|
||||
}
|
||||
|
||||
async fn get_proposal(&self, proposal_id: u64) -> Result<ProposalResponse> {
|
||||
@@ -467,27 +460,38 @@ impl super::client::Client for DummyClient {
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct DummyCommunicationChannel {
|
||||
current_epoch: Arc<AtomicU64>,
|
||||
aggregated_verification_key: VerificationKey,
|
||||
}
|
||||
|
||||
impl DummyCommunicationChannel {
|
||||
pub fn new(aggregated_verification_key: VerificationKey) -> Self {
|
||||
DummyCommunicationChannel {
|
||||
current_epoch: Arc::new(AtomicU64::new(1)),
|
||||
aggregated_verification_key,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_epoch(mut self, current_epoch: Arc<AtomicU64>) -> Self {
|
||||
self.current_epoch = current_epoch;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl super::comm::APICommunicationChannel for DummyCommunicationChannel {
|
||||
async fn current_epoch(&self) -> Result<EpochId> {
|
||||
Ok(self.current_epoch.load(Ordering::Relaxed))
|
||||
}
|
||||
|
||||
async fn aggregated_verification_key(&self, _epoch_id: EpochId) -> Result<VerificationKey> {
|
||||
Ok(self.aggregated_verification_key.clone())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tx_entry_fixture(tx_hash: &str) -> TxResponse {
|
||||
pub fn tx_entry_fixture(hash: Hash) -> TxResponse {
|
||||
TxResponse {
|
||||
hash: Hash::from_str(tx_hash).unwrap(),
|
||||
hash,
|
||||
height: Default::default(),
|
||||
index: 0,
|
||||
tx_result: ExecTxResult {
|
||||
@@ -505,88 +509,242 @@ pub fn tx_entry_fixture(tx_hash: &str) -> TxResponse {
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn signed_before() {
|
||||
let tx_hash =
|
||||
Hash::from_str("6B27412050B823E58BB38447D7870BBC8CBE3C51C905BEA89D459ACCDA80A00E").unwrap();
|
||||
let tx_entry = tx_entry_fixture(&tx_hash.to_string());
|
||||
let signature = String::from(
|
||||
"2DHbEZ6pzToGpsAXJrqJi7Wj1pAXeT18283q2YEEyNH5gTymwRozWBdja6SMAVt1dyYmUnM4ZNhsJ4wxZyGh4Z6J",
|
||||
);
|
||||
pub fn deposit_tx_fixture(voucher: &BandwidthVoucher) -> TxResponse {
|
||||
TxResponse {
|
||||
hash: voucher.tx_hash(),
|
||||
height: Default::default(),
|
||||
index: 0,
|
||||
tx_result: ExecTxResult {
|
||||
code: Default::default(),
|
||||
data: Default::default(),
|
||||
log: "".to_string(),
|
||||
info: "".to_string(),
|
||||
gas_wanted: 0,
|
||||
gas_used: 0,
|
||||
events: vec![Event {
|
||||
kind: format!("wasm-{}", DEPOSITED_FUNDS_EVENT_TYPE),
|
||||
attributes: vec![
|
||||
EventAttribute {
|
||||
key: DEPOSIT_VALUE.to_string(),
|
||||
value: voucher.get_voucher_value(),
|
||||
index: false,
|
||||
},
|
||||
EventAttribute {
|
||||
key: DEPOSIT_INFO.to_string(),
|
||||
value: VOUCHER_INFO.to_string(),
|
||||
index: false,
|
||||
},
|
||||
EventAttribute {
|
||||
key: DEPOSIT_IDENTITY_KEY.to_string(),
|
||||
value: voucher.identity_key().public_key().to_base58_string(),
|
||||
index: false,
|
||||
},
|
||||
EventAttribute {
|
||||
key: DEPOSIT_ENCRYPTION_KEY.parse().unwrap(),
|
||||
value: voucher.encryption_key().public_key().to_base58_string(),
|
||||
index: false,
|
||||
},
|
||||
],
|
||||
}],
|
||||
codespace: "".to_string(),
|
||||
},
|
||||
tx: vec![],
|
||||
proof: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn blinded_signature_fixture() -> BlindedSignature {
|
||||
let gen1_bytes = [
|
||||
151u8, 241, 211, 167, 49, 151, 215, 148, 38, 149, 99, 140, 79, 169, 172, 15, 195, 104, 140,
|
||||
79, 151, 116, 185, 5, 161, 78, 58, 63, 23, 27, 172, 88, 108, 85, 232, 63, 249, 122, 26,
|
||||
239, 251, 58, 240, 10, 219, 34, 198, 187,
|
||||
];
|
||||
|
||||
let dummy_bytes = gen1_bytes
|
||||
.iter()
|
||||
.chain(gen1_bytes.iter())
|
||||
.copied()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
BlindedSignature::from_bytes(&dummy_bytes).unwrap()
|
||||
}
|
||||
|
||||
pub fn voucher_request_fixture<C: Into<Coin>>(
|
||||
amount: C,
|
||||
tx_hash: Option<String>,
|
||||
) -> (BandwidthVoucher, BlindSignRequestBody) {
|
||||
let params = Parameters::new(4).unwrap();
|
||||
let mut rng = OsRng;
|
||||
let tx_hash = if let Some(provided) = &tx_hash {
|
||||
provided.parse().unwrap()
|
||||
} else {
|
||||
Hash::from_str("6B27412050B823E58BB38447D7870BBC8CBE3C51C905BEA89D459ACCDA80A00E").unwrap()
|
||||
};
|
||||
|
||||
let identity_keypair = identity::KeyPair::new(&mut rng);
|
||||
let encryption_keypair = encryption::KeyPair::new(&mut rng);
|
||||
let id_priv =
|
||||
identity::PrivateKey::from_bytes(&identity_keypair.private_key().to_bytes()).unwrap();
|
||||
let enc_priv =
|
||||
encryption::PrivateKey::from_bytes(&encryption_keypair.private_key().to_bytes()).unwrap();
|
||||
|
||||
let voucher = BandwidthVoucher::new(
|
||||
¶ms,
|
||||
"1234".to_string(),
|
||||
amount.into().amount.to_string(),
|
||||
VOUCHER_INFO.to_string(),
|
||||
tx_hash,
|
||||
identity::PrivateKey::from_base58_string(
|
||||
identity::KeyPair::new(&mut rng)
|
||||
.private_key()
|
||||
.to_base58_string(),
|
||||
)
|
||||
.unwrap(),
|
||||
encryption::PrivateKey::from_bytes(
|
||||
&encryption::KeyPair::new(&mut rng).private_key().to_bytes(),
|
||||
)
|
||||
.unwrap(),
|
||||
id_priv,
|
||||
enc_priv,
|
||||
);
|
||||
let (_, blind_sign_req) = prepare_blind_sign(
|
||||
¶ms,
|
||||
&voucher.get_private_attributes(),
|
||||
&voucher.get_public_attributes(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let key_pair = ttp_keygen(¶ms, 1, 1).unwrap().remove(0);
|
||||
let mut db_dir = std::env::temp_dir();
|
||||
db_dir.push(&key_pair.verification_key().to_bs58()[..8]);
|
||||
let storage = NymApiStorage::init(db_dir).await.unwrap();
|
||||
let tx_db = Arc::new(RwLock::new(HashMap::new()));
|
||||
tx_db
|
||||
.write()
|
||||
.unwrap()
|
||||
.insert(tx_hash.to_string(), tx_entry.clone());
|
||||
let nyxd_client =
|
||||
DummyClient::new(AccountId::from_str(TEST_REWARDING_VALIDATOR_ADDRESS).unwrap())
|
||||
.with_tx_db(&tx_db);
|
||||
let comm_channel = DummyCommunicationChannel::new(key_pair.verification_key());
|
||||
let staged_key_pair = crate::coconut::KeyPair::new();
|
||||
staged_key_pair.set(Some(key_pair)).await;
|
||||
|
||||
let rocket = rocket::build().attach(InternalSignRequest::stage(
|
||||
nyxd_client,
|
||||
TEST_COIN_DENOM.to_string(),
|
||||
staged_key_pair,
|
||||
comm_channel,
|
||||
storage.clone(),
|
||||
));
|
||||
let client = Client::tracked(rocket)
|
||||
.await
|
||||
.expect("valid rocket instance");
|
||||
|
||||
let request_body = BlindSignRequestBody::new(
|
||||
&blind_sign_req,
|
||||
tx_hash.to_string(),
|
||||
signature.clone(),
|
||||
&voucher.get_public_attributes(),
|
||||
let request = BlindSignRequestBody::new(
|
||||
voucher.blind_sign_request().clone(),
|
||||
tx_hash,
|
||||
voucher.sign(),
|
||||
voucher.get_public_attributes_plain(),
|
||||
4,
|
||||
);
|
||||
|
||||
let encrypted_signature = vec![1, 2, 3, 4];
|
||||
let remote_key = [42; 32];
|
||||
let expected_response = BlindedSignatureResponse::new(encrypted_signature, remote_key);
|
||||
storage
|
||||
.insert_blinded_signature_response(
|
||||
&tx_hash.to_string(),
|
||||
&expected_response.to_base58_string(),
|
||||
)
|
||||
(voucher, request)
|
||||
}
|
||||
|
||||
fn dummy_signature() -> identity::Signature {
|
||||
"3vUCc6MCN5AC2LNgDYjRB1QeErZSN1S8f6K14JHjpUcKWXbjGYFExA8DbwQQBki9gyUqrpBF94Drttb4eMcGQXkp"
|
||||
.parse()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
async fn nym_api_storage_fixture(identity: &identity::KeyPair) -> NymApiStorage {
|
||||
let mut db_dir = std::env::temp_dir();
|
||||
db_dir.push(identity.public_key().to_base58_string());
|
||||
NymApiStorage::init(db_dir).await.unwrap()
|
||||
}
|
||||
|
||||
struct TestFixture {
|
||||
rocket: Client,
|
||||
storage: NymApiStorage,
|
||||
tx_db: Arc<RwLock<HashMap<Hash, TxResponse>>>,
|
||||
epoch: Arc<AtomicU64>,
|
||||
}
|
||||
|
||||
impl TestFixture {
|
||||
async fn new() -> Self {
|
||||
let mut rng = OsRng;
|
||||
let params = Parameters::new(4).unwrap();
|
||||
let coconut_keypair = ttp_keygen(¶ms, 1, 1).unwrap().remove(0);
|
||||
let identity = identity::KeyPair::new(&mut rng);
|
||||
let epoch = Arc::new(AtomicU64::new(1));
|
||||
let comm_channel =
|
||||
DummyCommunicationChannel::new(coconut_keypair.verification_key().clone())
|
||||
.with_epoch(epoch.clone());
|
||||
let storage = nym_api_storage_fixture(&identity).await;
|
||||
|
||||
let staged_key_pair = crate::coconut::KeyPair::new();
|
||||
staged_key_pair.set(Some(coconut_keypair)).await;
|
||||
|
||||
let tx_db = Arc::new(RwLock::new(HashMap::new()));
|
||||
let nyxd_client =
|
||||
DummyClient::new(AccountId::from_str(TEST_REWARDING_VALIDATOR_ADDRESS).unwrap())
|
||||
.with_tx_db(&tx_db);
|
||||
|
||||
let rocket = rocket::build().attach(coconut::stage(
|
||||
nyxd_client,
|
||||
TEST_COIN_DENOM.to_string(),
|
||||
identity,
|
||||
staged_key_pair,
|
||||
comm_channel,
|
||||
storage.clone(),
|
||||
));
|
||||
|
||||
TestFixture {
|
||||
rocket: Client::tracked(rocket)
|
||||
.await
|
||||
.expect("valid rocket instance"),
|
||||
storage,
|
||||
tx_db,
|
||||
epoch,
|
||||
}
|
||||
}
|
||||
|
||||
fn set_epoch(&self, epoch: u64) {
|
||||
self.epoch.store(epoch, Ordering::Relaxed)
|
||||
}
|
||||
|
||||
fn add_tx(&self, hash: Hash, tx: TxResponse) {
|
||||
self.tx_db.write().unwrap().insert(hash, tx);
|
||||
}
|
||||
|
||||
fn add_deposit_tx(&self, voucher: &BandwidthVoucher) {
|
||||
let mut guard = self.tx_db.write().unwrap();
|
||||
let fixture = deposit_tx_fixture(voucher);
|
||||
guard.insert(voucher.tx_hash(), fixture);
|
||||
}
|
||||
|
||||
async fn issue_dummy_credential(&self) {
|
||||
let mut rng = OsRng;
|
||||
let mut tx_hash = [0u8; 32];
|
||||
rng.fill_bytes(&mut tx_hash);
|
||||
let tx_hash = Hash::from_bytes(Algorithm::Sha256, &tx_hash).unwrap();
|
||||
|
||||
let (voucher, req) = voucher_request_fixture(coin(1234, "unym"), Some(tx_hash.to_string()));
|
||||
self.add_deposit_tx(&voucher);
|
||||
|
||||
self.issue_credential(req).await;
|
||||
}
|
||||
|
||||
async fn issue_credential(&self, req: BlindSignRequestBody) -> BlindedSignatureResponse {
|
||||
let response = self
|
||||
.rocket
|
||||
.post(format!(
|
||||
"/{API_VERSION}/{COCONUT_ROUTES}/{BANDWIDTH}/{COCONUT_BLIND_SIGN}",
|
||||
))
|
||||
.json(&req)
|
||||
.dispatch()
|
||||
.await;
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
serde_json::from_str(&response.into_string().await.unwrap()).unwrap()
|
||||
}
|
||||
|
||||
async fn issued_credential(&self, id: i64) -> Option<IssuedCredentialResponse> {
|
||||
let response = self
|
||||
.rocket
|
||||
.get(format!(
|
||||
"/{API_VERSION}/{COCONUT_ROUTES}/{BANDWIDTH}/issued-credential/{id}"
|
||||
))
|
||||
.dispatch()
|
||||
.await;
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
serde_json::from_str(&response.into_string().await.unwrap()).unwrap()
|
||||
}
|
||||
|
||||
async fn issued_unchecked(&self, id: i64) -> IssuedCredentialInner {
|
||||
self.issued_credential(id)
|
||||
.await
|
||||
.unwrap()
|
||||
.credential
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn already_issued() {
|
||||
let (_, request_body) = voucher_request_fixture(coin(1234, TEST_COIN_DENOM), None);
|
||||
let tx_hash = request_body.tx_hash;
|
||||
let tx_entry = tx_entry_fixture(tx_hash);
|
||||
|
||||
let test_fixture = TestFixture::new().await;
|
||||
test_fixture.add_tx(tx_hash, tx_entry);
|
||||
|
||||
let sig = blinded_signature_fixture();
|
||||
let commitments = request_body.encode_commitments();
|
||||
let public = request_body.public_attributes_plain.clone();
|
||||
test_fixture
|
||||
.storage
|
||||
.store_issued_credential(42, tx_hash, &sig, dummy_signature(), commitments, public)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let response = client
|
||||
let response = test_fixture
|
||||
.rocket
|
||||
.post(format!(
|
||||
"/{}/{}/{}/{}",
|
||||
API_VERSION, COCONUT_ROUTES, BANDWIDTH, COCONUT_BLIND_SIGN
|
||||
@@ -595,6 +753,7 @@ async fn signed_before() {
|
||||
.dispatch()
|
||||
.await;
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
let expected_response = BlindedSignatureResponse::new(sig);
|
||||
|
||||
// This is a more direct way, but there's a bug which makes it hang https://github.com/SergioBenitez/Rocket/issues/1893
|
||||
// let blinded_signature_response = response
|
||||
@@ -612,6 +771,9 @@ async fn signed_before() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn state_functions() {
|
||||
let mut rng = OsRng;
|
||||
let identity = identity::KeyPair::new(&mut rng);
|
||||
|
||||
let nyxd_client =
|
||||
DummyClient::new(AccountId::from_str(TEST_REWARDING_VALIDATOR_ADDRESS).unwrap());
|
||||
let params = Parameters::new(4).unwrap();
|
||||
@@ -619,38 +781,49 @@ async fn state_functions() {
|
||||
let mut db_dir = std::env::temp_dir();
|
||||
db_dir.push(&key_pair.verification_key().to_bs58()[..8]);
|
||||
let storage = NymApiStorage::init(db_dir).await.unwrap();
|
||||
let comm_channel = DummyCommunicationChannel::new(key_pair.verification_key());
|
||||
let comm_channel = DummyCommunicationChannel::new(key_pair.verification_key().clone());
|
||||
let staged_key_pair = crate::coconut::KeyPair::new();
|
||||
staged_key_pair.set(Some(key_pair)).await;
|
||||
let state = State::new(
|
||||
nyxd_client,
|
||||
TEST_COIN_DENOM.to_string(),
|
||||
identity,
|
||||
staged_key_pair,
|
||||
comm_channel,
|
||||
storage.clone(),
|
||||
);
|
||||
|
||||
let tx_hash = String::from("6B27412050B823E58BB38447D7870BBC8CBE3C51C905BEA89D459ACCDA80A00E");
|
||||
assert!(state.signed_before(&tx_hash).await.unwrap().is_none());
|
||||
let tx_hash = "6B27412050B823E58BB38447D7870BBC8CBE3C51C905BEA89D459ACCDA80A00E"
|
||||
.parse()
|
||||
.unwrap();
|
||||
assert!(state.already_issued(tx_hash).await.unwrap().is_none());
|
||||
|
||||
let encrypted_signature = vec![1, 2, 3, 4];
|
||||
let remote_key = [42; 32];
|
||||
let expected_response = BlindedSignatureResponse::new(encrypted_signature, remote_key);
|
||||
let (_, request_body) = voucher_request_fixture(coin(1234, TEST_COIN_DENOM), None);
|
||||
let commitments = request_body.encode_commitments();
|
||||
let public = request_body.public_attributes_plain.clone();
|
||||
let sig = blinded_signature_fixture();
|
||||
storage
|
||||
.insert_blinded_signature_response(&tx_hash, &expected_response.to_base58_string())
|
||||
.store_issued_credential(
|
||||
42,
|
||||
tx_hash,
|
||||
&sig,
|
||||
dummy_signature(),
|
||||
commitments.clone(),
|
||||
public.clone(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
state
|
||||
.signed_before(&tx_hash)
|
||||
.already_issued(tx_hash)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.to_bytes(),
|
||||
expected_response.to_bytes()
|
||||
blinded_signature_fixture().to_bytes()
|
||||
);
|
||||
|
||||
let encryption_keypair = nym_crypto::asymmetric::encryption::KeyPair::new(&mut OsRng);
|
||||
let blinded_signature = BlindedSignature::from_bytes(&[
|
||||
183, 217, 166, 113, 40, 123, 74, 25, 72, 31, 136, 19, 125, 95, 217, 228, 96, 113, 25, 240,
|
||||
12, 102, 125, 11, 174, 20, 216, 82, 192, 71, 27, 194, 48, 20, 17, 95, 243, 179, 82, 21, 57,
|
||||
@@ -659,68 +832,46 @@ async fn state_functions() {
|
||||
222, 119, 93, 146, 116, 229, 0, 152, 51, 232, 2, 102, 204, 147, 202, 254, 243,
|
||||
])
|
||||
.unwrap();
|
||||
|
||||
// Check that the new payload is not stored if there was already something signed for tx_hash
|
||||
assert_eq!(
|
||||
state
|
||||
.encrypt_and_store(
|
||||
&tx_hash,
|
||||
encryption_keypair.public_key(),
|
||||
&blinded_signature,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.to_bytes(),
|
||||
expected_response.to_bytes()
|
||||
);
|
||||
let storage_err = storage
|
||||
.store_issued_credential(
|
||||
42,
|
||||
tx_hash,
|
||||
&blinded_signature,
|
||||
dummy_signature(),
|
||||
commitments.clone(),
|
||||
public.clone(),
|
||||
)
|
||||
.await;
|
||||
assert!(storage_err.is_err());
|
||||
|
||||
// And use a new hash to store a new signature
|
||||
let tx_hash = String::from("97D64C38D6601B1F0FD3A82E20D252685CB7A210AFB0261018590659AB82B0BF");
|
||||
let response = state
|
||||
.encrypt_and_store(
|
||||
&tx_hash,
|
||||
encryption_keypair.public_key(),
|
||||
let tx_hash = "97D64C38D6601B1F0FD3A82E20D252685CB7A210AFB0261018590659AB82B0BF"
|
||||
.parse()
|
||||
.unwrap();
|
||||
|
||||
storage
|
||||
.store_issued_credential(
|
||||
42,
|
||||
tx_hash,
|
||||
&blinded_signature,
|
||||
dummy_signature(),
|
||||
commitments.clone(),
|
||||
public.clone(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let remote_key =
|
||||
nym_crypto::asymmetric::encryption::PublicKey::from_bytes(&response.remote_key).unwrap();
|
||||
|
||||
let encryption_key = recompute_shared_key::<
|
||||
NymApiCredentialEncryptionAlgorithm,
|
||||
NymApiCredentialHkdfAlgorithm,
|
||||
>(&remote_key, encryption_keypair.private_key());
|
||||
let zero_iv = stream_cipher::zero_iv::<NymApiCredentialEncryptionAlgorithm>();
|
||||
let blinded_signature_bytes = stream_cipher::decrypt::<NymApiCredentialEncryptionAlgorithm>(
|
||||
&encryption_key,
|
||||
&zero_iv,
|
||||
&response.encrypted_signature,
|
||||
);
|
||||
|
||||
let received_blinded_signature =
|
||||
BlindedSignature::from_bytes(&blinded_signature_bytes).unwrap();
|
||||
assert_eq!(
|
||||
blinded_signature.to_bytes(),
|
||||
received_blinded_signature.to_bytes()
|
||||
);
|
||||
|
||||
// Check that the same value for tx_hash is returned
|
||||
|
||||
let other_signature = BlindedSignature::from_bytes(&[
|
||||
183, 217, 166, 113, 40, 123, 74, 25, 72, 31, 136, 19, 125, 95, 217, 228, 96, 113, 25, 240,
|
||||
12, 102, 125, 11, 174, 20, 216, 82, 192, 71, 27, 194, 48, 20, 17, 95, 243, 179, 82, 21, 57,
|
||||
143, 101, 19, 22, 186, 147, 13, 131, 236, 38, 138, 192, 235, 169, 142, 176, 118, 153, 238,
|
||||
141, 91, 94, 139, 168, 214, 17, 250, 96, 206, 139, 89, 139, 87, 31, 8, 106, 171, 8, 140,
|
||||
201, 158, 18, 152, 24, 98, 153, 170, 141, 35, 190, 200, 19, 148, 71, 197,
|
||||
])
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
state
|
||||
.encrypt_and_store(&tx_hash, encryption_keypair.public_key(), &other_signature,)
|
||||
.already_issued(tx_hash)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.to_bytes(),
|
||||
response.to_bytes()
|
||||
blinded_signature.to_bytes()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -731,6 +882,8 @@ async fn blind_sign_correct() {
|
||||
|
||||
let params = Parameters::new(4).unwrap();
|
||||
let mut rng = OsRng;
|
||||
let nym_api_identity = identity::KeyPair::new(&mut rng);
|
||||
|
||||
let identity_keypair = identity::KeyPair::new(&mut rng);
|
||||
let encryption_keypair = encryption::KeyPair::new(&mut rng);
|
||||
let voucher = BandwidthVoucher::new(
|
||||
@@ -749,55 +902,20 @@ async fn blind_sign_correct() {
|
||||
let storage = NymApiStorage::init(db_dir).await.unwrap();
|
||||
let tx_db = Arc::new(RwLock::new(HashMap::new()));
|
||||
|
||||
let mut tx_entry = tx_entry_fixture(&tx_hash.to_string());
|
||||
tx_entry.tx_result.events.push(Event {
|
||||
kind: format!("wasm-{}", DEPOSITED_FUNDS_EVENT_TYPE),
|
||||
attributes: vec![],
|
||||
});
|
||||
tx_entry.tx_result.events.get_mut(0).unwrap().attributes = vec![
|
||||
EventAttribute {
|
||||
key: DEPOSIT_VALUE.parse().unwrap(),
|
||||
value: "1234".parse().unwrap(),
|
||||
index: false,
|
||||
},
|
||||
EventAttribute {
|
||||
key: DEPOSIT_INFO.parse().unwrap(),
|
||||
value: VOUCHER_INFO.parse().unwrap(),
|
||||
index: false,
|
||||
},
|
||||
EventAttribute {
|
||||
key: DEPOSIT_IDENTITY_KEY.parse().unwrap(),
|
||||
value: identity_keypair
|
||||
.public_key()
|
||||
.to_base58_string()
|
||||
.parse()
|
||||
.unwrap(),
|
||||
index: false,
|
||||
},
|
||||
EventAttribute {
|
||||
key: DEPOSIT_ENCRYPTION_KEY.parse().unwrap(),
|
||||
value: encryption_keypair
|
||||
.public_key()
|
||||
.to_base58_string()
|
||||
.parse()
|
||||
.unwrap(),
|
||||
index: false,
|
||||
},
|
||||
];
|
||||
tx_db
|
||||
.write()
|
||||
.unwrap()
|
||||
.insert(tx_hash.to_string(), tx_entry.clone());
|
||||
let tx_entry = deposit_tx_fixture(&voucher);
|
||||
|
||||
tx_db.write().unwrap().insert(tx_hash, tx_entry.clone());
|
||||
let nyxd_client =
|
||||
DummyClient::new(AccountId::from_str(TEST_REWARDING_VALIDATOR_ADDRESS).unwrap())
|
||||
.with_tx_db(&tx_db);
|
||||
let comm_channel = DummyCommunicationChannel::new(key_pair.verification_key());
|
||||
let comm_channel = DummyCommunicationChannel::new(key_pair.verification_key().clone());
|
||||
let staged_key_pair = crate::coconut::KeyPair::new();
|
||||
staged_key_pair.set(Some(key_pair)).await;
|
||||
|
||||
let rocket = rocket::build().attach(InternalSignRequest::stage(
|
||||
let rocket = rocket::build().attach(coconut::stage(
|
||||
nyxd_client,
|
||||
TEST_COIN_DENOM.to_string(),
|
||||
nym_api_identity,
|
||||
staged_key_pair,
|
||||
comm_channel,
|
||||
storage.clone(),
|
||||
@@ -806,15 +924,13 @@ async fn blind_sign_correct() {
|
||||
.await
|
||||
.expect("valid rocket instance");
|
||||
|
||||
let request_signature = voucher.sign();
|
||||
|
||||
let request_body = BlindSignRequestBody::new(
|
||||
voucher.blind_sign_request(),
|
||||
tx_hash.to_string(),
|
||||
voucher
|
||||
.sign(voucher.blind_sign_request())
|
||||
.to_base58_string(),
|
||||
&voucher.get_public_attributes(),
|
||||
voucher.blind_sign_request().clone(),
|
||||
tx_hash,
|
||||
request_signature,
|
||||
voucher.get_public_attributes_plain(),
|
||||
4,
|
||||
);
|
||||
|
||||
let response = client
|
||||
@@ -825,6 +941,7 @@ async fn blind_sign_correct() {
|
||||
.json(&request_body)
|
||||
.dispatch()
|
||||
.await;
|
||||
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
// This is a more direct way, but there's a bug which makes it hang https://github.com/SergioBenitez/Rocket/issues/1893
|
||||
// assert!(response.into_json::<BlindedSignatureResponse>().is_some());
|
||||
@@ -847,26 +964,32 @@ async fn verification_of_bandwidth_credential() {
|
||||
let mut key_pairs = ttp_keygen(¶ms, 1, 1).unwrap();
|
||||
let voucher_value = 1234u64;
|
||||
let voucher_info = "voucher info";
|
||||
let public_attributes = vec![
|
||||
let public_attributes = [
|
||||
hash_to_scalar(voucher_value.to_string()),
|
||||
hash_to_scalar(voucher_info),
|
||||
];
|
||||
let public_attributes_ref = vec![&public_attributes[0], &public_attributes[1]];
|
||||
let indices: Vec<u64> = key_pairs
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(idx, _)| (idx + 1) as u64)
|
||||
.collect();
|
||||
let theta =
|
||||
theta_from_keys_and_attributes(¶ms, &key_pairs, &indices, &public_attributes).unwrap();
|
||||
theta_from_keys_and_attributes(¶ms, &key_pairs, &indices, &public_attributes_ref)
|
||||
.unwrap();
|
||||
let key_pair = key_pairs.remove(0);
|
||||
db_dir.push(&key_pair.verification_key().to_bs58()[..8]);
|
||||
let storage1 = NymApiStorage::init(db_dir).await.unwrap();
|
||||
let comm_channel = DummyCommunicationChannel::new(key_pair.verification_key());
|
||||
let comm_channel = DummyCommunicationChannel::new(key_pair.verification_key().clone());
|
||||
let staged_key_pair = crate::coconut::KeyPair::new();
|
||||
staged_key_pair.set(Some(key_pair)).await;
|
||||
let rocket = rocket::build().attach(InternalSignRequest::stage(
|
||||
let mut rng = OsRng;
|
||||
let identity = identity::KeyPair::new(&mut rng);
|
||||
|
||||
let rocket = rocket::build().attach(coconut::stage(
|
||||
nyxd_client.clone(),
|
||||
TEST_COIN_DENOM.to_string(),
|
||||
identity,
|
||||
staged_key_pair,
|
||||
comm_channel.clone(),
|
||||
storage1.clone(),
|
||||
@@ -80,12 +80,14 @@ async fn start_nym_api_tasks(
|
||||
let network_details = NetworkDetails::new(connected_nyxd.to_string(), nym_network_details);
|
||||
|
||||
let coconut_keypair = coconut::keypair::KeyPair::new();
|
||||
let identity_keypair = config.base.storage_paths.load_identity()?;
|
||||
|
||||
// let's build our rocket!
|
||||
let rocket = http::setup_rocket(
|
||||
&config,
|
||||
network_details,
|
||||
nyxd_client.clone(),
|
||||
identity_keypair,
|
||||
coconut_keypair.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -126,7 +126,16 @@ pub(crate) fn build_config(args: run::Args) -> Result<Config> {
|
||||
}
|
||||
};
|
||||
|
||||
let config = override_config(config, args);
|
||||
let mut config = override_config(config, args);
|
||||
// since we have no proper `init`, we have to do id check here:
|
||||
let made_new_keys = config
|
||||
.base
|
||||
.storage_paths
|
||||
.generate_identity_if_missing(&config.base.id)?;
|
||||
|
||||
if made_new_keys {
|
||||
config.save_to_default_location()?
|
||||
}
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::support::config::persistence::{
|
||||
CoconutSignerPaths, NetworkMonitorPaths, NodeStatusAPIPaths,
|
||||
CoconutSignerPaths, NetworkMonitorPaths, NodeStatusAPIPaths, NymApiPaths,
|
||||
};
|
||||
use crate::support::config::template::CONFIG_TEMPLATE;
|
||||
use nym_config::defaults::{mainnet, NymNetworkDetails};
|
||||
@@ -21,6 +21,7 @@ use zeroize::{Zeroize, ZeroizeOnDrop};
|
||||
pub(crate) mod helpers;
|
||||
pub(crate) mod old_config_v1_1_21;
|
||||
pub(crate) mod old_config_v1_1_27;
|
||||
|
||||
mod persistence;
|
||||
mod template;
|
||||
|
||||
@@ -272,22 +273,29 @@ impl Config {
|
||||
#[derive(Debug, Deserialize, PartialEq, Eq, Serialize, Zeroize, ZeroizeOnDrop)]
|
||||
pub struct Base {
|
||||
/// ID specifies the human readable ID of this particular nym-api.
|
||||
id: String,
|
||||
pub id: String,
|
||||
|
||||
#[zeroize(skip)]
|
||||
local_validator: Url,
|
||||
pub local_validator: Url,
|
||||
|
||||
/// Address of the validator contract managing the network
|
||||
#[zeroize(skip)]
|
||||
mixnet_contract_address: nyxd::AccountId,
|
||||
pub mixnet_contract_address: nyxd::AccountId,
|
||||
|
||||
/// Address of the vesting contract holding locked tokens
|
||||
#[zeroize(skip)]
|
||||
vesting_contract_address: nyxd::AccountId,
|
||||
pub vesting_contract_address: nyxd::AccountId,
|
||||
|
||||
/// Mnemonic used for rewarding and/or multisig operations
|
||||
// TODO: similarly to the note in gateway, this should get moved to a separate file
|
||||
mnemonic: bip39::Mnemonic,
|
||||
|
||||
/// Storage paths to the common nym-api files
|
||||
#[zeroize(skip)]
|
||||
// ideally we wouldn't be using default here, but I really really don't want to be writing
|
||||
// another big config migration
|
||||
#[serde(default)]
|
||||
pub storage_paths: NymApiPaths,
|
||||
}
|
||||
|
||||
impl Base {
|
||||
@@ -296,8 +304,11 @@ impl Base {
|
||||
.parse()
|
||||
.expect("default local validator is malformed!");
|
||||
|
||||
let id = id.into();
|
||||
|
||||
Base {
|
||||
id: id.into(),
|
||||
storage_paths: NymApiPaths::new_default(&id),
|
||||
id,
|
||||
local_validator: default_validator,
|
||||
mixnet_contract_address: mainnet::MIXNET_CONTRACT_ADDRESS.parse().unwrap(),
|
||||
vesting_contract_address: mainnet::VESTING_CONTRACT_ADDRESS.parse().unwrap(),
|
||||
|
||||
@@ -90,6 +90,7 @@ impl From<ConfigV1_1_21> for Config {
|
||||
mixnet_contract_address: value.base.mixnet_contract_address,
|
||||
vesting_contract_address: value.base.vesting_contract_address,
|
||||
mnemonic: value.base.mnemonic,
|
||||
storage_paths: Default::default(),
|
||||
},
|
||||
network_monitor: NetworkMonitor {
|
||||
enabled: value.network_monitor.enabled,
|
||||
|
||||
@@ -60,6 +60,7 @@ impl From<ConfigV1_1_27> for Config {
|
||||
mixnet_contract_address: value.base.mixnet_contract_address,
|
||||
vesting_contract_address: value.base.vesting_contract_address,
|
||||
mnemonic: value.base.mnemonic,
|
||||
storage_paths: Default::default(),
|
||||
},
|
||||
network_monitor: NetworkMonitor {
|
||||
enabled: value.network_monitor.enabled,
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::support::config::default_data_directory;
|
||||
use anyhow::{anyhow, Context};
|
||||
use nym_config::serde_helpers::de_maybe_stringified;
|
||||
use nym_crypto::asymmetric::identity;
|
||||
use rand_07::rngs::OsRng;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
@@ -15,6 +19,9 @@ pub const DEFAULT_DKG_PUBLIC_KEY_WITH_PROOF_FILENAME: &str = "dkg_public_key_wit
|
||||
pub const DEFAULT_COCONUT_VERIFICATION_KEY_FILENAME: &str = "coconut_verification_key.pem";
|
||||
pub const DEFAULT_COCONUT_SECRET_KEY_FILENAME: &str = "coconut_secret_key.pem";
|
||||
|
||||
pub const DEFAULT_PRIVATE_IDENTITY_KEY_FILENAME: &str = "private_identity.pem";
|
||||
pub const DEFAULT_PUBLIC_IDENTITY_KEY_FILENAME: &str = "public_identity.pem";
|
||||
|
||||
// #[derive(Debug, Deserialize, PartialEq, Eq, Serialize)]
|
||||
// pub struct NymApiPathfinder {
|
||||
// pub network_monitor: NetworkMonitorPathfinder,
|
||||
@@ -99,3 +106,66 @@ impl CoconutSignerPaths {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize, PartialEq, Eq, Serialize)]
|
||||
#[serde(default)]
|
||||
pub struct NymApiPaths {
|
||||
/// Path to file containing private identity key of the nym-api.
|
||||
#[serde(deserialize_with = "de_maybe_stringified")]
|
||||
pub private_identity_key_file: Option<PathBuf>,
|
||||
|
||||
/// Path to file containing public identity key of the nym-api.
|
||||
#[serde(deserialize_with = "de_maybe_stringified")]
|
||||
pub public_identity_key_file: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl NymApiPaths {
|
||||
pub fn new_default<P: AsRef<Path>>(id: P) -> Self {
|
||||
let data_dir = default_data_directory(id);
|
||||
|
||||
NymApiPaths {
|
||||
private_identity_key_file: Some(data_dir.join(DEFAULT_PRIVATE_IDENTITY_KEY_FILENAME)),
|
||||
public_identity_key_file: Some(data_dir.join(DEFAULT_PUBLIC_IDENTITY_KEY_FILENAME)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_identity_if_missing<P: AsRef<Path>>(&mut self, id: P) -> anyhow::Result<bool> {
|
||||
if self.private_identity_key_file.is_none() {
|
||||
log::warn!("identity key paths are not set - going to generate a fresh pair!");
|
||||
|
||||
let data_dir = default_data_directory(id);
|
||||
self.private_identity_key_file =
|
||||
Some(data_dir.join(DEFAULT_PRIVATE_IDENTITY_KEY_FILENAME));
|
||||
self.public_identity_key_file =
|
||||
Some(data_dir.join(DEFAULT_PUBLIC_IDENTITY_KEY_FILENAME));
|
||||
|
||||
let keypaths = nym_pemstore::KeyPairPath::new(
|
||||
self.private_identity_key_file.as_ref().unwrap(),
|
||||
self.public_identity_key_file.as_ref().unwrap(),
|
||||
);
|
||||
|
||||
let mut rng = OsRng;
|
||||
let keypair = identity::KeyPair::new(&mut rng);
|
||||
|
||||
nym_pemstore::store_keypair(&keypair, &keypaths)
|
||||
.context("failed to store identity keys of the nym api")?;
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_identity(&self) -> anyhow::Result<identity::KeyPair> {
|
||||
// if somebody has set their private key but removed public key, the panic is totally on them.
|
||||
let keypaths = nym_pemstore::KeyPairPath::new(
|
||||
self.private_identity_key_file
|
||||
.as_ref()
|
||||
.ok_or(anyhow!("private key path is not specified"))?,
|
||||
self.public_identity_key_file
|
||||
.as_ref()
|
||||
.ok_or(anyhow!("public key path is not specified"))?,
|
||||
);
|
||||
|
||||
nym_pemstore::load_keypair(&keypaths).context("failed to load identity keys of the nym api")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,12 @@ vesting_contract_address = '{{ base.vesting_contract_address }}'
|
||||
# Mnemonic used for rewarding and validator interaction
|
||||
mnemonic = '{{ base.mnemonic }}'
|
||||
|
||||
[base.storage_paths]
|
||||
# Path to file containing private identity key of the nym-api.
|
||||
keys.private_identity_key_file = '{{ base.storage_paths.private_identity_key_file }}'
|
||||
|
||||
# Path to file containing public identity key of the nym-api.
|
||||
keys.public_identity_key_file = '{{ base.storage_paths.public_identity_key_file }}'
|
||||
|
||||
##### network monitor config options #####
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::circulating_supply_api::cache::CirculatingSupplyCache;
|
||||
use crate::coconut::{self, comm::QueryCommunicationChannel, InternalSignRequest};
|
||||
use crate::coconut::{self, comm::QueryCommunicationChannel};
|
||||
use crate::network::models::NetworkDetails;
|
||||
use crate::network::network_routes;
|
||||
use crate::node_describe_cache::DescribedNodes;
|
||||
@@ -13,6 +13,7 @@ use crate::support::config::Config;
|
||||
use crate::support::{nyxd, storage};
|
||||
use crate::{circulating_supply_api, nym_contract_cache, nym_nodes::nym_node_routes};
|
||||
use anyhow::Result;
|
||||
use nym_crypto::asymmetric::identity;
|
||||
use rocket::http::Method;
|
||||
use rocket::{Ignite, Rocket};
|
||||
use rocket_cors::{AllowedHeaders, AllowedOrigins, Cors};
|
||||
@@ -25,6 +26,7 @@ pub(crate) async fn setup_rocket(
|
||||
config: &Config,
|
||||
network_details: NetworkDetails,
|
||||
_nyxd_client: nyxd::Client,
|
||||
identity_keypair: identity::KeyPair,
|
||||
coconut_keypair: coconut::keypair::KeyPair,
|
||||
) -> anyhow::Result<Rocket<Ignite>> {
|
||||
let openapi_settings = rocket_okapi::settings::OpenApiSettings::default();
|
||||
@@ -66,9 +68,10 @@ pub(crate) async fn setup_rocket(
|
||||
|
||||
let rocket = if config.coconut_signer.enabled {
|
||||
let comm_channel = QueryCommunicationChannel::new(_nyxd_client.clone());
|
||||
rocket.attach(InternalSignRequest::stage(
|
||||
rocket.attach(coconut::stage(
|
||||
_nyxd_client.clone(),
|
||||
mix_denom,
|
||||
identity_keypair,
|
||||
coconut_keypair,
|
||||
comm_channel,
|
||||
storage.clone().unwrap(),
|
||||
|
||||
@@ -283,11 +283,13 @@ impl crate::coconut::client::Client for Client {
|
||||
self.client_address().await
|
||||
}
|
||||
|
||||
async fn get_tx(&self, tx_hash: &str) -> crate::coconut::error::Result<nyxd::TxResponse> {
|
||||
let tx_hash: Hash = tx_hash
|
||||
.parse()
|
||||
.map_err(|_| CoconutError::TxHashParseError)?;
|
||||
Ok(self.0.read().await.get_tx(tx_hash).await?)
|
||||
async fn get_tx(&self, tx_hash: Hash) -> crate::coconut::error::Result<nyxd::TxResponse> {
|
||||
self.0.read().await.get_tx(tx_hash).await.map_err(|source| {
|
||||
CoconutError::TxRetrievalFailure {
|
||||
tx_hash: tx_hash.to_string(),
|
||||
source,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_proposal(
|
||||
|
||||
@@ -487,15 +487,15 @@ impl StorageManager {
|
||||
|
||||
// insert the actual status
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO mixnode_status (mixnode_details_id, reliability, timestamp) VALUES (?, ?, ?);
|
||||
"#,
|
||||
mixnode_id,
|
||||
mixnode_result.reliability,
|
||||
timestamp
|
||||
)
|
||||
.execute(&mut tx)
|
||||
.await?;
|
||||
r#"
|
||||
INSERT INTO mixnode_status (mixnode_details_id, reliability, timestamp) VALUES (?, ?, ?);
|
||||
"#,
|
||||
mixnode_id,
|
||||
mixnode_result.reliability,
|
||||
timestamp
|
||||
)
|
||||
.execute(&mut tx)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// finally commit the transaction
|
||||
@@ -972,45 +972,4 @@ impl StorageManager {
|
||||
|
||||
Ok(active_day_statuses)
|
||||
}
|
||||
|
||||
/// Creates new encrypted blinded signature response entry for a given deposit tx hash.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `tx_hash`: hash of the deposit transaction.
|
||||
/// * `blinded_signature_response`: the encrypted blinded signature response.
|
||||
pub(crate) async fn insert_blinded_signature_response(
|
||||
&self,
|
||||
tx_hash: &str,
|
||||
blinded_signature_response: &str,
|
||||
) -> Result<(), sqlx::Error> {
|
||||
sqlx::query!(
|
||||
"INSERT INTO signed_deposit(tx_hash, blinded_signature_response) VALUES (?, ?)",
|
||||
tx_hash,
|
||||
blinded_signature_response
|
||||
)
|
||||
.execute(&self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tries to obtain encrypted blinded signature response for a given transaction hash.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `tx_hash`: transaction hash of the deposit.
|
||||
pub(crate) async fn get_blinded_signature_response(
|
||||
&self,
|
||||
tx_hash: &str,
|
||||
) -> Result<Option<String>, sqlx::Error> {
|
||||
let blinded_signature_response = sqlx::query!(
|
||||
"SELECT blinded_signature_response FROM signed_deposit WHERE tx_hash = ?",
|
||||
tx_hash
|
||||
)
|
||||
.fetch_optional(&self.connection_pool)
|
||||
.await?
|
||||
.map(|row| row.blinded_signature_response);
|
||||
|
||||
Ok(blinded_signature_response)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -716,25 +716,4 @@ impl NymApiStorage {
|
||||
.await
|
||||
.map_err(|err| err.into())
|
||||
}
|
||||
|
||||
pub(crate) async fn get_blinded_signature_response(
|
||||
&self,
|
||||
tx_hash: &str,
|
||||
) -> Result<Option<String>, NymApiStorageError> {
|
||||
self.manager
|
||||
.get_blinded_signature_response(tx_hash)
|
||||
.await
|
||||
.map_err(|err| err.into())
|
||||
}
|
||||
|
||||
pub(crate) async fn insert_blinded_signature_response(
|
||||
&self,
|
||||
tx_hash: &str,
|
||||
blinded_signature_response: &str,
|
||||
) -> Result<(), NymApiStorageError> {
|
||||
self.manager
|
||||
.insert_blinded_signature_response(tx_hash, blinded_signature_response)
|
||||
.await
|
||||
.map_err(|err| err.into())
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+39
-95
@@ -569,27 +569,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "bls12_381"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "54757888b09a69be70b5ec303e382a74227392086ba808cb01eeca29233a2397"
|
||||
version = "0.8.0"
|
||||
source = "git+https://github.com/jstuczyn/bls12_381?branch=feature/gt-serialization-0.8.0#c4543fde7d02efea6ecfcf22e14476ddb516b483"
|
||||
dependencies = [
|
||||
"digest 0.9.0",
|
||||
"ff 0.10.1",
|
||||
"group 0.10.0",
|
||||
"pairing 0.20.0",
|
||||
"rand_core 0.6.4",
|
||||
"subtle 2.4.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bls12_381"
|
||||
version = "0.6.0"
|
||||
source = "git+https://github.com/jstuczyn/bls12_381?branch=gt-serialisation#10fb6f700bfda17c8475af3bfd31e3fec15f2278"
|
||||
dependencies = [
|
||||
"digest 0.9.0",
|
||||
"ff 0.11.1",
|
||||
"group 0.11.0",
|
||||
"pairing 0.21.0",
|
||||
"ff 0.13.0",
|
||||
"group 0.13.0",
|
||||
"pairing",
|
||||
"rand_core 0.6.4",
|
||||
"subtle 2.4.1",
|
||||
"zeroize",
|
||||
@@ -1352,6 +1338,7 @@ dependencies = [
|
||||
"byteorder",
|
||||
"digest 0.9.0",
|
||||
"rand_core 0.5.1",
|
||||
"serde",
|
||||
"subtle 2.4.1",
|
||||
"zeroize",
|
||||
]
|
||||
@@ -1779,6 +1766,7 @@ version = "1.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91cff35c70bba8a626e3185d8cd48cc11b5437e1a5bcd15b9b5fa3c64b6dfee7"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"signature 1.6.4",
|
||||
]
|
||||
|
||||
@@ -1815,6 +1803,7 @@ dependencies = [
|
||||
"ed25519 1.5.3",
|
||||
"rand 0.7.3",
|
||||
"serde",
|
||||
"serde_bytes",
|
||||
"sha2 0.9.9",
|
||||
"zeroize",
|
||||
]
|
||||
@@ -2038,26 +2027,6 @@ dependencies = [
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ff"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0f40b2dcd8bc322217a5f6559ae5f9e9d1de202a2ecee2e9eafcbece7562a4f"
|
||||
dependencies = [
|
||||
"rand_core 0.6.4",
|
||||
"subtle 2.4.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ff"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "131655483be284720a17d74ff97592b8e76576dc25563148601df2d7c9080924"
|
||||
dependencies = [
|
||||
"rand_core 0.6.4",
|
||||
"subtle 2.4.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ff"
|
||||
version = "0.12.1"
|
||||
@@ -2074,6 +2043,7 @@ version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449"
|
||||
dependencies = [
|
||||
"bitvec",
|
||||
"rand_core 0.6.4",
|
||||
"subtle 2.4.1",
|
||||
]
|
||||
@@ -2663,30 +2633,6 @@ dependencies = [
|
||||
"system-deps 6.1.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "group"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c363a5301b8f153d80747126a04b3c82073b9fe3130571a9d170cacdeaf7912"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"ff 0.10.1",
|
||||
"rand_core 0.6.4",
|
||||
"subtle 2.4.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "group"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc5ac374b108929de78460075f3dc439fa66df9d8fc77e8f12caa5165fcf0c89"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"ff 0.11.1",
|
||||
"rand_core 0.6.4",
|
||||
"subtle 2.4.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "group"
|
||||
version = "0.12.1"
|
||||
@@ -3411,9 +3357,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.147"
|
||||
version = "0.2.150"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
|
||||
checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c"
|
||||
|
||||
[[package]]
|
||||
name = "libgit2-sys"
|
||||
@@ -3656,9 +3602,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "0.8.8"
|
||||
version = "0.8.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2"
|
||||
checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
@@ -3860,10 +3806,12 @@ dependencies = [
|
||||
"cosmwasm-std",
|
||||
"getset",
|
||||
"nym-coconut-interface",
|
||||
"nym-crypto",
|
||||
"nym-mixnet-contract-common",
|
||||
"nym-node-requests",
|
||||
"schemars",
|
||||
"serde",
|
||||
"tendermint",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3880,6 +3828,7 @@ dependencies = [
|
||||
"rand 0.7.3",
|
||||
"thiserror",
|
||||
"url",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3950,12 +3899,12 @@ dependencies = [
|
||||
name = "nym-coconut"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"bls12_381 0.6.0",
|
||||
"bls12_381",
|
||||
"bs58 0.4.0",
|
||||
"digest 0.9.0",
|
||||
"ff 0.11.1",
|
||||
"ff 0.13.0",
|
||||
"getrandom 0.2.10",
|
||||
"group 0.11.0",
|
||||
"group 0.13.0",
|
||||
"itertools",
|
||||
"nym-dkg",
|
||||
"nym-pemstore",
|
||||
@@ -4088,7 +4037,7 @@ dependencies = [
|
||||
name = "nym-credentials"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bls12_381 0.5.0",
|
||||
"bls12_381",
|
||||
"cosmrs",
|
||||
"log",
|
||||
"nym-api-requests",
|
||||
@@ -4096,6 +4045,7 @@ dependencies = [
|
||||
"nym-crypto",
|
||||
"nym-validator-client",
|
||||
"thiserror",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4115,6 +4065,8 @@ dependencies = [
|
||||
"nym-pemstore",
|
||||
"nym-sphinx-types",
|
||||
"rand 0.7.3",
|
||||
"serde",
|
||||
"serde_bytes",
|
||||
"subtle-encoding",
|
||||
"thiserror",
|
||||
"x25519-dalek 1.1.1",
|
||||
@@ -4126,10 +4078,10 @@ name = "nym-dkg"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bitvec",
|
||||
"bls12_381 0.6.0",
|
||||
"bls12_381",
|
||||
"bs58 0.4.0",
|
||||
"ff 0.11.1",
|
||||
"group 0.11.0",
|
||||
"ff 0.13.0",
|
||||
"group 0.13.0",
|
||||
"lazy_static",
|
||||
"nym-pemstore",
|
||||
"rand 0.8.5",
|
||||
@@ -4871,20 +4823,11 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
||||
|
||||
[[package]]
|
||||
name = "pairing"
|
||||
version = "0.20.0"
|
||||
version = "0.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7de9d09263c9966e8196fe0380c9dbbc7ea114b5cf371ba29004bc1f9c6db7f3"
|
||||
checksum = "81fec4625e73cf41ef4bb6846cafa6d44736525f442ba45e407c4a000a13996f"
|
||||
dependencies = [
|
||||
"group 0.10.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pairing"
|
||||
version = "0.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2e415e349a3006dd7d9482cdab1c980a845bed1377777d768cb693a44540b42"
|
||||
dependencies = [
|
||||
"group 0.11.0",
|
||||
"group 0.13.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6494,9 +6437,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.5.3"
|
||||
version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877"
|
||||
checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.48.0",
|
||||
@@ -7448,9 +7391,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.31.0"
|
||||
version = "1.34.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40de3a2ba249dcb097e01be5e67a5ff53cf250397715a071a81543e8a832a920"
|
||||
checksum = "d0c014766411e834f7af5b8f4cf46257aab4036ca95e9d2c144a10f59ad6f5b9"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"bytes",
|
||||
@@ -7459,16 +7402,16 @@ dependencies = [
|
||||
"num_cpus",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"socket2 0.5.3",
|
||||
"socket2 0.5.5",
|
||||
"tokio-macros",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.1.0"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
|
||||
checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -7557,9 +7500,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.8"
|
||||
version = "0.7.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d"
|
||||
checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
@@ -8625,6 +8568,7 @@ checksum = "5a0c105152107e3b96f6a00a65e86ce82d9b125230e1c4302940eca58ff71f4f"
|
||||
dependencies = [
|
||||
"curve25519-dalek 3.2.0",
|
||||
"rand_core 0.5.1",
|
||||
"serde",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ nym-bin-common = { path = "../../../common/bin-common" }
|
||||
# extra dependencies for libp2p examples
|
||||
libp2p = { git = "https://github.com/ChainSafe/rust-libp2p.git", rev = "e3440d25681df380c9f0f8cfdcfd5ecc0a4f2fb6", features = [ "identify", "macros", "ping", "tokio", "tcp", "dns", "websocket", "noise", "mplex", "yamux", "gossipsub" ]}
|
||||
tokio-stream = "0.1.12"
|
||||
tokio-util = { version = "0.7", features = ["codec"] }
|
||||
tokio-util = { workspace = true, features = ["codec"] }
|
||||
parking_lot = "0.12"
|
||||
hex = "0.4"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user