Compare commits

...

33 Commits

Author SHA1 Message Date
Jędrzej Stuczyński 2ea8bea486 clippy 2024-02-13 10:35:24 +00:00
Jędrzej Stuczyński 6fdf0c4c2e locally marking credentials as spent 2024-02-13 09:53:13 +00:00
Jędrzej Stuczyński ec248ec721 added database code for the serial number storage 2024-02-13 09:44:18 +00:00
Jędrzej Stuczyński 288baea49f cargo fmt 2024-02-12 18:37:46 +00:00
Jędrzej Stuczyński 4a0e623e3d removing redundant epoch_id field 2024-02-12 17:29:57 +00:00
Jędrzej Stuczyński 3ecf835a45 clippy 2024-02-12 17:29:57 +00:00
Jędrzej Stuczyński 7258f83b47 nym-cli commands for issuing free passes 2024-02-12 17:29:57 +00:00
Jędrzej Stuczyński e7c04863d1 clippy 2024-02-12 17:29:44 +00:00
Jędrzej Stuczyński 1a0424d57d cargo fmt 2024-02-12 17:01:08 +00:00
Jędrzej Stuczyński f0666b33d2 validating request attributes 2024-02-09 17:37:41 +00:00
Jędrzej Stuczyński b4880db840 storage implementation 2024-02-09 17:37:41 +00:00
Jędrzej Stuczyński 370cc36fe3 nym-api logic for issuing free passes (minus storage impl) 2024-02-09 17:37:41 +00:00
Jędrzej Stuczyński 9cc533b672 request type for obtaining free pass 2024-02-09 17:37:41 +00:00
benedettadavico 5b22671144 cargo fmt 2024-02-09 16:41:01 +01:00
benedettadavico f3f8326022 add return statement 2024-02-09 16:32:53 +01:00
Jędrzej Stuczyński 114228e24a ibid 2024-02-09 14:57:49 +00:00
benedettadavico dab29401be running cargo fmt 2024-02-09 15:32:35 +01:00
Jędrzej Stuczyński 3c13eef452 gateway downgrading advertised protocol for incompatible clients 2024-02-09 14:17:20 +00:00
Jędrzej Stuczyński bb3a7f40b6 fixed SQL type for epoch_id 2024-02-09 13:57:35 +00:00
Jędrzej Stuczyński 1e99358885 restored OldV1Credential::as_bytes to be available to non-test code 2024-02-09 10:14:34 +00:00
Jędrzej Stuczyński c5e9c3fbb9 reintroduced handling of old v1 credentials 2024-02-09 10:07:18 +00:00
Jędrzej Stuczyński 5e08135a68 clippy and fixing tests 2024-02-09 10:00:23 +00:00
Jędrzej Stuczyński e1c9ca5e20 missing serialization 2024-02-09 10:00:23 +00:00
Jędrzej Stuczyński d9e1257d9b persisting the issued credentials 2024-02-09 10:00:23 +00:00
Jędrzej Stuczyński 7598c951ed reintroduced recovery of vouchers 2024-02-09 10:00:23 +00:00
Jędrzej Stuczyński 62663d6d12 removed nym-api placeholders 2024-02-09 10:00:23 +00:00
Jędrzej Stuczyński 6ca9adcf0d gateway handling of both credential types 2024-02-09 10:00:23 +00:00
Jędrzej Stuczyński 7ecd4bc05e removed usage of coconut-interface crate 2024-02-09 10:00:23 +00:00
Jędrzej Stuczyński 67ce8a98a5 wip in removing the Credential type for more strongly typed alternative 2024-02-09 10:00:22 +00:00
Jędrzej Stuczyński b50468b566 wip 2024-02-09 10:00:22 +00:00
Jędrzej Stuczyński 85d404b225 using bincode serialization 2024-02-09 10:00:22 +00:00
Jędrzej Stuczyński f05998323a serde for 'IssuanceBandwidthCredential' 2024-02-09 10:00:22 +00:00
Jędrzej Stuczyński 279db3cc83 revamped BandwidthVoucher to allow for different kinds of bandwidth credentials 2024-02-09 10:00:22 +00:00
104 changed files with 4437 additions and 2617 deletions
Generated
+942 -919
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -27,7 +27,6 @@ members = [
"common/client-libs/gateway-client",
"common/client-libs/mixnet-client",
"common/client-libs/validator-client",
"common/coconut-interface",
"common/commands",
"common/config",
"common/cosmwasm-smart-contracts/coconut-bandwidth-contract",
@@ -43,6 +42,7 @@ members = [
"common/credential-storage",
"common/credentials",
"common/credential-utils",
"common/credentials-interface",
"common/crypto",
"common/dkg",
"common/execute",
-1
View File
@@ -38,7 +38,6 @@ tokio-tungstenite = { workspace = true }
nym-bandwidth-controller = { path = "../../common/bandwidth-controller" }
nym-bin-common = { path = "../../common/bin-common", features = ["output_format"] }
nym-client-core = { path = "../../common/client-core", features = ["fs-surb-storage", "cli"] }
nym-coconut-interface = { path = "../../common/coconut-interface" }
nym-config = { path = "../../common/config" }
nym-credential-storage = { path = "../../common/credential-storage" }
nym-credentials = { path = "../../common/credentials" }
-1
View File
@@ -23,7 +23,6 @@ url = { workspace = true }
# internal
nym-bin-common = { path = "../../common/bin-common", features = ["output_format"] }
nym-client-core = { path = "../../common/client-core", features = ["fs-surb-storage", "cli"] }
nym-coconut-interface = { path = "../../common/coconut-interface" }
nym-config = { path = "../../common/config" }
nym-credentials = { path = "../../common/credentials" }
nym-crypto = { path = "../../common/crypto" }
+3 -1
View File
@@ -8,14 +8,16 @@ license.workspace = true
[dependencies]
bip39 = { workspace = true }
log = { workspace = true }
rand = "0.7.3"
thiserror = { workspace = true }
url = { workspace = true }
zeroize = { workspace = true }
nym-coconut-interface = { path = "../coconut-interface" }
nym-coconut = { path = "../nymcoconut" }
nym-credential-storage = { path = "../credential-storage" }
nym-credentials = { path = "../credentials" }
nym-credentials-interface = { path = "../credentials-interface" }
nym-crypto = { path = "../crypto", features = ["rand", "asymmetric", "symmetric", "aes", "hashing"] }
nym-network-defaults = { path = "../network-defaults" }
nym-validator-client = { path = "../client-libs/validator-client", default-features = false }
+28 -32
View File
@@ -2,18 +2,18 @@
// SPDX-License-Identifier: Apache-2.0
use crate::error::BandwidthControllerError;
use nym_coconut_interface::Base58;
use nym_credential_storage::models::StorableIssuedCredential;
use nym_credential_storage::storage::Storage;
use nym_credentials::coconut::bandwidth::BandwidthVoucher;
use nym_credentials::coconut::bandwidth::{CredentialType, IssuanceBandwidthCredential};
use nym_credentials::coconut::utils::obtain_aggregate_signature;
use nym_crypto::asymmetric::{encryption, identity};
use nym_network_defaults::VOUCHER_INFO;
use nym_validator_client::coconut::all_coconut_api_clients;
use nym_validator_client::nyxd::contract_traits::CoconutBandwidthSigningClient;
use nym_validator_client::nyxd::contract_traits::DkgQueryClient;
use nym_validator_client::nyxd::Coin;
use rand::rngs::OsRng;
use state::State;
use zeroize::Zeroizing;
pub mod state;
@@ -24,13 +24,11 @@ where
let mut rng = OsRng;
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),
amount.clone(),
CredentialType::Voucher.to_string(),
signing_key.public_key().to_base58_string(),
encryption_key.public_key().to_base58_string(),
None,
@@ -38,21 +36,15 @@ where
.await?
.transaction_hash;
let voucher = BandwidthVoucher::new(
&params,
voucher_value,
VOUCHER_INFO.to_string(),
tx_hash,
signing_key,
encryption_key,
);
let voucher =
IssuanceBandwidthCredential::new_voucher(amount, tx_hash, signing_key, encryption_key);
let state = State { voucher, params };
let state = State { voucher };
Ok(state)
}
pub async fn get_credential<C, St>(
pub async fn get_bandwidth_voucher<C, St>(
state: &State,
client: &C,
storage: &St,
@@ -62,6 +54,9 @@ where
St: Storage,
<St as Storage>::StorageError: Send + Sync + 'static,
{
// temporary
assert!(state.voucher.typ().is_voucher());
let epoch_id = client.get_current_epoch().await?.epoch_id;
let threshold = client
.get_current_epoch_threshold()
@@ -70,22 +65,23 @@ where
let coconut_api_clients = all_coconut_api_clients(client, epoch_id).await?;
let signature = obtain_aggregate_signature(
&state.params,
&state.voucher,
&coconut_api_clients,
threshold,
)
.await?;
let signature =
obtain_aggregate_signature(&state.voucher, &coconut_api_clients, threshold).await?;
let issued = state.voucher.to_issued_credential(signature, epoch_id);
// make sure the data gets zeroized after persisting it
let credential_data = Zeroizing::new(issued.pack_v1());
let storable = StorableIssuedCredential {
serialization_revision: issued.current_serialization_revision(),
credential_data: credential_data.as_ref(),
credential_type: issued.typ().to_string(),
epoch_id: epoch_id
.try_into()
.expect("our epoch is has run over u32::MAX!"),
};
storage
.insert_coconut_credential(
state.voucher.get_voucher_value(),
VOUCHER_INFO.to_string(),
state.voucher.get_private_attributes()[0].to_bs58(),
state.voucher.get_private_attributes()[1].to_bs58(),
signature.to_bs58(),
epoch_id.to_string(),
)
.insert_issued_credential(storable)
.await
.map_err(|err| BandwidthControllerError::CredentialStorageError(Box::new(err)))
}
@@ -1,19 +1,14 @@
// Copyright 2022-2023 - Nym Technologies SA <contact@nymtech.net>
// Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_coconut_interface::Parameters;
use nym_credentials::coconut::bandwidth::BandwidthVoucher;
use nym_credentials::coconut::bandwidth::IssuanceBandwidthCredential;
pub struct State {
pub voucher: BandwidthVoucher,
pub params: Parameters,
pub voucher: IssuanceBandwidthCredential,
}
impl State {
pub fn new(voucher: BandwidthVoucher) -> Self {
State {
voucher,
params: BandwidthVoucher::default_parameters(),
}
pub fn new(voucher: IssuanceBandwidthCredential) -> Self {
State { voucher }
}
}
+4 -1
View File
@@ -1,7 +1,7 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_coconut_interface::CoconutError;
use nym_coconut::CoconutError;
use nym_credential_storage::error::StorageError;
use nym_credentials::error::Error as CredentialsError;
use nym_crypto::asymmetric::encryption::KeyRecoveryError;
@@ -45,4 +45,7 @@ pub enum BandwidthControllerError {
#[error("Threshold not set yet")]
NoThreshold,
#[error("can't handle recovering storage with revision {stored}. {expected} was expected")]
UnsupportedCredentialStorageRevision { stored: u8, expected: u8 },
}
+58 -43
View File
@@ -1,28 +1,38 @@
// Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net>
// Copyright 2021-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::error::BandwidthControllerError;
use nym_credential_storage::error::StorageError;
use crate::utils::stored_credential_to_issued_bandwidth;
use log::{error, warn};
use nym_credential_storage::storage::Storage;
use nym_credentials::coconut::bandwidth::CredentialSpendingData;
use nym_credentials::coconut::utils::obtain_aggregate_verification_key;
use nym_credentials_interface::VerificationKey;
use nym_validator_client::coconut::all_coconut_api_clients;
use nym_validator_client::nym_api::EpochId;
use nym_validator_client::nyxd::contract_traits::DkgQueryClient;
use std::str::FromStr;
use zeroize::Zeroizing;
use {
nym_coconut_interface::Base58,
nym_credentials::coconut::{
bandwidth::prepare_for_spending, utils::obtain_aggregate_verification_key,
},
};
pub mod acquire;
pub mod error;
mod utils;
pub struct BandwidthController<C, St> {
storage: St,
client: C,
}
pub struct PreparedCredential {
/// The cryptographic material required for spending the underlying credential.
pub data: CredentialSpendingData,
/// The (DKG) epoch id under which the credential has been issued so that the verifier
/// could use correct verification key for validation.
pub epoch_id: EpochId,
/// The database id of the stored credential.
pub credential_id: i64,
}
impl<C, St: Storage> BandwidthController<C, St> {
pub fn new(storage: St, client: C) -> Self {
BandwidthController { storage, client }
@@ -32,49 +42,54 @@ impl<C, St: Storage> BandwidthController<C, St> {
&self.storage
}
pub async fn prepare_coconut_credential(
async fn get_aggregate_verification_key(
&self,
) -> Result<(nym_coconut_interface::Credential, i64), BandwidthControllerError>
epoch_id: EpochId,
) -> Result<VerificationKey, BandwidthControllerError>
where
C: DkgQueryClient + Sync + Send,
<St as Storage>::StorageError: Send + Sync + 'static,
{
let bandwidth_credential = self
let coconut_api_clients = all_coconut_api_clients(&self.client, epoch_id).await?;
Ok(obtain_aggregate_verification_key(&coconut_api_clients)?)
}
pub async fn prepare_bandwidth_credential(
&self,
) -> Result<PreparedCredential, BandwidthControllerError>
where
C: DkgQueryClient + Sync + Send,
<St as Storage>::StorageError: Send + Sync + 'static,
{
let retrieved_credential = self
.storage
.get_next_coconut_credential()
.get_next_unspent_credential()
.await
.map_err(|err| BandwidthControllerError::CredentialStorageError(Box::new(err)))?;
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 = 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)
.map_err(|_| StorageError::InconsistentData)?;
let coconut_api_clients = all_coconut_api_clients(&self.client, epoch_id).await?;
let epoch_id = retrieved_credential.epoch_id as EpochId;
let credential_id = retrieved_credential.id;
let verification_key = obtain_aggregate_verification_key(&coconut_api_clients).await?;
let issued_bandwidth = stored_credential_to_issued_bandwidth(retrieved_credential)?;
// the below would only be executed once we know where we want to spend it (i.e. which gateway and stuff)
Ok((
prepare_for_spending(
voucher_value,
voucher_info,
&serial_number,
&binding_number,
epoch_id,
&signature,
&verification_key,
)?,
bandwidth_credential.id,
))
let verification_key = match self.get_aggregate_verification_key(epoch_id).await {
Ok(key) => key,
Err(err) => {
warn!("failed to obtain master verification key: {err}. Putting the credential back into the database");
// TODO: ERROR RECOVERY:
error!("unimplemented: putting the credential back into the database");
return Err(err);
}
};
let spend_request = issued_bandwidth.prepare_for_spending(&verification_key)?;
Ok(PreparedCredential {
data: spend_request,
epoch_id,
credential_id,
})
}
pub async fn consume_credential(&self, id: i64) -> Result<(), BandwidthControllerError>
@@ -93,7 +108,7 @@ impl<C, St: Storage> BandwidthController<C, St> {
impl<C, St> Clone for BandwidthController<C, St>
where
C: Clone,
St: Storage + Clone,
St: Clone,
{
fn clone(&self) -> Self {
BandwidthController {
+22
View File
@@ -0,0 +1,22 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::error::BandwidthControllerError;
use nym_credential_storage::models::StoredIssuedCredential;
use nym_credentials::coconut::bandwidth::issued::CURRENT_SERIALIZATION_REVISION;
use nym_credentials::coconut::bandwidth::IssuedBandwidthCredential;
pub fn stored_credential_to_issued_bandwidth(
cred: StoredIssuedCredential,
) -> Result<IssuedBandwidthCredential, BandwidthControllerError> {
if cred.serialization_revision != CURRENT_SERIALIZATION_REVISION {
return Err(
BandwidthControllerError::UnsupportedCredentialStorageRevision {
stored: cred.serialization_revision,
expected: CURRENT_SERIALIZATION_REVISION,
},
);
}
Ok(IssuedBandwidthCredential::unpack_v1(&cred.credential_data)?)
}
+1 -1
View File
@@ -19,7 +19,7 @@ tokio = { version = "1.24.1", features = ["macros"] }
# internal
nym-bandwidth-controller = { path = "../../bandwidth-controller" }
nym-coconut-interface = { path = "../../coconut-interface" }
nym-credentials = { path = "../../credentials" }
nym-credential-storage = { path = "../../credential-storage" }
nym-crypto = { path = "../../crypto" }
nym-gateway-requests = { path = "../../../gateway/gateway-requests" }
+26 -13
View File
@@ -1,4 +1,4 @@
// Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net>
// Copyright 2021-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::error::GatewayClientError;
@@ -12,14 +12,16 @@ use crate::{cleanup_socket_message, try_decrypt_binary_message};
use futures::{SinkExt, StreamExt};
use log::*;
use nym_bandwidth_controller::BandwidthController;
use nym_coconut_interface::Credential;
use nym_credential_storage::ephemeral_storage::EphemeralStorage as EphemeralCredentialStorage;
use nym_credential_storage::storage::Storage as CredentialStorage;
use nym_credentials::CredentialSpendingData;
use nym_crypto::asymmetric::identity;
use nym_gateway_requests::authentication::encrypted_address::EncryptedAddressBytes;
use nym_gateway_requests::iv::IV;
use nym_gateway_requests::registration::handshake::{client_handshake, SharedKeys};
use nym_gateway_requests::{BinaryRequest, ClientControlRequest, ServerResponse, PROTOCOL_VERSION};
use nym_gateway_requests::{
BinaryRequest, ClientControlRequest, ServerResponse, CURRENT_PROTOCOL_VERSION,
};
use nym_network_defaults::{REMAINING_BANDWIDTH_THRESHOLD, TOKENS_TO_BURN};
use nym_sphinx::forwarding::packet::MixPacket;
use nym_task::TaskClient;
@@ -79,6 +81,9 @@ pub struct GatewayClient<C, St = EphemeralCredentialStorage> {
/// Delay between each subsequent reconnection attempt.
reconnection_backoff: Duration,
// currently unused (but populated)
negotiated_protocol: Option<u8>,
/// Listen to shutdown messages.
shutdown: TaskClient,
}
@@ -108,6 +113,7 @@ impl<C, St> GatewayClient<C, St> {
should_reconnect_on_failure: true,
reconnection_attempts: DEFAULT_RECONNECTION_ATTEMPTS,
reconnection_backoff: DEFAULT_RECONNECTION_BACKOFF,
negotiated_protocol: None,
shutdown,
}
}
@@ -383,17 +389,17 @@ impl<C, St> GatewayClient<C, St> {
// note: in +1.2.0 we will have to return a hard error here
Ok(())
}
Some(v) if v != PROTOCOL_VERSION => {
Some(v) if v > CURRENT_PROTOCOL_VERSION => {
let err = GatewayClientError::IncompatibleProtocol {
gateway: Some(v),
current: PROTOCOL_VERSION,
current: CURRENT_PROTOCOL_VERSION,
};
error!("{err}");
Err(err)
}
Some(_) => {
info!("the gateway is using exactly the same protocol version as we are. We're good to continue!");
info!("the gateway is using exactly the same (or older) protocol version as we are. We're good to continue!");
Ok(())
}
}
@@ -439,6 +445,10 @@ impl<C, St> GatewayClient<C, St> {
if self.authenticated {
self.shared_key = Some(Arc::new(shared_key));
}
// populate the negotiated protocol for future uses
self.negotiated_protocol = gateway_protocol;
Ok(())
}
@@ -515,13 +525,13 @@ impl<C, St> GatewayClient<C, St> {
async fn claim_coconut_bandwidth(
&mut self,
credential: Credential,
credential: CredentialSpendingData,
) -> Result<(), GatewayClientError> {
let mut rng = OsRng;
let iv = IV::new_random(&mut rng);
let msg = ClientControlRequest::new_enc_coconut_bandwidth_credential(
&credential,
let msg = ClientControlRequest::new_enc_coconut_bandwidth_credential_v2(
credential,
self.shared_key.as_ref().unwrap(),
iv,
)
@@ -567,18 +577,19 @@ impl<C, St> GatewayClient<C, St> {
return self.try_claim_testnet_bandwidth().await;
}
let (credential, credential_id) = self
let prepared_credential = self
.bandwidth_controller
.as_ref()
.unwrap()
.prepare_coconut_credential()
.prepare_bandwidth_credential()
.await?;
self.claim_coconut_bandwidth(credential).await?;
self.claim_coconut_bandwidth(prepared_credential.data)
.await?;
self.bandwidth_controller
.as_ref()
.unwrap()
.consume_credential(credential_id)
.consume_credential(prepared_credential.credential_id)
.await?;
Ok(())
@@ -817,6 +828,7 @@ impl GatewayClient<InitOnly, EphemeralCredentialStorage> {
should_reconnect_on_failure: false,
reconnection_attempts: DEFAULT_RECONNECTION_ATTEMPTS,
reconnection_backoff: DEFAULT_RECONNECTION_BACKOFF,
negotiated_protocol: None,
shutdown,
}
}
@@ -848,6 +860,7 @@ impl GatewayClient<InitOnly, EphemeralCredentialStorage> {
should_reconnect_on_failure: self.should_reconnect_on_failure,
reconnection_attempts: self.reconnection_attempts,
reconnection_backoff: self.reconnection_backoff,
negotiated_protocol: self.negotiated_protocol,
shutdown,
}
}
@@ -33,7 +33,7 @@ tokio = { workspace = true, features = ["sync", "time"] }
futures = { workspace = true }
openssl = { version = "^0.10.55", features = ["vendored"], optional = true }
nym-coconut-interface = { path = "../../coconut-interface" }
nym-coconut = { path = "../../nymcoconut" }
nym-network-defaults = { path = "../../network-defaults" }
nym-api-requests = { path = "../../../nym-api/nym-api-requests" }
@@ -8,8 +8,10 @@ use crate::{
nym_api, DirectSigningReqwestRpcValidatorClient, QueryReqwestRpcValidatorClient,
ReqwestRpcClient, ValidatorClientError,
};
use nym_api_requests::coconut::models::FreePassNonceResponse;
use nym_api_requests::coconut::{
BlindSignRequestBody, BlindedSignatureResponse, VerifyCredentialBody, VerifyCredentialResponse,
BlindSignRequestBody, BlindedSignatureResponse, FreePassRequest, VerifyCredentialBody,
VerifyCredentialResponse,
};
use nym_api_requests::models::{DescribedGateway, MixNodeBondAnnotated};
use nym_api_requests::models::{
@@ -348,4 +350,15 @@ impl NymApiClient {
.verify_bandwidth_credential(request_body)
.await?)
}
pub async fn free_pass_nonce(&self) -> Result<FreePassNonceResponse, ValidatorClientError> {
Ok(self.nym_api.free_pass_nonce().await?)
}
pub async fn issue_free_pass_credential(
&self,
request: &FreePassRequest,
) -> Result<BlindedSignatureResponse, ValidatorClientError> {
Ok(self.nym_api.free_pass(request).await?)
}
}
@@ -4,9 +4,9 @@
use crate::nyxd::contract_traits::{DkgQueryClient, PagedDkgQueryClient};
use crate::nyxd::error::NyxdError;
use crate::NymApiClient;
use nym_coconut::{Base58, CoconutError, VerificationKey};
use nym_coconut_dkg_common::types::{EpochId, NodeIndex};
use nym_coconut_dkg_common::verification_key::ContractVKShare;
use nym_coconut_interface::{Base58, CoconutError, VerificationKey};
use thiserror::Error;
use url::Url;
@@ -32,6 +32,8 @@ pub mod error;
pub mod routes;
pub use http_api_client::Client;
use nym_api_requests::coconut::models::FreePassNonceResponse;
use nym_api_requests::coconut::FreePassRequest;
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
@@ -373,6 +375,36 @@ pub trait NymApiClientExt: ApiClient {
.await
}
async fn free_pass_nonce(&self) -> Result<FreePassNonceResponse, NymAPIError> {
self.get_json(
&[
routes::API_VERSION,
routes::COCONUT_ROUTES,
routes::BANDWIDTH,
routes::COCONUT_FREE_PASS_NONCE,
],
NO_PARAMS,
)
.await
}
async fn free_pass(
&self,
request: &FreePassRequest,
) -> Result<BlindedSignatureResponse, NymAPIError> {
self.post_json(
&[
routes::API_VERSION,
routes::COCONUT_ROUTES,
routes::BANDWIDTH,
routes::COCONUT_FREE_PASS_NONCE,
],
NO_PARAMS,
request,
)
.await
}
async fn blind_sign(
&self,
request_body: &BlindSignRequestBody,
@@ -15,6 +15,8 @@ pub const REWARDED: &str = "rewarded";
pub const COCONUT_ROUTES: &str = "coconut";
pub const BANDWIDTH: &str = "bandwidth";
pub const COCONUT_FREE_PASS: &str = "free-pass";
pub const COCONUT_FREE_PASS_NONCE: &str = "free-pass-nonce";
pub const COCONUT_BLIND_SIGN: &str = "blind-sign";
pub const COCONUT_VERIFY_BANDWIDTH_CREDENTIAL: &str = "verify-bandwidth-credential";
pub const COCONUT_EPOCH_CREDENTIALS: &str = "epoch-credentials";
@@ -140,9 +140,6 @@ pub enum NyxdError {
#[error("Cosmwasm std error: {0}")]
CosmwasmStdError(#[from] cosmwasm_std::StdError),
#[error("Coconut interface error: {0}")]
CoconutInterfaceError(#[from] nym_coconut_interface::error::CoconutInterfaceError),
#[error("Account had an unexpected bech32 prefix. Expected: {expected}, got: {got}")]
UnexpectedBech32Prefix { got: String, expected: String },
}
@@ -359,6 +359,10 @@ where
S: OfflineSigner + Send + Sync,
NyxdError: From<<S as OfflineSigner>::Error>,
{
pub fn signing_account(&self) -> Result<AccountData, NyxdError> {
Ok(self.find_account(&self.address())?)
}
pub fn address(&self) -> AccountId {
match self.client.signer_addresses() {
Ok(addresses) => addresses[0].clone(),
-14
View File
@@ -1,14 +0,0 @@
[package]
name = "nym-coconut-interface"
version = "0.1.0"
edition = "2021"
description = "Crutch library until there is proper SerDe support for coconut structs"
license.workspace = true
[dependencies]
bs58 = "0.4.0"
getset = "0.1.1"
serde = { workspace = true, features = ["derive"] }
thiserror = { workspace = true }
nym-coconut = {path = "../nymcoconut" }
-17
View File
@@ -1,17 +0,0 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_coconut::CoconutError;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum CoconutInterfaceError {
#[error("not enough bytes: {0} received, minimum {1} required")]
InvalidByteLength(usize, usize),
#[error("Could not decode base 58 string - {0}")]
MalformedString(#[from] bs58::decode::Error),
#[error("Coconut error - {0}")]
CoconutError(#[from] CoconutError),
}
-196
View File
@@ -1,196 +0,0 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod error;
use getset::{CopyGetters, Getters};
use serde::{Deserialize, Serialize};
use error::CoconutInterfaceError;
// We list these explicity instead of glob export due to shadowing warnings with the pub tests
// module.
pub use nym_coconut::{
aggregate_signature_shares, aggregate_verification_keys, blind_sign, hash_to_scalar,
prepare_blind_sign, prove_bandwidth_credential, Attribute, Base58, BlindSignRequest,
BlindedSignature, Bytable, CoconutError, KeyPair, Parameters, PrivateAttribute,
PublicAttribute, SecretKey, Signature, SignatureShare, Theta, VerificationKey,
};
#[derive(Debug, Serialize, Deserialize, Getters, CopyGetters, Clone, PartialEq, Eq)]
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,
}
impl Credential {
pub fn new(
n_params: u32,
theta: Theta,
voucher_value: u64,
voucher_info: String,
epoch_id: u64,
) -> Credential {
Credential {
n_params,
theta,
voucher_value,
voucher_info,
epoch_id,
}
}
pub fn blinded_serial_number(&self) -> String {
self.theta.blinded_serial_number_bs58()
}
pub fn has_blinded_serial_number(
&self,
blinded_serial_number_bs58: &str,
) -> Result<bool, CoconutInterfaceError> {
Ok(self
.theta
.has_blinded_serial_number(blinded_serial_number_bs58)?)
}
pub fn voucher_value(&self) -> u64 {
self.voucher_value
}
pub fn verify(&self, verification_key: &VerificationKey) -> bool {
let params = Parameters::new(self.n_params).unwrap();
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(&params, verification_key, &self.theta, public_attributes)
}
pub fn as_bytes(&self) -> Vec<u8> {
let n_params_bytes = self.n_params.to_be_bytes();
let theta_bytes = self.theta.to_bytes();
let theta_bytes_len = theta_bytes.len();
let voucher_value_bytes = self.voucher_value.to_be_bytes();
let epoch_id_bytes = self.epoch_id.to_be_bytes();
let voucher_info_bytes = self.voucher_info.as_bytes();
let voucher_info_len = voucher_info_bytes.len();
let mut bytes = Vec::with_capacity(28 + theta_bytes_len + voucher_info_len);
bytes.extend_from_slice(&n_params_bytes);
bytes.extend_from_slice(&(theta_bytes_len as u64).to_be_bytes());
bytes.extend_from_slice(&theta_bytes);
bytes.extend_from_slice(&voucher_value_bytes);
bytes.extend_from_slice(&epoch_id_bytes);
bytes.extend_from_slice(voucher_info_bytes);
bytes
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, CoconutError> {
if bytes.len() < 28 {
return Err(CoconutError::Deserialization(String::from(
"To few bytes in credential",
)));
}
let mut four_byte = [0u8; 4];
let mut eight_byte = [0u8; 8];
four_byte.copy_from_slice(&bytes[..4]);
let n_params = u32::from_be_bytes(four_byte);
eight_byte.copy_from_slice(&bytes[4..12]);
let theta_len = u64::from_be_bytes(eight_byte);
if bytes.len() < 28 + theta_len as usize {
return Err(CoconutError::Deserialization(String::from(
"To few bytes in credential",
)));
}
let theta = Theta::from_bytes(&bytes[12..12 + theta_len as usize])
.map_err(|e| CoconutError::Deserialization(e.to_string()))?;
eight_byte.copy_from_slice(&bytes[12 + theta_len as usize..20 + theta_len as usize]);
let voucher_value = u64::from_be_bytes(eight_byte);
eight_byte.copy_from_slice(&bytes[20 + theta_len as usize..28 + theta_len as usize]);
let epoch_id = u64::from_be_bytes(eight_byte);
let voucher_info = String::from_utf8(bytes[28 + theta_len as usize..].to_vec())
.map_err(|e| CoconutError::Deserialization(e.to_string()))?;
Ok(Credential {
n_params,
theta,
voucher_value,
voucher_info,
epoch_id,
})
}
}
impl Bytable for Credential {
fn to_byte_vec(&self) -> Vec<u8> {
self.as_bytes()
}
fn try_from_byte_slice(slice: &[u8]) -> Result<Self, CoconutError> {
Credential::from_bytes(slice)
}
}
impl Base58 for Credential {}
#[cfg(test)]
mod tests {
use nym_coconut::{prove_bandwidth_credential, Signature};
use super::*;
#[test]
fn serde_coconut_credential() {
let voucher_value = 1000000u64;
let voucher_info = String::from("BandwidthVoucher");
let serial_number =
Attribute::try_from_bs58("7Rp3imcuNX3w9se9wm5th8gSvc2czsnMrGsdt5HsrycA").unwrap();
let binding_number =
Attribute::try_from_bs58("Auf8yVEgyEAWNHaXUZmimS4n9g5YiYnNYqp6F9BtBe9E").unwrap();
let signature = Signature::try_from_bs58(
"ta3pM9ffj5T6YGbwjSBp2W118rcwyP9PXStc\
7ssb91g5GQYMQHhuTNajbdZcjxUFBFL5rhED8EHpRzE8r432ss3qbPBfpNev4CdkfMkQ3wepyM7hy7q1W6Rn9WmFoZL\
ZR9j",
)
.unwrap();
let params = Parameters::new(4).unwrap();
let verification_key = VerificationKey::try_from_bs58("8CFtVVXdwLy4WHMQPE4\
woe89q3DRHoNxBSchftrEjSBPWA4r4xZv4Y9qSvS5x5bMmFtp7BX6ikECAnuXr5EjXWSsgjirZJmpS5XDUynVfht1cD\
FWGDvy2XFrRCuoCMotNXi3PoF6wYqdTR9Rqcfoj3i2H5Nid422WBaLtVoC9QNobvpvaqq6vX5PbsSyPayvU8HCXFxM6\
JjScYpbRTxQtdwefWLrk3LmXyJQBWi7c2VAhSxu9msp7VTBycqdwQNgxHETStZuwXsozxaGQ2KssVUCaaoYPR4g2RqK\
UAvtWwA7pMiAQNcbkXcbsjCgVjWaCpMWC37XA31cLcFf3zbjHD9e5tXjAcqa4M89fbFhuvvSXxowSAZ5NoWrN32kd5d\
wxJm1JW3Tt2h6yDDBe84oMy71462dZn7N78DVk2mFNGwBCibrZWA7oUzRBMfYxiQrksoFcou7QfLLd58zoNYmPQPt84\
1VpQopEBfdQ7Nf9zoXxBt3zMy7g5NsFGvzh7KTbDUyeeXrdkKJPQBs6dqaizr9sS8CPPmR4uk96vDTRh8CJ5FbSsmb8\
nP71dRvvwRZJHGzwYirMo6SXS3ZYxFuiA3mkxYuqDHCwkTWDuRCcAaztrDYRZg7VCMo4Q446AaEso5eqpeWpHZQt53E\
ZRpqmNYKASGwMhTeEHPSLgSmtoAAUcaRWpGRzYfd6kzEma8tdGLwyP4rLXgvSvtDLP37dU7YgF3LEXbGAz57U9ATy46\
6sroLpHPdaCWB8RF11wvB6Tu196JnJd2KyQBP1iUWP3rtZs3GhAF1QVcxquh8BqDZzAcpQ6wCS1P9c5GxKgww77FVF5\
Kp83XtoxSrw3GaYVyKTGxNh3vcKPR31txCjTxPaN2fg7TaPLhoQJX4YaAroFSXqrqbbRsisuHhhCeUP2YwDjHedes9y")
.unwrap();
let theta = prove_bandwidth_credential(
&params,
&verification_key,
&signature,
&serial_number,
&binding_number,
)
.unwrap();
let credential = Credential::new(4, theta, voucher_value, voucher_info, 42);
let serialized_credential = credential.as_bytes();
let deserialized_credential = Credential::from_bytes(&serialized_credential).unwrap();
assert_eq!(credential, deserialized_credential);
}
}
+4
View File
@@ -15,6 +15,7 @@ cfg-if = "1.0.0"
clap = { workspace = true, features = ["derive"] }
csv = "1.3.0"
cw-utils = { workspace = true }
futures = { workspace = true }
handlebars = "3.0.1"
humantime-serde = "1.0"
inquire = "0.6.2"
@@ -25,9 +26,11 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = { workspace = true }
thiserror = { workspace = true }
time = { workspace = true, features = ["parsing", "formatting"] }
tokio = { workspace = true, features = ["sync"]}
toml = "0.5.6"
url = { workspace = true }
tap = "1"
zeroize = { workspace = true }
cosmrs = { workspace = true }
cosmwasm-std = { workspace = true }
@@ -49,6 +52,7 @@ nym-sphinx = { path = "../../common/nymsphinx" }
nym-client-core = { path = "../../common/client-core" }
nym-config = { path = "../../common/config" }
nym-credentials = { path = "../../common/credentials" }
nym-credentials-interface = { path = "../../common/credentials-interface" }
nym-credential-storage = { path = "../../common/credential-storage" }
nym-credential-utils = { path = "../../common/credential-utils" }
@@ -0,0 +1,188 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::context::SigningClient;
use anyhow::{anyhow, bail};
use clap::ArgGroup;
use clap::Parser;
use futures::StreamExt;
use log::{error, info};
use nym_coconut_dkg_common::types::EpochId;
use nym_credential_utils::utils::block_until_coconut_is_available;
use nym_credentials::coconut::bandwidth::freepass::MAX_FREE_PASS_VALIDITY;
use nym_credentials::{
obtain_aggregate_verification_key, IssuanceBandwidthCredential, IssuedBandwidthCredential,
};
use nym_credentials_interface::VerificationKey;
use nym_validator_client::coconut::all_coconut_api_clients;
use nym_validator_client::nyxd::contract_traits::{DkgQueryClient, NymContractsProvider};
use nym_validator_client::nyxd::CosmWasmClient;
use nym_validator_client::signing::AccountData;
use nym_validator_client::CoconutApiClient;
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
use std::sync::Arc;
use time::format_description::well_known::Rfc3339;
use time::OffsetDateTime;
use zeroize::Zeroizing;
fn parse_rfc3339_expiration_date(raw: &str) -> Result<OffsetDateTime, time::error::Parse> {
OffsetDateTime::parse(raw, &Rfc3339)
}
#[derive(Debug, Parser)]
#[clap(group(ArgGroup::new("expiration").required(true)))]
pub struct Args {
/// Specifies the expiration date of the free pass(es)
/// Requires
#[clap(long, group = "expiration", value_parser = parse_rfc3339_expiration_date)]
pub(crate) expiration_date: Option<OffsetDateTime>,
#[clap(long, group = "expiration")]
pub(crate) expiration_timestamp: Option<i64>,
/// The number of free passes to issue
#[clap(long, default_value = "1")]
pub(crate) amount: u64,
/// Path to the output directory for generated free passes.
#[clap(long)]
pub(crate) output_dir: PathBuf,
}
async fn get_freepass(
api_clients: Vec<CoconutApiClient>,
aggregate_vk: &VerificationKey,
threshold: u64,
epoch_id: EpochId,
signing_account: &AccountData,
expiration_date: OffsetDateTime,
) -> anyhow::Result<IssuedBandwidthCredential> {
let issuance_pass = IssuanceBandwidthCredential::new_freepass(Some(expiration_date));
let signing_data = issuance_pass.prepare_for_signing();
let credential_shares = Arc::new(tokio::sync::Mutex::new(Vec::new()));
futures::stream::iter(api_clients)
.for_each_concurrent(None, |client| async {
// move the client into the block
let client = client;
let api_url = client.api_client.api_url();
info!("contacting {api_url} for blinded free pass");
match issuance_pass
.obtain_partial_freepass_credential(
&client.api_client,
signing_account,
&client.verification_key,
signing_data.clone(),
)
.await
{
Ok(partial_credential) => {
credential_shares
.lock()
.await
.push((partial_credential, client.node_id).into());
}
Err(err) => {
error!("failed to obtain partial free pass from {api_url}: {err}")
}
}
})
.await;
// SAFETY: the futures have completed, so we MUST have the only arc reference
#[allow(clippy::unwrap_used)]
let credential_shares = Arc::into_inner(credential_shares).unwrap().into_inner();
if credential_shares.len() < threshold as usize {
bail!("we managed to obtain only {} partial credentials while the minimum threshold is {threshold}", credential_shares.len());
}
let signature = issuance_pass.aggregate_signature_shares(aggregate_vk, &credential_shares)?;
Ok(issuance_pass.into_issued_credential(signature, epoch_id))
}
pub async fn execute(args: Args, client: SigningClient) -> anyhow::Result<()> {
let address = client.address();
if !args.output_dir.is_dir() {
bail!("the provided output directory is not a directory!");
}
if args.output_dir.read_dir()?.next().is_some() {
bail!("the provided output directory is not empty!");
}
let Some(bandwidth_contract) = client.coconut_bandwidth_contract_address() else {
bail!("the bandwidth contract address is not set")
};
let Some(bandwidth_admin) = client
.get_contract(bandwidth_contract)
.await
.map(|c| c.contract_info.admin)?
else {
bail!("the bandwidth contract doesn't have any admin set")
};
// sanity checks since nym-apis will reject invalid requests anyway
if address != bandwidth_admin {
bail!("the provided mnemonic does not correspond to the current admin of the bandwidth contract")
}
let expiration_date = match args.expiration_date {
Some(date) => date,
// SAFETY: one of those arguments must have been set
None => OffsetDateTime::from_unix_timestamp(args.expiration_timestamp.unwrap())?,
};
let now = OffsetDateTime::now_utc();
if expiration_date > now + MAX_FREE_PASS_VALIDITY {
bail!("the provided free pass request has too long expiry (expiry is set to on {expiration_date})")
}
// issuance start
block_until_coconut_is_available(&client).await?;
let signing_account = client.signing_account()?;
let epoch_id = client.get_current_epoch().await?.epoch_id;
let threshold = client
.get_current_epoch_threshold()
.await?
.ok_or(anyhow!("no threshold available"))?;
let api_clients = all_coconut_api_clients(&client, epoch_id).await?;
if api_clients.len() < threshold as usize {
bail!(
"we have only {} api clients available while the minimum threshold is {threshold}",
api_clients.len()
)
}
let aggregate_vk = obtain_aggregate_verification_key(&api_clients)?;
for i in 0..args.amount {
let human_index = i + 1;
info!("trying to obtain free pass {human_index}/{}", args.amount);
let free_pass = get_freepass(
api_clients.clone(),
&aggregate_vk,
threshold,
epoch_id,
&signing_account,
expiration_date,
)
.await?;
let credential_data = Zeroizing::new(free_pass.pack_v1());
let output = args.output_dir.join(format!("freepass_{i}.nym"));
info!("saving the freepass to '{}'", output.display());
File::create(output)?.write_all(&credential_data)?;
}
Ok(())
}
+2
View File
@@ -3,6 +3,7 @@
use clap::{Args, Subcommand};
pub mod generate_freepass;
pub mod issue_credentials;
pub mod recover_credentials;
@@ -15,6 +16,7 @@ pub struct Coconut {
#[derive(Debug, Subcommand)]
pub enum CoconutCommands {
GenerateFreepass(generate_freepass::Args),
IssueCredentials(issue_credentials::Args),
RecoverCredentials(recover_credentials::Args),
}
+3 -1
View File
@@ -11,7 +11,9 @@ async-trait = { workspace = true }
log = { workspace = true }
thiserror = { workspace = true }
tokio = { version = "1.24.1", features = ["sync"]}
tokio = { workspace = true, features = ["sync"]}
zeroize = { workspace = true, features = ["zeroize_derive"] }
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx]
workspace = true
@@ -0,0 +1,17 @@
/*
* Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
* SPDX-License-Identifier: Apache-2.0
*/
DROP TABLE coconut_credentials;
CREATE TABLE coconut_credentials
(
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
-- introduce a way for us to introduce breaking changes in serialization
serialization_revision INTEGER NOT NULL,
credential_type TEXT NOT NULL,
credential_data BLOB NOT NULL,
epoch_id INTEGER NOT NULL,
consumed BOOLEAN NOT NULL
);
@@ -1,59 +1,60 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// Copyright 2023-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::models::CoconutCredential;
use crate::models::StoredIssuedCredential;
use std::sync::Arc;
use tokio::sync::RwLock;
#[derive(Clone)]
pub struct CoconutCredentialManager {
inner: Arc<RwLock<Vec<CoconutCredential>>>,
inner: Arc<RwLock<CoconutCredentialManagerInner>>,
}
#[derive(Default)]
struct CoconutCredentialManagerInner {
data: Vec<StoredIssuedCredential>,
_next_id: i64,
}
impl CoconutCredentialManagerInner {
fn next_id(&mut self) -> i64 {
let next = self._next_id;
self._next_id += 1;
next
}
}
impl CoconutCredentialManager {
/// Creates new empty instance of the `CoconutCredentialManager`.
pub fn new() -> Self {
CoconutCredentialManager {
inner: Arc::new(RwLock::new(Vec::new())),
inner: Default::default(),
}
}
/// Inserts provided signature into the database.
///
/// # Arguments
///
/// * `voucher_value`: Plaintext bandwidth value of the credential.
/// * `voucher_info`: Plaintext information of the credential.
/// * `serial_number`: Base58 representation of the serial number attribute.
/// * `binding_number`: Base58 representation of the binding number attribute.
/// * `signature`: Coconut credential in the form of a signature.
pub async fn insert_coconut_credential(
pub async fn insert_issued_credential(
&self,
voucher_value: String,
voucher_info: String,
serial_number: String,
binding_number: String,
signature: String,
epoch_id: String,
credential_type: String,
serialization_revision: u8,
credential_data: &[u8],
epoch_id: u32,
) {
let mut creds = self.inner.write().await;
let id = creds.len() as i64;
creds.push(CoconutCredential {
let mut inner = self.inner.write().await;
let id = inner.next_id();
inner.data.push(StoredIssuedCredential {
id,
voucher_value,
voucher_info,
serial_number,
binding_number,
signature,
serialization_revision,
credential_data: credential_data.to_vec(),
credential_type,
epoch_id,
consumed: false,
});
})
}
/// Tries to retrieve one of the stored, unused credentials.
pub async fn get_next_coconut_credential(&self) -> Option<CoconutCredential> {
pub async fn get_next_unspent_credential(&self) -> Option<StoredIssuedCredential> {
let creds = self.inner.read().await;
creds.iter().find(|c| !c.consumed).cloned()
creds.data.iter().find(|c| !c.consumed).cloned()
}
/// Consumes in the database the specified credential.
@@ -63,7 +64,7 @@ impl CoconutCredentialManager {
/// * `id`: Database id.
pub async fn consume_coconut_credential(&self, id: i64) {
let mut creds = self.inner.write().await;
if let Some(cred) = creds.get_mut(id as usize) {
if let Some(cred) = creds.data.get_mut(id as usize) {
cred.consumed = true;
}
}
@@ -1,7 +1,7 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::models::CoconutCredential;
use crate::models::StoredIssuedCredential;
#[derive(Clone)]
pub struct CoconutCredentialManager {
@@ -18,43 +18,29 @@ impl CoconutCredentialManager {
CoconutCredentialManager { connection_pool }
}
/// Inserts provided signature into the database.
///
/// # Arguments
///
/// * `voucher_value`: Plaintext bandwidth value of the credential.
/// * `voucher_info`: Plaintext information of the credential.
/// * `serial_number`: Base58 representation of the serial number attribute.
/// * `binding_number`: Base58 representation of the binding number attribute.
/// * `signature`: Coconut credential in the form of a signature.
pub async fn insert_coconut_credential(
pub async fn insert_issued_credential(
&self,
voucher_value: String,
voucher_info: String,
serial_number: String,
binding_number: String,
signature: String,
epoch_id: String,
credential_type: String,
serialization_revision: u8,
credential_data: &[u8],
epoch_id: u32,
) -> Result<(), sqlx::Error> {
sqlx::query!(
"INSERT INTO coconut_credentials(voucher_value, voucher_info, serial_number, binding_number, signature, epoch_id, consumed) VALUES (?, ?, ?, ?, ?, ?, ?)",
voucher_value, voucher_info, serial_number, binding_number, signature, epoch_id, false
)
.execute(&self.connection_pool)
.await?;
r#"
INSERT INTO coconut_credentials(serialization_revision, credential_type, credential_data, epoch_id, consumed)
VALUES (?, ?, ?, ?, false)
"#,
serialization_revision, credential_type, credential_data, epoch_id
).execute(&self.connection_pool).await?;
Ok(())
}
/// Tries to retrieve one of the stored, unused credentials.
pub async fn get_next_coconut_credential(
pub async fn get_next_unspent_credential(
&self,
) -> Result<Option<CoconutCredential>, sqlx::Error> {
sqlx::query_as!(
CoconutCredential,
"SELECT * FROM coconut_credentials WHERE NOT consumed"
)
.fetch_optional(&self.connection_pool)
.await
) -> Result<Option<StoredIssuedCredential>, sqlx::Error> {
sqlx::query_as("SELECT * FROM coconut_credentials WHERE NOT consumed LIMIT 1")
.fetch_optional(&self.connection_pool)
.await
}
/// Consumes in the database the specified credential.
@@ -3,7 +3,7 @@
use crate::backends::memory::CoconutCredentialManager;
use crate::error::StorageError;
use crate::models::CoconutCredential;
use crate::models::{StorableIssuedCredential, StoredIssuedCredential};
use crate::storage::Storage;
use async_trait::async_trait;
@@ -27,33 +27,27 @@ impl Default for EphemeralStorage {
impl Storage for EphemeralStorage {
type StorageError = StorageError;
async fn insert_coconut_credential(
async fn insert_issued_credential<'a>(
&self,
voucher_value: String,
voucher_info: String,
serial_number: String,
binding_number: String,
signature: String,
epoch_id: String,
bandwidth_credential: StorableIssuedCredential<'a>,
) -> Result<(), StorageError> {
self.coconut_credential_manager
.insert_coconut_credential(
voucher_value,
voucher_info,
serial_number,
binding_number,
signature,
epoch_id,
.insert_issued_credential(
bandwidth_credential.credential_type,
bandwidth_credential.serialization_revision,
bandwidth_credential.credential_data,
bandwidth_credential.epoch_id,
)
.await;
Ok(())
}
async fn get_next_coconut_credential(&self) -> Result<CoconutCredential, StorageError> {
async fn get_next_unspent_credential(
&self,
) -> Result<StoredIssuedCredential, Self::StorageError> {
let credential = self
.coconut_credential_manager
.get_next_coconut_credential()
.get_next_unspent_credential()
.await
.ok_or(StorageError::NoCredential)?;
+33 -10
View File
@@ -1,15 +1,38 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
#[derive(Clone)]
pub struct CoconutCredential {
#[allow(dead_code)]
use zeroize::{Zeroize, ZeroizeOnDrop};
// #[derive(Clone)]
// pub struct CoconutCredential {
// #[allow(dead_code)]
// pub id: i64,
// pub voucher_value: String,
// pub voucher_info: String,
// pub serial_number: String,
// pub binding_number: String,
// pub signature: String,
// pub epoch_id: String,
// pub consumed: bool,
// }
#[cfg_attr(not(target_arch = "wasm32"), derive(sqlx::FromRow))]
#[derive(Zeroize, ZeroizeOnDrop, Clone)]
pub struct StoredIssuedCredential {
pub id: i64,
pub voucher_value: String,
pub voucher_info: String,
pub serial_number: String,
pub binding_number: String,
pub signature: String,
pub epoch_id: String,
pub serialization_revision: u8,
pub credential_data: Vec<u8>,
pub credential_type: String,
pub epoch_id: u32,
pub consumed: bool,
}
pub struct StorableIssuedCredential<'a> {
pub serialization_revision: u8,
pub credential_data: &'a [u8],
pub credential_type: String,
pub epoch_id: u32,
}
@@ -5,7 +5,7 @@ use crate::backends::sqlite::CoconutCredentialManager;
use crate::error::StorageError;
use crate::storage::Storage;
use crate::models::CoconutCredential;
use crate::models::{StorableIssuedCredential, StoredIssuedCredential};
use async_trait::async_trait;
use log::{debug, error};
use sqlx::ConnectOptions;
@@ -58,33 +58,28 @@ impl PersistentStorage {
impl Storage for PersistentStorage {
type StorageError = StorageError;
async fn insert_coconut_credential(
async fn insert_issued_credential<'a>(
&self,
voucher_value: String,
voucher_info: String,
serial_number: String,
binding_number: String,
signature: String,
epoch_id: String,
) -> Result<(), StorageError> {
bandwidth_credential: StorableIssuedCredential<'a>,
) -> Result<(), Self::StorageError> {
self.coconut_credential_manager
.insert_coconut_credential(
voucher_value,
voucher_info,
serial_number,
binding_number,
signature,
epoch_id,
.insert_issued_credential(
bandwidth_credential.credential_type,
bandwidth_credential.serialization_revision,
bandwidth_credential.credential_data,
bandwidth_credential.epoch_id,
)
.await?;
Ok(())
}
async fn get_next_coconut_credential(&self) -> Result<CoconutCredential, StorageError> {
async fn get_next_unspent_credential(
&self,
) -> Result<StoredIssuedCredential, Self::StorageError> {
let credential = self
.coconut_credential_manager
.get_next_coconut_credential()
.get_next_unspent_credential()
.await?
.ok_or(StorageError::NoCredential)?;
+6 -19
View File
@@ -1,7 +1,7 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::models::CoconutCredential;
use crate::models::{StorableIssuedCredential, StoredIssuedCredential};
use async_trait::async_trait;
use std::error::Error;
@@ -9,28 +9,15 @@ use std::error::Error;
pub trait Storage: Send + Sync {
type StorageError: Error;
/// Inserts provided signature into the database.
///
/// # Arguments
///
/// * `voucher_value`: How much bandwidth is in the credential.
/// * `voucher_info`: What type of credential it is.
/// * `serial_number`: Serial number of the credential.
/// * `binding_number`: Binding number of the credential.
/// * `signature`: Coconut credential in the form of a signature.
/// * `epoch_id`: The epoch when it was signed.
async fn insert_coconut_credential(
async fn insert_issued_credential<'a>(
&self,
voucher_value: String,
voucher_info: String,
serial_number: String,
binding_number: String,
signature: String,
epoch_id: String,
bandwidth_credential: StorableIssuedCredential<'a>,
) -> Result<(), Self::StorageError>;
/// Tries to retrieve one of the stored, unused credentials.
async fn get_next_coconut_credential(&self) -> Result<CoconutCredential, Self::StorageError>;
async fn get_next_unspent_credential(
&self,
) -> Result<StoredIssuedCredential, Self::StorageError>;
/// Marks as consumed in the database the specified credential.
///
+1
View File
@@ -12,6 +12,7 @@ thiserror = { workspace = true }
tokio = { workspace = true }
nym-bandwidth-controller = { path = "../../common/bandwidth-controller" }
nym-coconut = { path = "../nymcoconut" }
nym-credentials = { path = "../../common/credentials" }
nym-credential-storage = { path = "../../common/credential-storage" }
nym-validator-client = { path = "../../common/client-libs/validator-client" }
@@ -3,11 +3,13 @@
use crate::errors::Result;
use log::error;
use nym_credentials::coconut::bandwidth::BandwidthVoucher;
use nym_credentials::coconut::bandwidth::IssuanceBandwidthCredential;
use std::fs::{create_dir_all, read_dir, File};
use std::io::{Read, Write};
use std::path::PathBuf;
pub const DUMPED_VOUCHER_EXTENSION: &str = "credentialrecovery";
pub struct RecoveryStorage {
recovery_dir: PathBuf,
}
@@ -18,14 +20,16 @@ impl RecoveryStorage {
Ok(Self { recovery_dir })
}
pub fn unconsumed_vouchers(&self) -> Result<Vec<BandwidthVoucher>> {
pub fn unconsumed_vouchers(&self) -> Result<Vec<IssuanceBandwidthCredential>> {
let entries = read_dir(&self.recovery_dir)?;
let mut paths = vec![];
for entry in entries.flatten() {
let path = entry.path();
if path.is_file() {
paths.push(path)
if let Some(extension) = path.extension() {
if extension == DUMPED_VOUCHER_EXTENSION {
paths.push(path)
}
}
}
@@ -34,7 +38,7 @@ impl RecoveryStorage {
if let Ok(mut file) = File::open(&path) {
let mut buff = Vec::new();
if file.read_to_end(&mut buff).is_ok() {
match BandwidthVoucher::try_from_bytes(&buff) {
match IssuanceBandwidthCredential::try_from_recovered_bytes(&buff) {
Ok(voucher) => vouchers.push(voucher),
Err(err) => {
error!("failed to parse the voucher at {}: {err}", path.display())
@@ -47,11 +51,17 @@ impl RecoveryStorage {
Ok(vouchers)
}
pub fn insert_voucher(&self, voucher: &BandwidthVoucher) -> Result<PathBuf> {
let file_name = voucher.tx_hash().to_string();
pub fn voucher_filename(voucher: &IssuanceBandwidthCredential) -> String {
let prefix = voucher.typ().to_string();
let suffix = voucher.blinded_serial_number_bs58();
format!("{prefix}-{suffix}.{DUMPED_VOUCHER_EXTENSION}")
}
pub fn insert_voucher(&self, voucher: &IssuanceBandwidthCredential) -> Result<PathBuf> {
let file_name = Self::voucher_filename(voucher);
let file_path = self.recovery_dir.join(file_name);
let mut file = File::create(&file_path)?;
let buff = voucher.to_bytes();
let buff = voucher.to_recovery_bytes();
file.write_all(&buff)?;
Ok(file_path)
+19 -8
View File
@@ -5,6 +5,7 @@ use nym_bandwidth_controller::acquire::state::State;
use nym_client_core::config::disk_persistence::CommonClientPaths;
use nym_config::DEFAULT_DATA_DIR;
use nym_credential_storage::persistent_storage::PersistentStorage;
use nym_credentials::coconut::bandwidth::CredentialType;
use nym_validator_client::nyxd::contract_traits::{
dkg_query_client::EpochState, CoconutBandwidthSigningClient, DkgQueryClient,
};
@@ -43,7 +44,7 @@ where
let state = nym_bandwidth_controller::acquire::deposit(client, amount.clone()).await?;
if nym_bandwidth_controller::acquire::get_credential(&state, client, persistent_storage)
if nym_bandwidth_controller::acquire::get_bandwidth_voucher(&state, client, persistent_storage)
.await
.is_err()
{
@@ -128,19 +129,29 @@ where
{
let mut recovered_amount: u128 = 0;
for voucher in recovery_storage.unconsumed_vouchers()? {
let voucher_value = voucher.get_voucher_value();
let voucher_value = match voucher.typ() {
CredentialType::Voucher => voucher.get_bandwidth_attribute(),
CredentialType::FreePass => {
error!("unimplemented recovery of free pass credentials");
continue;
}
};
recovered_amount += voucher_value.parse::<u128>()?;
let voucher_name = RecoveryStorage::voucher_filename(&voucher);
let state = State::new(voucher);
let voucher = state.voucher.tx_hash();
if let Err(e) =
nym_bandwidth_controller::acquire::get_credential(&state, client, shared_storage).await
nym_bandwidth_controller::acquire::get_bandwidth_voucher(&state, client, shared_storage)
.await
{
error!("Could not recover deposit {voucher} due to {e}, try again later",)
error!("Could not recover deposit {voucher_name} due to {e}, try again later",)
} else {
info!("Converted deposit {voucher} to a credential, removing recovery data for it",);
if let Err(e) = recovery_storage.remove_voucher(voucher.to_string()) {
warn!("Could not remove recovery data: {e}");
info!(
"Converted deposit {voucher_name} to a credential, removing recovery data for it",
);
if let Err(err) = recovery_storage.remove_voucher(voucher_name) {
warn!("Could not remove recovery data: {err}");
}
}
}
+18
View File
@@ -0,0 +1,18 @@
[package]
name = "nym-credentials-interface"
version = "0.1.0"
authors.workspace = true
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
edition.workspace = true
license.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
bls12_381 = { workspace = true, default-features = false }
serde = { workspace = true, features = ["derive"] }
thiserror = { workspace = true }
nym-coconut = { path = "../nymcoconut" }
+136
View File
@@ -0,0 +1,136 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use bls12_381::Scalar;
use serde::{Deserialize, Serialize};
use std::fmt::{Display, Formatter};
use std::str::FromStr;
use thiserror::Error;
pub use nym_coconut::{
aggregate_signature_shares, aggregate_verification_keys, blind_sign, hash_to_scalar, keygen,
prepare_blind_sign, prove_bandwidth_credential, verify_credential, Attribute, Base58,
BlindSignRequest, BlindedSerialNumber, BlindedSignature, Bytable, CoconutError, KeyPair,
Parameters, PrivateAttribute, PublicAttribute, SecretKey, Signature, SignatureShare,
VerificationKey, VerifyCredentialRequest,
};
pub const VOUCHER_INFO_TYPE: &str = "BandwidthVoucher";
pub const FREE_PASS_INFO_TYPE: &str = "FreeBandwidthPass";
// pub trait NymCredential {
// fn prove_credential(&self) -> Result<(), ()>;
// }
#[derive(Debug, Error)]
#[error("{0} is not a valid credential type")]
pub struct UnknownCredentialType(String);
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum CredentialType {
Voucher,
FreePass,
}
impl FromStr for CredentialType {
type Err = UnknownCredentialType;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s == VOUCHER_INFO_TYPE {
Ok(CredentialType::Voucher)
} else if s == FREE_PASS_INFO_TYPE {
Ok(CredentialType::FreePass)
} else {
Err(UnknownCredentialType(s.to_string()))
}
}
}
impl CredentialType {
pub fn validate(&self, type_plain: &str) -> bool {
match self {
CredentialType::Voucher => type_plain == VOUCHER_INFO_TYPE,
CredentialType::FreePass => type_plain == FREE_PASS_INFO_TYPE,
}
}
pub fn is_free_pass(&self) -> bool {
matches!(self, CredentialType::FreePass)
}
pub fn is_voucher(&self) -> bool {
matches!(self, CredentialType::Voucher)
}
}
impl Display for CredentialType {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
CredentialType::Voucher => VOUCHER_INFO_TYPE.fmt(f),
CredentialType::FreePass => FREE_PASS_INFO_TYPE.fmt(f),
}
}
}
#[derive(Debug, Clone)]
pub struct CredentialSigningData {
pub pedersen_commitments_openings: Vec<Scalar>,
pub blind_sign_request: BlindSignRequest,
pub public_attributes_plain: Vec<String>,
pub typ: CredentialType,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
pub struct CredentialSpendingData {
pub embedded_private_attributes: usize,
pub verify_credential_request: VerifyCredentialRequest,
pub public_attributes_plain: Vec<String>,
pub typ: CredentialType,
/// The (DKG) epoch id under which the credential has been issued so that the verifier could use correct verification key for validation.
pub epoch_id: u64,
}
impl CredentialSpendingData {
pub fn verify(&self, params: &Parameters, verification_key: &VerificationKey) -> bool {
let hashed_public_attributes = self
.public_attributes_plain
.iter()
.map(hash_to_scalar)
.collect::<Vec<_>>();
// get references to the attributes
let public_attributes = hashed_public_attributes.iter().collect::<Vec<_>>();
verify_credential(
params,
verification_key,
&self.verify_credential_request,
&public_attributes,
)
}
pub fn validate_type_attribute(&self) -> bool {
// the first attribute is variant specific bandwidth encoding, the second one should be the type
let Some(type_plain) = self.public_attributes_plain.get(1) else {
return false;
};
self.typ.validate(type_plain)
}
pub fn get_bandwidth_attribute(&self) -> Option<&String> {
// the first attribute is variant specific bandwidth encoding, the second one should be the type
self.public_attributes_plain.first()
}
pub fn blinded_serial_number(&self) -> BlindedSerialNumber {
self.verify_credential_request.blinded_serial_number()
}
}
+5 -2
View File
@@ -8,14 +8,17 @@ license.workspace = true
[dependencies]
bls12_381 = { workspace = true, default-features = false, features = ["pairings", "alloc", "experimental"] }
bincode = "1.3.3"
cosmrs = { workspace = true }
thiserror = { workspace = true }
log = { workspace = true }
time = { workspace = true, features = ["serde"] }
serde = { workspace = true, features = ["derive"] }
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"] }
nym-credentials-interface = { path = "../credentials-interface" }
nym-crypto = { path = "../crypto", features = ["rand", "asymmetric", "serde"] }
nym-api-requests = { path = "../../nym-api/nym-api-requests" }
nym-validator-client = { path = "../client-libs/validator-client", default-features = false }
-428
View File
@@ -1,428 +0,0 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
// for time being assume the bandwidth credential consists of public identity of the requester
// and private (though known... just go along with it) infinite bandwidth value
// right now this has no double-spending protection, spender binding, etc
// it's the simplest possible case
use cosmrs::tendermint::hash::Algorithm;
use cosmrs::tendermint::Hash;
use nym_coconut_interface::{
hash_to_scalar, prepare_blind_sign, Attribute, BlindSignRequest, Credential, Parameters,
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;
#[derive(Zeroize, ZeroizeOnDrop)]
pub struct BandwidthVoucher {
// 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
binding_number: PrivateAttribute,
// 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,
/// the plain text information
voucher_info_plain: String,
/// 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
signing_key: identity::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,
voucher_info: String,
tx_hash: Hash,
signing_key: identity::PrivateKey,
encryption_key: encryption::PrivateKey,
) -> Self {
let serial_number = params.random_scalar();
let binding_number = params.random_scalar();
let voucher_value_plain = voucher_value.clone();
let voucher_info_plain = voucher_info.clone();
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_prehashed, &_voucher_info_prehashed],
)
.unwrap();
BandwidthVoucher {
serial_number,
binding_number,
_voucher_value_prehashed,
voucher_value_plain,
_voucher_info_prehashed,
voucher_info_plain,
tx_hash,
signing_key,
unused_ed25519: encryption_key,
pedersen_commitments_openings,
blind_sign_request,
}
}
pub fn to_bytes(&self) -> Vec<u8> {
let serial_number_b = self.serial_number.to_bytes();
let binding_number_b = self.binding_number.to_bytes();
let voucher_value_plain_b = self.voucher_value_plain.as_bytes();
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.unused_ed25519.to_bytes();
let blind_sign_request_b = self.blind_sign_request.to_bytes();
let mut ret = Vec::new();
ret.extend_from_slice(&serial_number_b);
ret.extend_from_slice(&binding_number_b);
ret.extend_from_slice(tx_hash_b);
ret.extend_from_slice(&signing_key_b);
ret.extend_from_slice(&encryption_key_b);
ret.extend_from_slice(&(voucher_value_plain_b.len() as u64).to_be_bytes());
ret.extend_from_slice(&(voucher_info_plain_b.len() as u64).to_be_bytes());
ret.extend_from_slice(&(blind_sign_request_b.len() as u64).to_be_bytes());
ret.extend_from_slice(&(self.pedersen_commitments_openings.len() as u64).to_be_bytes());
ret.extend_from_slice(voucher_value_plain_b);
ret.extend_from_slice(voucher_info_plain_b);
ret.extend_from_slice(&blind_sign_request_b);
for commitment in self.pedersen_commitments_openings.iter() {
ret.extend_from_slice(&commitment.to_bytes());
}
ret
}
pub fn try_from_bytes(bytes: &[u8]) -> Result<Self, Error> {
if bytes.len() < 32 * 5 + 4 * 8 {
return Err(Error::BandwidthVoucherDeserializationError(format!(
"Less then {} bytes needed",
32 * 5 + 4 * 8
)));
}
let mut buff = [0u8; 32];
let mut small_buff = [0u8; 8];
let scalar_err =
|| Error::BandwidthVoucherDeserializationError(String::from("Invalid Scalar"));
buff.copy_from_slice(&bytes[..32]);
let serial_number = Option::<PrivateAttribute>::from(PrivateAttribute::from_bytes(&buff))
.ok_or_else(scalar_err)?;
buff.copy_from_slice(&bytes[32..2 * 32]);
let binding_number = Option::<PrivateAttribute>::from(PrivateAttribute::from_bytes(&buff))
.ok_or_else(scalar_err)?;
buff.copy_from_slice(&bytes[2 * 32..3 * 32]);
let tx_hash = Hash::from_bytes(Algorithm::Sha256, &buff).map_err(|_| {
Error::BandwidthVoucherDeserializationError(String::from("Invalid transaction Hash"))
})?;
buff.copy_from_slice(&bytes[3 * 32..4 * 32]);
let signing_key = identity::PrivateKey::from_bytes(&buff).map_err(|_| {
Error::BandwidthVoucherDeserializationError(String::from("Invalid key"))
})?;
buff.copy_from_slice(&bytes[4 * 32..5 * 32]);
let encryption_key = encryption::PrivateKey::from_bytes(&buff).map_err(|_| {
Error::BandwidthVoucherDeserializationError(String::from("Invalid key"))
})?;
small_buff.copy_from_slice(&bytes[5 * 32..5 * 32 + 8]);
let voucher_value_plain_no = u64::from_be_bytes(small_buff) as usize;
small_buff.copy_from_slice(&bytes[5 * 32 + 8..5 * 32 + 2 * 8]);
let voucher_info_plain_no = u64::from_be_bytes(small_buff) as usize;
small_buff.copy_from_slice(&bytes[5 * 32 + 2 * 8..5 * 32 + 3 * 8]);
let blind_sign_request_no = u64::from_be_bytes(small_buff) as usize;
small_buff.copy_from_slice(&bytes[5 * 32 + 3 * 8..5 * 32 + 4 * 8]);
let pedersen_commitments_openings_no = u64::from_be_bytes(small_buff) as usize;
let total_length = 32 * 5
+ 4 * 8
+ voucher_value_plain_no
+ voucher_info_plain_no
+ blind_sign_request_no
+ pedersen_commitments_openings_no * 32;
if bytes.len() != total_length {
return Err(Error::BandwidthVoucherDeserializationError(format!(
"Expected {total_length} bytes",
)));
}
let utf_err = |_| {
Err(Error::BandwidthVoucherDeserializationError(String::from(
"Invalid UTF8 string",
)))
};
let mut var_length_pointer = 5 * 32 + 4 * 8;
let voucher_value_plain = String::from_utf8(
bytes[var_length_pointer..var_length_pointer + voucher_value_plain_no].to_vec(),
)
.or_else(utf_err)?;
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_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],
)?;
var_length_pointer += blind_sign_request_no;
let mut pedersen_commitments_openings = Vec::new();
for _ in 0..pedersen_commitments_openings_no {
buff.copy_from_slice(&bytes[var_length_pointer..var_length_pointer + 32]);
let commitment =
Option::<Attribute>::from(Attribute::from_bytes(&buff)).ok_or_else(scalar_err)?;
var_length_pointer += 32;
pedersen_commitments_openings.push(commitment);
}
Ok(Self {
serial_number,
binding_number,
_voucher_value_prehashed,
voucher_value_plain,
_voucher_info_prehashed,
voucher_info_plain,
tx_hash,
signing_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 {
values.len() == 2
&& plain_values.len() == 2
&& 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 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.unused_ed25519
}
pub fn pedersen_commitments_openings(&self) -> &Vec<Attribute> {
&self.pedersen_commitments_openings
}
pub fn blind_sign_request(&self) -> &BlindSignRequest {
&self.blind_sign_request
}
pub fn get_voucher_value(&self) -> String {
self.voucher_value_plain.clone()
}
pub fn get_public_attributes_plain(&self) -> Vec<String> {
vec![
self.voucher_value_plain.clone(),
self.voucher_info_plain.clone(),
]
}
pub fn get_private_attributes(&self) -> Vec<&PrivateAttribute> {
vec![&self.serial_number, &self.binding_number]
}
pub fn signable_plaintext(request: &BlindSignRequest, tx_hash: Hash) -> Vec<u8> {
let mut message = request.to_bytes();
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,
epoch_id: u64,
signature: &Signature,
verification_key: &VerificationKey,
) -> Result<Credential, Error> {
let params = Parameters::new(BandwidthVoucher::ENCODED_ATTRIBUTES)?;
prepare_credential_for_spending(
&params,
voucher_value,
voucher_info,
serial_number,
binding_number,
epoch_id,
signature,
verification_key,
)
}
#[cfg(test)]
mod test {
use super::*;
use cosmrs::tendermint::hash::Algorithm;
use nym_coconut_interface::Base58;
use rand::rngs::OsRng;
fn voucher_fixture() -> BandwidthVoucher {
let params = Parameters::new(4).unwrap();
let mut rng = OsRng;
BandwidthVoucher::new(
&params,
"1234".to_string(),
"voucher info".to_string(),
Hash::from_bytes(Algorithm::Sha256, &[0; 32]).unwrap(),
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(),
)
}
#[test]
fn serde_voucher() {
let voucher = voucher_fixture();
let bytes = voucher.to_bytes();
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_plain,
deserialized_voucher.voucher_value_plain
);
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.unused_ed25519.to_string(),
deserialized_voucher.unused_ed25519.to_string()
);
assert_eq!(
voucher.pedersen_commitments_openings,
deserialized_voucher.pedersen_commitments_openings
);
assert_eq!(
voucher.blind_sign_request.to_bs58(),
deserialized_voucher.blind_sign_request.to_bs58()
);
}
#[test]
fn voucher_consistency() {
let voucher = voucher_fixture();
assert!(!BandwidthVoucher::verify_against_plain(
&[],
&voucher.get_public_attributes_plain()
));
assert!(!BandwidthVoucher::verify_against_plain(
&voucher.get_public_attributes(),
&[],
));
assert!(!BandwidthVoucher::verify_against_plain(
&voucher.get_public_attributes(),
&[
voucher.get_public_attributes_plain()[0].clone(),
String::new()
]
));
assert!(!BandwidthVoucher::verify_against_plain(
&voucher.get_public_attributes(),
&[
String::new(),
voucher.get_public_attributes_plain()[1].clone()
]
));
assert!(!BandwidthVoucher::verify_against_plain(
&[voucher.get_public_attributes()[0], &Attribute::one()],
&voucher.get_public_attributes_plain()
));
assert!(!BandwidthVoucher::verify_against_plain(
&[&Attribute::one(), voucher.get_public_attributes()[1]],
&voucher.get_public_attributes_plain()
));
assert!(BandwidthVoucher::verify_against_plain(
&voucher.get_public_attributes(),
&voucher.get_public_attributes_plain()
));
}
}
@@ -0,0 +1,134 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::coconut::utils::scalar_serde_helper;
use crate::error::Error;
use nym_api_requests::coconut::FreePassRequest;
use nym_credentials_interface::{
hash_to_scalar, Attribute, BlindedSignature, CredentialSigningData, PublicAttribute,
};
use nym_validator_client::signing::AccountData;
use serde::{Deserialize, Serialize};
use time::{Duration, OffsetDateTime, Time};
use zeroize::{Zeroize, ZeroizeOnDrop};
pub const MAX_FREE_PASS_VALIDITY: Duration = Duration::WEEK; // 1 week
#[derive(Zeroize, ZeroizeOnDrop, Serialize, Deserialize)]
pub struct FreePassIssuedData {
/// the plain validity value of this credential expressed as unix timestamp
#[zeroize(skip)]
expiry_date: OffsetDateTime,
}
impl<'a> From<&'a FreePassIssuanceData> for FreePassIssuedData {
fn from(value: &'a FreePassIssuanceData) -> Self {
FreePassIssuedData {
expiry_date: value.expiry_date,
}
}
}
impl FreePassIssuedData {
pub fn expiry_date_plain(&self) -> String {
self.expiry_date.unix_timestamp().to_string()
}
}
#[derive(Zeroize, Serialize, Deserialize)]
pub struct FreePassIssuanceData {
/// the plain validity value of this credential expressed as unix timestamp
#[zeroize(skip)]
expiry_date: OffsetDateTime,
// the expiry date, as unix timestamp, hashed into a scalar
#[serde(with = "scalar_serde_helper")]
expiry_date_prehashed: PublicAttribute,
}
impl FreePassIssuanceData {
pub fn new(expiry_date: Option<OffsetDateTime>) -> Self {
// ideally we should have implemented a proper error handling here, sure.
// but given it's meant to only be used by nym, imo it's fine to just panic here in case of invalid arguments
let expiry_date = if let Some(provided) = expiry_date {
if provided - OffsetDateTime::now_utc() > MAX_FREE_PASS_VALIDITY {
panic!("the provided expiry date is bigger than the maximum value of {MAX_FREE_PASS_VALIDITY}");
}
provided
} else {
Self::default_expiry_date()
};
let expiry_date_prehashed = hash_to_scalar(expiry_date.unix_timestamp().to_string());
FreePassIssuanceData {
expiry_date,
expiry_date_prehashed,
}
}
pub fn default_expiry_date() -> OffsetDateTime {
// set it to furthest midnight in the future such as it's no more than a week away,
// i.e. if it's currently for example 9:43 on 2nd March 2024, it will set it to 0:00 on 9th March 2024
(OffsetDateTime::now_utc() + MAX_FREE_PASS_VALIDITY).replace_time(Time::MIDNIGHT)
}
pub fn expiry_date_attribute(&self) -> &Attribute {
&self.expiry_date_prehashed
}
pub fn expiry_date_plain(&self) -> String {
self.expiry_date.unix_timestamp().to_string()
}
pub async fn obtain_free_pass_nonce(
&self,
client: &nym_validator_client::client::NymApiClient,
) -> Result<u32, Error> {
let server_response = client.free_pass_nonce().await?;
Ok(server_response.current_nonce)
}
pub fn create_free_pass_request(
&self,
signing_request: &CredentialSigningData,
account_data: &AccountData,
issuer_nonce: u32,
) -> Result<FreePassRequest, Error> {
let plaintext = issuer_nonce.to_be_bytes();
let nonce_signature = account_data
.private_key()
.sign(&plaintext)
.map_err(|_| Error::Secp256k1SignFailure)?;
Ok(FreePassRequest {
cosmos_pubkey: account_data.public_key(),
inner_sign_request: signing_request.blind_sign_request.clone(),
used_nonce: issuer_nonce,
nonce_signature,
public_attributes_plain: signing_request.public_attributes_plain.clone(),
})
}
pub async fn obtain_blinded_credential(
&self,
client: &nym_validator_client::client::NymApiClient,
request: &FreePassRequest,
) -> Result<BlindedSignature, Error> {
let server_response = client.issue_free_pass_credential(request).await?;
Ok(server_response.blinded_signature)
}
pub async fn request_blinded_credential(
&self,
signing_request: &CredentialSigningData,
account_data: &AccountData,
client: &nym_validator_client::client::NymApiClient,
) -> Result<BlindedSignature, Error> {
let signing_nonce = self.obtain_free_pass_nonce(client).await?;
let request =
self.create_free_pass_request(signing_request, account_data, signing_nonce)?;
self.obtain_blinded_credential(client, &request).await
}
}
@@ -0,0 +1,329 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::coconut::bandwidth::freepass::FreePassIssuanceData;
use crate::coconut::bandwidth::issued::IssuedBandwidthCredential;
use crate::coconut::bandwidth::voucher::BandwidthVoucherIssuanceData;
use crate::coconut::bandwidth::{
bandwidth_credential_params, CredentialSigningData, CredentialType,
};
use crate::coconut::utils::scalar_serde_helper;
use crate::error::Error;
use nym_credentials_interface::{
aggregate_signature_shares, hash_to_scalar, prepare_blind_sign, Attribute, BlindedSerialNumber,
BlindedSignature, Parameters, PrivateAttribute, PublicAttribute, Signature, SignatureShare,
VerificationKey,
};
use nym_crypto::asymmetric::{encryption, identity};
use nym_validator_client::nym_api::EpochId;
use nym_validator_client::nyxd::{Coin, Hash};
use nym_validator_client::signing::AccountData;
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
use zeroize::{Zeroize, ZeroizeOnDrop};
#[derive(Zeroize, ZeroizeOnDrop, Serialize, Deserialize)]
pub enum BandwidthCredentialIssuanceDataVariant {
Voucher(BandwidthVoucherIssuanceData),
FreePass(FreePassIssuanceData),
}
impl From<FreePassIssuanceData> for BandwidthCredentialIssuanceDataVariant {
fn from(value: FreePassIssuanceData) -> Self {
BandwidthCredentialIssuanceDataVariant::FreePass(value)
}
}
impl From<BandwidthVoucherIssuanceData> for BandwidthCredentialIssuanceDataVariant {
fn from(value: BandwidthVoucherIssuanceData) -> Self {
BandwidthCredentialIssuanceDataVariant::Voucher(value)
}
}
impl BandwidthCredentialIssuanceDataVariant {
pub fn info(&self) -> CredentialType {
match self {
BandwidthCredentialIssuanceDataVariant::Voucher(..) => CredentialType::Voucher,
BandwidthCredentialIssuanceDataVariant::FreePass(..) => CredentialType::FreePass,
}
}
// currently this works under the assumption of there being a single unique public attribute for given variant
pub fn public_value(&self) -> &Attribute {
match self {
BandwidthCredentialIssuanceDataVariant::Voucher(voucher) => voucher.value_attribute(),
BandwidthCredentialIssuanceDataVariant::FreePass(freepass) => {
freepass.expiry_date_attribute()
}
}
}
// currently this works under the assumption of there being a single unique public attribute for given variant
pub fn public_value_plain(&self) -> String {
match self {
BandwidthCredentialIssuanceDataVariant::Voucher(voucher) => voucher.value_plain(),
BandwidthCredentialIssuanceDataVariant::FreePass(freepass) => {
freepass.expiry_date_plain()
}
}
}
pub fn voucher_data(&self) -> Option<&BandwidthVoucherIssuanceData> {
match self {
BandwidthCredentialIssuanceDataVariant::Voucher(voucher) => Some(voucher),
_ => None,
}
}
}
// all types of bandwidth credentials contain serial number and binding number
#[derive(Zeroize, ZeroizeOnDrop, Serialize, Deserialize)]
pub struct IssuanceBandwidthCredential {
// private attributes
/// a random secret value generated by the client used for double-spending detection
#[serde(with = "scalar_serde_helper")]
serial_number: PrivateAttribute,
/// a random secret value generated by the client used to bind multiple credentials together
#[serde(with = "scalar_serde_helper")]
binding_number: PrivateAttribute,
/// data specific to given bandwidth credential, for example a value for bandwidth voucher and expiry date for the free pass
variant_data: BandwidthCredentialIssuanceDataVariant,
/// type of the bandwdith credential hashed onto a scalar
#[serde(with = "scalar_serde_helper")]
type_prehashed: PublicAttribute,
}
impl IssuanceBandwidthCredential {
pub const PUBLIC_ATTRIBUTES: u32 = 2;
pub const PRIVATE_ATTRIBUTES: u32 = 2;
pub const ENCODED_ATTRIBUTES: u32 = Self::PUBLIC_ATTRIBUTES + Self::PRIVATE_ATTRIBUTES;
pub fn default_parameters() -> Parameters {
// safety: the unwrap is fine here as Self::ENCODED_ATTRIBUTES is non-zero
Parameters::new(Self::ENCODED_ATTRIBUTES).unwrap()
}
pub fn new<B: Into<BandwidthCredentialIssuanceDataVariant>>(variant_data: B) -> Self {
let variant_data = variant_data.into();
let type_prehashed = hash_to_scalar(variant_data.info().to_string());
let params = bandwidth_credential_params();
let serial_number = params.random_scalar();
let binding_number = params.random_scalar();
IssuanceBandwidthCredential {
serial_number,
binding_number,
variant_data,
type_prehashed,
}
}
pub fn new_voucher(
value: impl Into<Coin>,
deposit_tx_hash: Hash,
signing_key: identity::PrivateKey,
unused_ed25519: encryption::PrivateKey,
) -> Self {
Self::new(BandwidthVoucherIssuanceData::new(
value,
deposit_tx_hash,
signing_key,
unused_ed25519,
))
}
pub fn new_freepass(expiry_date: Option<OffsetDateTime>) -> Self {
Self::new(FreePassIssuanceData::new(expiry_date))
}
pub fn blind_serial_number(&self) -> BlindedSerialNumber {
(bandwidth_credential_params().gen2() * self.serial_number).into()
}
pub fn blinded_serial_number_bs58(&self) -> String {
use nym_credentials_interface::Base58;
self.blind_serial_number().to_bs58()
}
pub fn typ(&self) -> CredentialType {
self.variant_data.info()
}
pub fn get_private_attributes(&self) -> Vec<&PrivateAttribute> {
vec![&self.serial_number, &self.binding_number]
}
pub fn get_public_attributes(&self) -> Vec<&PublicAttribute> {
vec![self.variant_data.public_value(), &self.type_prehashed]
}
pub fn get_plain_public_attributes(&self) -> Vec<String> {
vec![
self.variant_data.public_value_plain(),
self.typ().to_string(),
]
}
pub fn get_variant_data(&self) -> &BandwidthCredentialIssuanceDataVariant {
&self.variant_data
}
pub fn get_bandwidth_attribute(&self) -> String {
self.variant_data.public_value_plain()
}
pub fn prepare_for_signing(&self) -> CredentialSigningData {
let params = bandwidth_credential_params();
// safety: the creation of the request can only fail if one provided invalid parameters
// and we created then specific to this type of the credential so the unwrap is fine
let (pedersen_commitments_openings, blind_sign_request) = prepare_blind_sign(
params,
&[&self.serial_number, &self.binding_number],
&self.get_public_attributes(),
)
.unwrap();
CredentialSigningData {
pedersen_commitments_openings,
blind_sign_request,
public_attributes_plain: self.get_plain_public_attributes(),
typ: self.typ(),
}
}
pub fn unblind_signature(
&self,
validator_vk: &VerificationKey,
signing_data: &CredentialSigningData,
blinded_signature: BlindedSignature,
) -> Result<Signature, Error> {
let public_attributes = self.get_public_attributes();
let private_attributes = self.get_private_attributes();
let params = bandwidth_credential_params();
let unblinded_signature = blinded_signature.unblind_and_verify(
params,
validator_vk,
&private_attributes,
&public_attributes,
&signing_data.blind_sign_request.get_commitment_hash(),
&signing_data.pedersen_commitments_openings,
)?;
Ok(unblinded_signature)
}
pub async fn obtain_partial_freepass_credential(
&self,
client: &nym_validator_client::client::NymApiClient,
account_data: &AccountData,
validator_vk: &VerificationKey,
signing_data: impl Into<Option<CredentialSigningData>>,
) -> Result<Signature, Error> {
// if we provided signing data, do use them, otherwise generate fresh data
let signing_data = signing_data
.into()
.unwrap_or_else(|| self.prepare_for_signing());
let blinded_signature = match &self.variant_data {
BandwidthCredentialIssuanceDataVariant::FreePass(freepass) => {
freepass
.request_blinded_credential(&signing_data, account_data, client)
.await?
}
_ => return Err(Error::NotAFreePass),
};
self.unblind_signature(validator_vk, &signing_data, blinded_signature)
}
// ideally this would have been generic over credential type, but we really don't need secp256k1 keys for bandwidth vouchers
pub async fn obtain_partial_bandwidth_voucher_credential(
&self,
client: &nym_validator_client::client::NymApiClient,
validator_vk: &VerificationKey,
signing_data: impl Into<Option<CredentialSigningData>>,
) -> Result<Signature, Error> {
// if we provided signing data, do use them, otherwise generate fresh data
let signing_data = signing_data
.into()
.unwrap_or_else(|| self.prepare_for_signing());
let blinded_signature = match &self.variant_data {
BandwidthCredentialIssuanceDataVariant::Voucher(voucher) => {
// TODO: the request can be re-used between different apis
let request = voucher.create_blind_sign_request_body(&signing_data);
voucher.obtain_blinded_credential(client, &request).await?
}
_ => return Err(Error::NotABandwdithVoucher),
};
self.unblind_signature(validator_vk, &signing_data, blinded_signature)
}
pub fn aggregate_signature_shares(
&self,
verification_key: &VerificationKey,
shares: &[SignatureShare],
) -> Result<Signature, Error> {
let public_attributes = self.get_public_attributes();
let private_attributes = self.get_private_attributes();
let params = bandwidth_credential_params();
let mut attributes = Vec::with_capacity(private_attributes.len() + public_attributes.len());
attributes.extend_from_slice(&private_attributes);
attributes.extend_from_slice(&public_attributes);
aggregate_signature_shares(params, verification_key, &attributes, shares)
.map_err(Error::SignatureAggregationError)
}
// also drops self after the conversion
pub fn into_issued_credential(
self,
aggregate_signature: Signature,
epoch_id: EpochId,
) -> IssuedBandwidthCredential {
self.to_issued_credential(aggregate_signature, epoch_id)
}
pub fn to_issued_credential(
&self,
aggregate_signature: Signature,
epoch_id: EpochId,
) -> IssuedBandwidthCredential {
IssuedBandwidthCredential::new(
self.serial_number,
self.binding_number,
aggregate_signature,
(&self.variant_data).into(),
self.type_prehashed,
epoch_id,
)
}
// TODO: is that actually needed?
pub fn to_recovery_bytes(&self) -> Vec<u8> {
use bincode::Options;
// safety: our data format is stable and thus the serialization should not fail
make_recovery_bincode_serializer().serialize(self).unwrap()
}
// TODO: is that actually needed?
pub fn try_from_recovered_bytes(bytes: &[u8]) -> Result<Self, Error> {
use bincode::Options;
Ok(make_recovery_bincode_serializer().deserialize(bytes)?)
}
}
fn make_recovery_bincode_serializer() -> impl bincode::Options {
use bincode::Options;
bincode::DefaultOptions::new()
.with_big_endian()
.with_varint_encoding()
}
@@ -0,0 +1,185 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::coconut::bandwidth::bandwidth_credential_params;
use crate::coconut::bandwidth::freepass::FreePassIssuedData;
use crate::coconut::bandwidth::issuance::{
BandwidthCredentialIssuanceDataVariant, IssuanceBandwidthCredential,
};
use crate::coconut::bandwidth::voucher::BandwidthVoucherIssuedData;
use crate::coconut::bandwidth::{CredentialSpendingData, CredentialType};
use crate::coconut::utils::scalar_serde_helper;
use crate::error::Error;
use nym_credentials_interface::prove_bandwidth_credential;
use nym_credentials_interface::{
Parameters, PrivateAttribute, PublicAttribute, Signature, VerificationKey,
};
use nym_validator_client::nym_api::EpochId;
use serde::{Deserialize, Serialize};
use zeroize::{Zeroize, ZeroizeOnDrop};
pub const CURRENT_SERIALIZATION_REVISION: u8 = 1;
#[derive(Zeroize, Serialize, Deserialize)]
pub enum BandwidthCredentialIssuedDataVariant {
Voucher(BandwidthVoucherIssuedData),
FreePass(FreePassIssuedData),
}
impl<'a> From<&'a BandwidthCredentialIssuanceDataVariant> for BandwidthCredentialIssuedDataVariant {
fn from(value: &'a BandwidthCredentialIssuanceDataVariant) -> Self {
match value {
BandwidthCredentialIssuanceDataVariant::Voucher(voucher) => {
BandwidthCredentialIssuedDataVariant::Voucher(voucher.into())
}
BandwidthCredentialIssuanceDataVariant::FreePass(freepass) => {
BandwidthCredentialIssuedDataVariant::FreePass(freepass.into())
}
}
}
}
impl From<FreePassIssuedData> for BandwidthCredentialIssuedDataVariant {
fn from(value: FreePassIssuedData) -> Self {
BandwidthCredentialIssuedDataVariant::FreePass(value)
}
}
impl From<BandwidthVoucherIssuedData> for BandwidthCredentialIssuedDataVariant {
fn from(value: BandwidthVoucherIssuedData) -> Self {
BandwidthCredentialIssuedDataVariant::Voucher(value)
}
}
impl BandwidthCredentialIssuedDataVariant {
pub fn info(&self) -> CredentialType {
match self {
BandwidthCredentialIssuedDataVariant::Voucher(..) => CredentialType::Voucher,
BandwidthCredentialIssuedDataVariant::FreePass(..) => CredentialType::FreePass,
}
}
// currently this works under the assumption of there being a single unique public attribute for given variant
pub fn public_value_plain(&self) -> String {
match self {
BandwidthCredentialIssuedDataVariant::Voucher(voucher) => voucher.value_plain(),
BandwidthCredentialIssuedDataVariant::FreePass(freepass) => {
freepass.expiry_date_plain()
}
}
}
}
// the only important thing to zeroize here are the private attributes, the rest can be made fully public for what we're concerned
#[derive(Zeroize, ZeroizeOnDrop, Serialize, Deserialize)]
pub struct IssuedBandwidthCredential {
// private attributes
/// a random secret value generated by the client used for double-spending detection
#[serde(with = "scalar_serde_helper")]
serial_number: PrivateAttribute,
/// a random secret value generated by the client used to bind multiple credentials together
#[serde(with = "scalar_serde_helper")]
binding_number: PrivateAttribute,
/// the underlying aggregated signature on the attributes
#[zeroize(skip)]
signature: Signature,
/// data specific to given bandwidth credential, for example a value for bandwidth voucher and expiry date for the free pass
variant_data: BandwidthCredentialIssuedDataVariant,
/// type of the bandwdith credential hashed onto a scalar
#[serde(with = "scalar_serde_helper")]
type_prehashed: PublicAttribute,
/// Specifies the (DKG) epoch id when this credential has been issued
epoch_id: EpochId,
}
impl IssuedBandwidthCredential {
pub fn new(
serial_number: PrivateAttribute,
binding_number: PrivateAttribute,
signature: Signature,
variant_data: BandwidthCredentialIssuedDataVariant,
type_prehashed: PublicAttribute,
epoch_id: EpochId,
) -> Self {
IssuedBandwidthCredential {
serial_number,
binding_number,
signature,
variant_data,
type_prehashed,
epoch_id,
}
}
pub fn current_serialization_revision(&self) -> u8 {
CURRENT_SERIALIZATION_REVISION
}
/// Pack (serialize) this credential data into a stream of bytes using v1 serializer.
pub fn pack_v1(&self) -> Vec<u8> {
use bincode::Options;
// safety: our data format is stable and thus the serialization should not fail
make_storable_bincode_serializer().serialize(self).unwrap()
}
/// Unpack (deserialize) the credential data from the given bytes using v1 serializer.
pub fn unpack_v1(bytes: &[u8]) -> Result<Self, Error> {
use bincode::Options;
Ok(make_storable_bincode_serializer().deserialize(bytes)?)
}
pub fn randomise_signature(&mut self) {
let signature_prime = self.signature.randomise(bandwidth_credential_params());
self.signature = signature_prime.0
}
pub fn default_parameters() -> Parameters {
IssuanceBandwidthCredential::default_parameters()
}
pub fn typ(&self) -> CredentialType {
self.variant_data.info()
}
pub fn get_plain_public_attributes(&self) -> Vec<String> {
vec![
self.variant_data.public_value_plain(),
self.typ().to_string(),
]
}
pub fn prepare_for_spending(
&self,
verification_key: &VerificationKey,
) -> Result<CredentialSpendingData, Error> {
let params = bandwidth_credential_params();
let verify_credential_request = prove_bandwidth_credential(
params,
verification_key,
&self.signature,
&self.serial_number,
&self.binding_number,
)?;
Ok(CredentialSpendingData {
embedded_private_attributes: IssuanceBandwidthCredential::PRIVATE_ATTRIBUTES as usize,
verify_credential_request,
public_attributes_plain: self.get_plain_public_attributes(),
typ: self.typ(),
epoch_id: self.epoch_id,
})
}
}
fn make_storable_bincode_serializer() -> impl bincode::Options {
use bincode::Options;
bincode::DefaultOptions::new()
.with_big_endian()
.with_varint_encoding()
}
@@ -0,0 +1,22 @@
// Copyright 2021-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::sync::OnceLock;
pub use issuance::IssuanceBandwidthCredential;
pub use issued::IssuedBandwidthCredential;
pub use nym_credentials_interface::{
CredentialSigningData, CredentialSpendingData, CredentialType, Parameters,
UnknownCredentialType,
};
pub mod freepass;
pub mod issuance;
pub mod issued;
pub mod voucher;
// works under the assumption of having 4 attributes in the underlying credential(s)
pub fn bandwidth_credential_params() -> &'static Parameters {
static BANDWIDTH_CREDENTIAL_PARAMS: OnceLock<Parameters> = OnceLock::new();
BANDWIDTH_CREDENTIAL_PARAMS.get_or_init(IssuanceBandwidthCredential::default_parameters)
}
@@ -0,0 +1,133 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::coconut::bandwidth::CredentialSigningData;
use crate::coconut::utils::scalar_serde_helper;
use crate::error::Error;
use nym_api_requests::coconut::BlindSignRequestBody;
use nym_credentials_interface::{
hash_to_scalar, Attribute, BlindSignRequest, BlindedSignature, PublicAttribute,
};
use nym_crypto::asymmetric::{encryption, identity};
use nym_validator_client::nyxd::{Coin, Hash};
use serde::{Deserialize, Serialize};
use zeroize::{Zeroize, ZeroizeOnDrop};
#[derive(Zeroize, ZeroizeOnDrop, Serialize, Deserialize)]
pub struct BandwidthVoucherIssuedData {
/// the plain value (e.g., bandwidth) encoded in this voucher
// note: for legacy reasons we're only using the value of the coin and ignoring the denom
#[zeroize(skip)]
value: Coin,
}
impl<'a> From<&'a BandwidthVoucherIssuanceData> for BandwidthVoucherIssuedData {
fn from(value: &'a BandwidthVoucherIssuanceData) -> Self {
BandwidthVoucherIssuedData {
value: value.value.clone(),
}
}
}
impl BandwidthVoucherIssuedData {
pub fn value_plain(&self) -> String {
self.value.amount.to_string()
}
}
#[derive(Zeroize, ZeroizeOnDrop, Serialize, Deserialize)]
pub struct BandwidthVoucherIssuanceData {
/// the plain value (e.g., bandwidth) encoded in this voucher
// note: for legacy reasons we're only using the value of the coin and ignoring the denom
#[zeroize(skip)]
value: Coin,
// note: as mentioned above, we're only hashing the value of the coin!
#[serde(with = "scalar_serde_helper")]
value_prehashed: PublicAttribute,
/// the hash of the deposit transaction
#[zeroize(skip)]
deposit_tx_hash: Hash,
/// base58 encoded private key ensuring the depositer requested these attributes
signing_key: identity::PrivateKey,
/// base58 encoded private key ensuring only this client receives the signature share
unused_ed25519: encryption::PrivateKey,
}
impl BandwidthVoucherIssuanceData {
pub fn new(
value: impl Into<Coin>,
deposit_tx_hash: Hash,
signing_key: identity::PrivateKey,
unused_ed25519: encryption::PrivateKey,
) -> Self {
let value = value.into();
let value_prehashed = hash_to_scalar(value.amount.to_string());
BandwidthVoucherIssuanceData {
value,
value_prehashed,
deposit_tx_hash,
signing_key,
unused_ed25519,
}
}
pub fn request_plaintext(request: &BlindSignRequest, tx_hash: Hash) -> Vec<u8> {
let mut message = request.to_bytes();
message.extend_from_slice(tx_hash.as_bytes());
message
}
fn request_signature(&self, signing_request: &CredentialSigningData) -> identity::Signature {
let message =
Self::request_plaintext(&signing_request.blind_sign_request, self.deposit_tx_hash);
self.signing_key.sign(message)
}
pub fn create_blind_sign_request_body(
&self,
signing_request: &CredentialSigningData,
) -> BlindSignRequestBody {
let request_signature = self.request_signature(signing_request);
BlindSignRequestBody::new(
signing_request.blind_sign_request.clone(),
self.deposit_tx_hash,
request_signature,
signing_request.public_attributes_plain.clone(),
)
}
pub async fn obtain_blinded_credential(
&self,
client: &nym_validator_client::client::NymApiClient,
request_body: &BlindSignRequestBody,
) -> Result<BlindedSignature, Error> {
let server_response = client.blind_sign(request_body).await?;
Ok(server_response.blinded_signature)
}
pub fn value_plain(&self) -> String {
self.value.amount.to_string()
}
pub fn value_attribute(&self) -> &Attribute {
&self.value_prehashed
}
pub fn tx_hash(&self) -> Hash {
self.deposit_tx_hash
}
pub fn identity_key(&self) -> &identity::PrivateKey {
&self.signing_key
}
pub fn encryption_key(&self) -> &encryption::PrivateKey {
&self.unused_ed25519
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// Copyright 2021-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod bandwidth;
+36 -96
View File
@@ -1,17 +1,15 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// Copyright 2021-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::coconut::bandwidth::BandwidthVoucher;
use crate::coconut::bandwidth::IssuanceBandwidthCredential;
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,
Credential, Parameters, Signature, SignatureShare, VerificationKey,
use nym_credentials_interface::{
aggregate_verification_keys, Signature, SignatureShare, VerificationKey,
};
use nym_validator_client::client::CoconutApiClient;
pub async fn obtain_aggregate_verification_key(
pub fn obtain_aggregate_verification_key(
api_clients: &[CoconutApiClient],
) -> Result<VerificationKey, Error> {
if api_clients.is_empty() {
@@ -30,44 +28,8 @@ pub async fn obtain_aggregate_verification_key(
Ok(aggregate_verification_keys(&shares, Some(&indices))?)
}
async fn obtain_partial_credential(
params: &Parameters,
voucher: &BandwidthVoucher,
client: &nym_validator_client::client::NymApiClient,
validator_vk: &VerificationKey,
) -> Result<Signature, Error> {
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.clone(),
voucher.tx_hash(),
request_signature,
public_attributes_plain,
);
let response = client.blind_sign(&blind_sign_request_body).await?;
let blinded_signature = response.blinded_signature;
let public_attributes = voucher.get_public_attributes();
let private_attributes = voucher.get_private_attributes();
let unblinded_signature = blinded_signature.unblind_and_verify(
params,
validator_vk,
&private_attributes,
&public_attributes,
&blind_sign_request.get_commitment_hash(),
voucher.pedersen_commitments_openings(),
)?;
Ok(unblinded_signature)
}
pub async fn obtain_aggregate_signature(
params: &Parameters,
voucher: &BandwidthVoucher,
voucher: &IssuanceBandwidthCredential,
coconut_api_clients: &[CoconutApiClient],
threshold: u64,
) -> Result<Signature, Error> {
@@ -75,16 +37,9 @@ pub async fn obtain_aggregate_signature(
return Err(Error::NoValidatorsAvailable);
}
let mut shares = Vec::with_capacity(coconut_api_clients.len());
let validators_partial_vks: Vec<_> = coconut_api_clients
.iter()
.map(|api_client| api_client.verification_key.clone())
.collect();
let indices: Vec<_> = coconut_api_clients
.iter()
.map(|api_client| api_client.node_id)
.collect();
let verification_key =
aggregate_verification_keys(&validators_partial_vks, Some(indices.as_ref()))?;
let verification_key = obtain_aggregate_verification_key(coconut_api_clients)?;
let request = voucher.prepare_for_signing();
for coconut_api_client in coconut_api_clients.iter() {
debug!(
@@ -92,13 +47,13 @@ pub async fn obtain_aggregate_signature(
coconut_api_client.api_client.api_url()
);
match obtain_partial_credential(
params,
voucher,
&coconut_api_client.api_client,
&coconut_api_client.verification_key,
)
.await
match voucher
.obtain_partial_bandwidth_voucher_credential(
&coconut_api_client.api_client,
&coconut_api_client.verification_key,
Some(request.clone()),
)
.await
{
Ok(signature) => {
let share = SignatureShare::new(signature, coconut_api_client.node_id);
@@ -116,42 +71,27 @@ 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);
aggregate_signature_shares(params, &verification_key, &attributes, &shares)
.map_err(Error::SignatureAggregationError)
voucher.aggregate_signature_shares(&verification_key, &shares)
}
// TODO: better type flow
#[allow(clippy::too_many_arguments)]
pub fn prepare_credential_for_spending(
params: &Parameters,
voucher_value: u64,
voucher_info: String,
serial_number: &Attribute,
binding_number: &Attribute,
epoch_id: u64,
signature: &Signature,
verification_key: &VerificationKey,
) -> Result<Credential, Error> {
let theta = prove_bandwidth_credential(
params,
verification_key,
signature,
serial_number,
binding_number,
)?;
pub(crate) mod scalar_serde_helper {
use bls12_381::Scalar;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use zeroize::Zeroizing;
Ok(Credential::new(
BandwidthVoucher::ENCODED_ATTRIBUTES,
theta,
voucher_value,
voucher_info,
epoch_id,
))
pub fn serialize<S: Serializer>(scalar: &Scalar, serializer: S) -> Result<S::Ok, S::Error> {
scalar.to_bytes().serialize(serializer)
}
pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Scalar, D::Error> {
let b = <[u8; 32]>::deserialize(deserializer)?;
// make sure the bytes get zeroed
let bytes = Zeroizing::new(b);
let maybe_scalar: Option<Scalar> = Scalar::from_bytes(&bytes).into();
maybe_scalar.ok_or(serde::de::Error::custom(
"did not construct a valid bls12-381 scalar out of the provided bytes",
))
}
}
+13 -1
View File
@@ -1,7 +1,7 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_coconut_interface::CoconutError;
use nym_credentials_interface::CoconutError;
use nym_crypto::asymmetric::encryption::KeyRecoveryError;
use nym_validator_client::ValidatorClientError;
@@ -12,6 +12,9 @@ pub enum Error {
#[error("IO error")]
IOError(#[from] std::io::Error),
#[error("failed to (de)serialize credential structure: {0}")]
SerializationFailure(#[from] bincode::Error),
#[error("The detailed description is yet to be determined")]
BandwidthCredentialError,
@@ -41,4 +44,13 @@ pub enum Error {
#[error("Could not deserialize bandwidth voucher - {0}")]
BandwidthVoucherDeserializationError(String),
#[error("the provided issuance data wasn't prepared for a bandwidth voucher")]
NotABandwdithVoucher,
#[error("the provided issuance data wasn't prepared for a free pass")]
NotAFreePass,
#[error("failed to create a secp256k1 signature")]
Secp256k1SignFailure,
}
+4
View File
@@ -4,4 +4,8 @@
pub mod coconut;
pub mod error;
pub use coconut::bandwidth::{
CredentialSigningData, CredentialSpendingData, IssuanceBandwidthCredential,
IssuedBandwidthCredential,
};
pub use coconut::utils::{obtain_aggregate_signature, obtain_aggregate_verification_key};
+3 -4
View File
@@ -457,6 +457,9 @@ pub const ETH_ERC20_APPROVE_FUNCTION_NAME: &str = "approve";
/// How much bandwidth (in bytes) one token can buy
pub const BYTES_PER_UTOKEN: u64 = 1024;
/// How much bandwidth (in bytes) one freepass provides
pub const BYTES_PER_FREEPASS: u64 = 1024 * 1024 * 1024; // 1GB
/// Threshold for claiming more bandwidth: 1 MB
pub const REMAINING_BANDWIDTH_THRESHOLD: i64 = 1024 * 1024;
/// How many ERC20 tokens should be burned to buy bandwidth
@@ -466,10 +469,6 @@ pub const UTOKENS_TO_BURN: u64 = TOKENS_TO_BURN * 1000000;
/// Default bandwidth (in bytes) that we try to buy
pub const BANDWIDTH_VALUE: u64 = UTOKENS_TO_BURN * BYTES_PER_UTOKEN;
pub const VOUCHER_INFO: &str = "BandwidthVoucher";
pub const ETH_MIN_BLOCK_DEPTH: usize = 7;
/// Defaults Cosmos Hub/ATOM path
pub const COSMOS_DERIVATION_PATH: &str = "m/44'/118'/0'/0/0";
// as set by validators in their configs
+2 -2
View File
@@ -1,4 +1,4 @@
use crate::{BlindSignRequest, BlindedSignature, Bytable, Theta};
use crate::{BlindSignRequest, BlindedSignature, Bytable, VerifyCredentialRequest};
macro_rules! impl_clone {
($struct:ident) => {
@@ -12,4 +12,4 @@ macro_rules! impl_clone {
impl_clone!(BlindSignRequest);
impl_clone!(BlindedSignature);
impl_clone!(Theta);
impl_clone!(VerifyCredentialRequest);
+3 -2
View File
@@ -1,7 +1,8 @@
use crate::elgamal::PrivateKey;
use crate::scheme::SecretKey;
use crate::{
Base58, BlindSignRequest, BlindedSignature, PublicKey, Signature, Theta, VerificationKey,
Base58, BlindSignRequest, BlindedSignature, PublicKey, Signature, VerificationKey,
VerifyCredentialRequest,
};
use serde::de::Unexpected;
use serde::{de::Error, de::Visitor, Deserialize, Deserializer, Serialize, Serializer};
@@ -53,4 +54,4 @@ impl_serde!(PrivateKey, V4);
impl_serde!(BlindSignRequest, V5);
impl_serde!(BlindedSignature, V6);
impl_serde!(Signature, V7);
impl_serde!(Theta, V8);
impl_serde!(VerifyCredentialRequest, V8);
+3 -1
View File
@@ -14,6 +14,7 @@ pub use scheme::issuance::blind_sign;
pub use scheme::issuance::prepare_blind_sign;
pub use scheme::issuance::verify_partial_blind_signature;
pub use scheme::issuance::BlindSignRequest;
pub use scheme::keygen::keygen;
pub use scheme::keygen::ttp_keygen;
pub use scheme::keygen::KeyPair;
pub use scheme::keygen::SecretKey;
@@ -23,7 +24,8 @@ pub use scheme::setup::Parameters;
pub use scheme::verification::check_vk_pairing;
pub use scheme::verification::prove_bandwidth_credential;
pub use scheme::verification::verify_credential;
pub use scheme::verification::Theta;
pub use scheme::verification::BlindedSerialNumber;
pub use scheme::verification::VerifyCredentialRequest;
pub use scheme::BlindedSignature;
pub use scheme::Signature;
pub use scheme::SignatureShare;
+39 -10
View File
@@ -1,17 +1,46 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use bls12_381::G2Projective;
use group::Curve;
use std::convert::TryFrom;
use std::convert::TryInto;
use crate::error::{CoconutError, Result};
use crate::traits::{Base58, Bytable};
use crate::utils::try_deserialize_g2_projective;
use bls12_381::{G2Affine, G2Projective};
use group::Curve;
use std::convert::TryFrom;
use std::convert::TryInto;
use std::fmt::{Debug, Formatter};
use std::ops::Deref;
pub struct BlindedSerialNumber {
pub(crate) inner: G2Projective,
#[derive(PartialEq, Eq, Clone, Copy)]
pub struct BlindedSerialNumber(G2Projective);
// use custom Debug implementation to show base58 encoding (rather than raw curve elements)
impl Debug for BlindedSerialNumber {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("BlindedSerialNumber")
.field(&self.to_bs58())
.finish()
}
}
impl From<G2Projective> for BlindedSerialNumber {
fn from(value: G2Projective) -> Self {
BlindedSerialNumber(value)
}
}
impl From<G2Affine> for BlindedSerialNumber {
fn from(value: G2Affine) -> Self {
BlindedSerialNumber(value.into())
}
}
impl Deref for BlindedSerialNumber {
type Target = G2Projective;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl TryFrom<&[u8]> for BlindedSerialNumber {
@@ -34,13 +63,13 @@ impl TryFrom<&[u8]> for BlindedSerialNumber {
),
)?;
Ok(BlindedSerialNumber { inner })
Ok(BlindedSerialNumber(inner))
}
}
impl Bytable for BlindedSerialNumber {
fn to_byte_vec(&self) -> Vec<u8> {
self.inner.to_affine().to_compressed().to_vec()
self.0.to_affine().to_compressed().to_vec()
}
fn try_from_byte_slice(slice: &[u8]) -> Result<Self> {
-1
View File
@@ -565,7 +565,6 @@ impl TryFrom<&[u8]> for KeyPair {
/// Generate a single Coconut keypair ((x, y0, y1...), (g2^x, g2^y0, ...)).
/// It is not suitable for threshold credentials as all subsequent calls to `keygen` generate keys
/// that are independent of each other.
#[cfg(test)]
pub fn keygen(params: &Parameters) -> KeyPair {
let attributes = params.gen_hs().len();
+9
View File
@@ -248,6 +248,15 @@ pub struct SignatureShare {
index: SignerIndex,
}
impl From<(Signature, SignerIndex)> for SignatureShare {
fn from(value: (Signature, SignerIndex)) -> Self {
SignatureShare {
signature: value.0,
index: value.1,
}
}
}
impl SignatureShare {
pub fn new(signature: Signature, index: SignerIndex) -> Self {
SignatureShare { signature, index }
+3 -3
View File
@@ -44,11 +44,11 @@ impl Parameters {
})
}
pub(crate) fn gen1(&self) -> &G1Affine {
pub fn gen1(&self) -> &G1Affine {
&self.g1
}
pub(crate) fn gen2(&self) -> &G2Affine {
pub fn gen2(&self) -> &G2Affine {
&self.g2
}
@@ -56,7 +56,7 @@ impl Parameters {
&self._g2_prepared_miller
}
pub(crate) fn gen_hs(&self) -> &[G1Affine] {
pub fn gen_hs(&self) -> &[G1Affine] {
&self.hs
}
+37 -39
View File
@@ -1,41 +1,40 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// Copyright 2021-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use core::ops::Neg;
use std::convert::TryFrom;
use std::convert::TryInto;
use bls12_381::{multi_miller_loop, G1Affine, G2Prepared, G2Projective, Scalar};
use group::{Curve, Group};
use crate::error::{CoconutError, Result};
use crate::proofs::ProofKappaZeta;
use crate::scheme::double_use::BlindedSerialNumber;
use crate::scheme::setup::Parameters;
use crate::scheme::Signature;
use crate::scheme::VerificationKey;
use crate::traits::{Base58, Bytable};
use crate::utils::try_deserialize_g2_projective;
use crate::Attribute;
use bls12_381::{multi_miller_loop, G1Affine, G2Prepared, G2Projective, Scalar};
use core::ops::Neg;
use group::{Curve, Group};
use std::convert::TryFrom;
use std::convert::TryInto;
pub use crate::scheme::double_use::BlindedSerialNumber;
// TODO NAMING: this whole thing
// Theta
#[derive(Debug, PartialEq, Eq)]
pub struct Theta {
pub struct VerifyCredentialRequest {
// blinded_message (kappa)
pub blinded_message: G2Projective,
// blinded serial number (zeta)
pub blinded_serial_number: G2Projective,
pub blinded_serial_number: BlindedSerialNumber,
// sigma
pub credential: Signature,
// pi_v
pub pi_v: ProofKappaZeta,
}
impl TryFrom<&[u8]> for Theta {
impl TryFrom<&[u8]> for VerifyCredentialRequest {
type Error = CoconutError;
fn try_from(bytes: &[u8]) -> Result<Theta> {
fn try_from(bytes: &[u8]) -> Result<VerifyCredentialRequest> {
if bytes.len() < 288 {
return Err(
CoconutError::Deserialization(
@@ -53,20 +52,15 @@ 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,
CoconutError::Deserialization(
"failed to deserialize the blinded serial number (zeta)".to_string(),
),
)?;
let blinded_serial_number_bytes = &bytes[96..192];
let blinded_serial_number =
BlindedSerialNumber::try_from_byte_slice(blinded_serial_number_bytes)?;
let credential = Signature::try_from(&bytes[192..288])?;
let pi_v = ProofKappaZeta::from_bytes(&bytes[288..])?;
Ok(Theta {
Ok(VerifyCredentialRequest {
blinded_message,
blinded_serial_number,
credential,
@@ -75,7 +69,7 @@ impl TryFrom<&[u8]> for Theta {
}
}
impl Theta {
impl VerifyCredentialRequest {
fn verify_proof(&self, params: &Parameters, verification_key: &VerificationKey) -> bool {
self.pi_v.verify(
params,
@@ -87,7 +81,7 @@ impl Theta {
pub fn has_blinded_serial_number(&self, blinded_serial_number_bs58: &str) -> Result<bool> {
let blinded_serial_number = BlindedSerialNumber::try_from_bs58(blinded_serial_number_bs58)?;
let ret = self.blinded_serial_number.eq(&blinded_serial_number.inner);
let ret = self.blinded_serial_number.eq(&blinded_serial_number);
Ok(ret)
}
@@ -107,29 +101,30 @@ impl Theta {
bytes
}
pub fn from_bytes(bytes: &[u8]) -> Result<Theta> {
Theta::try_from(bytes)
pub fn from_bytes(bytes: &[u8]) -> Result<VerifyCredentialRequest> {
VerifyCredentialRequest::try_from(bytes)
}
pub fn blinded_serial_number(&self) -> BlindedSerialNumber {
self.blinded_serial_number
}
pub fn blinded_serial_number_bs58(&self) -> String {
let blinded_serial_nuumber = BlindedSerialNumber {
inner: self.blinded_serial_number,
};
blinded_serial_nuumber.to_bs58()
self.blinded_serial_number.to_bs58()
}
}
impl Bytable for Theta {
impl Bytable for VerifyCredentialRequest {
fn to_byte_vec(&self) -> Vec<u8> {
self.to_bytes()
}
fn try_from_byte_slice(slice: &[u8]) -> Result<Self> {
Theta::try_from(slice)
VerifyCredentialRequest::try_from(slice)
}
}
impl Base58 for Theta {}
impl Base58 for VerifyCredentialRequest {}
pub fn compute_kappa(
params: &Parameters,
@@ -156,7 +151,7 @@ pub fn prove_bandwidth_credential(
signature: &Signature,
serial_number: &Attribute,
binding_number: &Attribute,
) -> Result<Theta> {
) -> Result<VerifyCredentialRequest> {
if verification_key.beta_g2.len() < 2 {
return Err(
CoconutError::Verification(
@@ -196,9 +191,9 @@ pub fn prove_bandwidth_credential(
&blinded_serial_number,
);
Ok(Theta {
Ok(VerifyCredentialRequest {
blinded_message,
blinded_serial_number,
blinded_serial_number: blinded_serial_number.into(),
credential: signature_prime,
pi_v,
})
@@ -256,7 +251,7 @@ pub fn check_vk_pairing(
pub fn verify_credential(
params: &Parameters,
verification_key: &VerificationKey,
theta: &Theta,
theta: &VerifyCredentialRequest,
public_attributes: &[&Attribute],
) -> bool {
if public_attributes.len() + theta.pi_v.private_attributes_len()
@@ -358,6 +353,9 @@ mod tests {
.unwrap();
let bytes = theta.to_bytes();
assert_eq!(Theta::try_from(bytes.as_slice()).unwrap(), theta);
assert_eq!(
VerifyCredentialRequest::try_from(bytes.as_slice()).unwrap(),
theta
);
}
}
+1 -1
View File
@@ -12,7 +12,7 @@ pub fn theta_from_keys_and_attributes(
coconut_keypairs: &Vec<KeyPair>,
indices: &[scheme::SignerIndex],
public_attributes: &[&PublicAttribute],
) -> Result<Theta, CoconutError> {
) -> Result<VerifyCredentialRequest, CoconutError> {
let serial_number = params.random_scalar();
let binding_number = params.random_scalar();
let private_attributes = vec![&serial_number, &binding_number];
-1
View File
@@ -31,7 +31,6 @@ nym-validator-client = { path = "../../common/client-libs/validator-client" }
nym-mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract" }
nym-vesting-contract-common = { path = "../../common/cosmwasm-smart-contracts/vesting-contract" }
nym-config = { path = "../../common/config" }
nym-coconut-interface = { path = "../../common/coconut-interface" }
nym-crypto = { path = "../../common/crypto", features = ["asymmetric"] }
[dev-dependencies]
+2 -1
View File
@@ -55,6 +55,7 @@ tokio-stream = { version = "0.1.11", features = ["fs"] }
tokio-tungstenite = { version = "0.20.1" }
tokio-util = { workspace = true, features = ["codec"] }
url = { workspace = true, features = ["serde"] }
time = { workspace = true }
zeroize = { workspace = true }
# internal
@@ -62,9 +63,9 @@ nym-node = { path = "../nym-node" }
nym-api-requests = { path = "../nym-api/nym-api-requests" }
nym-bin-common = { path = "../common/bin-common", features = ["output_format"] }
nym-coconut-interface = { path = "../common/coconut-interface" }
nym-config = { path = "../common/config" }
nym-credentials = { path = "../common/credentials" }
nym-credentials-interface = { path = "../common/credentials-interface" }
nym-crypto = { path = "../common/crypto" }
nym-gateway-requests = { path = "gateway-requests" }
nym-mixnet-client = { path = "../common/client-libs/mixnet-client" }
+2 -1
View File
@@ -25,10 +25,11 @@ nym-crypto = { path = "../../common/crypto" }
nym-pemstore = { path = "../../common/pemstore" }
nym-sphinx = { path = "../../common/nymsphinx" }
nym-coconut-interface = { path = "../../common/coconut-interface" }
nym-credentials = { path = "../../common/credentials" }
nym-credentials-interface = { path = "../../common/credentials-interface" }
[dependencies.tungstenite]
workspace = true
default-features = false
+6 -1
View File
@@ -9,12 +9,17 @@ pub use types::*;
pub mod authentication;
pub mod iv;
pub mod models;
pub mod registration;
pub mod types;
/// Defines the current version of the communication protocol between gateway and clients.
/// It has to be incremented for any breaking change.
pub const PROTOCOL_VERSION: u8 = 1;
// history:
// 1 - initial release
// 2 - changes to client credentials structure
pub const INITIAL_PROTOCOL_VERSION: u8 = 1;
pub const CURRENT_PROTOCOL_VERSION: u8 = 2;
pub type GatewayMac = HmacOutput<GatewayIntegrityHmacAlgorithm>;
+338
View File
@@ -0,0 +1,338 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::GatewayRequestsError;
use nym_credentials::coconut::bandwidth::CredentialSpendingData;
use nym_credentials_interface::{CoconutError, VerifyCredentialRequest};
use serde::{Deserialize, Serialize};
// reimplements old coconut-interface::Credential for backwards compatibility sake
// (so that 'new' gateways could still understand those requests)
#[derive(Debug, PartialEq, Eq)]
pub struct OldV1Credential {
pub n_params: u32,
pub theta: VerifyCredentialRequest,
pub voucher_value: u64,
pub voucher_info: String,
pub epoch_id: u64,
}
// attempt to convert the old request type into the new variant
impl TryFrom<OldV1Credential> for CredentialSpendingRequest {
type Error = GatewayRequestsError;
fn try_from(value: OldV1Credential) -> Result<Self, Self::Error> {
if value.n_params <= 2 {
return Err(GatewayRequestsError::InvalidNumberOfEmbededParameters(
value.n_params,
));
}
let embedded_private_attributes = value.n_params as usize - 2;
let typ = value.voucher_info.parse()?;
let public_attributes_plain = vec![value.voucher_value.to_string(), value.voucher_info];
Ok(CredentialSpendingRequest {
data: CredentialSpendingData {
embedded_private_attributes,
verify_credential_request: value.theta,
public_attributes_plain,
typ,
epoch_id: value.epoch_id,
},
})
}
}
impl OldV1Credential {
pub fn as_bytes(&self) -> Vec<u8> {
let n_params_bytes = self.n_params.to_be_bytes();
let theta_bytes = self.theta.to_bytes();
let theta_bytes_len = theta_bytes.len();
let voucher_value_bytes = self.voucher_value.to_be_bytes();
let epoch_id_bytes = self.epoch_id.to_be_bytes();
let voucher_info_bytes = self.voucher_info.as_bytes();
let voucher_info_len = voucher_info_bytes.len();
let mut bytes = Vec::with_capacity(28 + theta_bytes_len + voucher_info_len);
bytes.extend_from_slice(&n_params_bytes);
bytes.extend_from_slice(&(theta_bytes_len as u64).to_be_bytes());
bytes.extend_from_slice(&theta_bytes);
bytes.extend_from_slice(&voucher_value_bytes);
bytes.extend_from_slice(&epoch_id_bytes);
bytes.extend_from_slice(voucher_info_bytes);
bytes
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, CoconutError> {
if bytes.len() < 28 {
return Err(CoconutError::Deserialization(String::from(
"To few bytes in credential",
)));
}
let mut four_byte = [0u8; 4];
let mut eight_byte = [0u8; 8];
four_byte.copy_from_slice(&bytes[..4]);
let n_params = u32::from_be_bytes(four_byte);
eight_byte.copy_from_slice(&bytes[4..12]);
let theta_len = u64::from_be_bytes(eight_byte);
if bytes.len() < 28 + theta_len as usize {
return Err(CoconutError::Deserialization(String::from(
"To few bytes in credential",
)));
}
let theta = VerifyCredentialRequest::from_bytes(&bytes[12..12 + theta_len as usize])
.map_err(|e| CoconutError::Deserialization(e.to_string()))?;
eight_byte.copy_from_slice(&bytes[12 + theta_len as usize..20 + theta_len as usize]);
let voucher_value = u64::from_be_bytes(eight_byte);
eight_byte.copy_from_slice(&bytes[20 + theta_len as usize..28 + theta_len as usize]);
let epoch_id = u64::from_be_bytes(eight_byte);
let voucher_info = String::from_utf8(bytes[28 + theta_len as usize..].to_vec())
.map_err(|e| CoconutError::Deserialization(e.to_string()))?;
Ok(OldV1Credential {
n_params,
theta,
voucher_value,
voucher_info,
epoch_id,
})
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct CredentialSpendingRequest {
/// The cryptographic material required for spending the underlying credential.
pub data: CredentialSpendingData,
}
// just a helper macro for checking required length and advancing the buffer
macro_rules! ensure_len_and_advance {
($b:expr, $n:expr) => {{
if $b.len() < $n {
return Err(GatewayRequestsError::CredentialDeserializationFailureEOF);
}
// create binding to the desired range
let bytes = &$b[..$n];
// update the initial binding
$b = &$b[$n..];
bytes
}};
}
impl CredentialSpendingRequest {
pub fn new(data: CredentialSpendingData) -> Self {
CredentialSpendingRequest { data }
}
pub fn matches_blinded_serial_number(
&self,
blinded_serial_number_bs58: &str,
) -> Result<bool, CoconutError> {
self.data
.verify_credential_request
.has_blinded_serial_number(blinded_serial_number_bs58)
}
pub fn unchecked_voucher_value(&self) -> u64 {
self.data
.get_bandwidth_attribute()
.expect("failed to extract bandwidth attribute")
.parse()
.expect("failed to parse voucher value")
}
pub fn to_bytes(&self) -> Vec<u8> {
// simple length prefixed serialization
// TODO: change it to a standard format instead
let mut bytes = Vec::new();
let embedded_private = (self.data.embedded_private_attributes as u32).to_be_bytes();
let theta = self.data.verify_credential_request.to_bytes();
let theta_len = (theta.len() as u32).to_be_bytes();
let public = (self.data.public_attributes_plain.len() as u32).to_be_bytes();
let typ = self.data.typ.to_string();
let typ_bytes = typ.as_bytes();
let typ_len = (typ_bytes.len() as u32).to_be_bytes();
bytes.extend_from_slice(&embedded_private);
bytes.extend_from_slice(&theta_len);
bytes.extend_from_slice(&theta);
bytes.extend_from_slice(&public);
for pub_element in &self.data.public_attributes_plain {
let bytes_el = pub_element.as_bytes();
let len = (bytes_el.len() as u32).to_be_bytes();
bytes.extend_from_slice(&len);
bytes.extend_from_slice(bytes_el);
}
bytes.extend_from_slice(&typ_len);
bytes.extend_from_slice(typ_bytes);
bytes.extend_from_slice(&self.data.epoch_id.to_be_bytes());
bytes
}
pub fn try_from_bytes(raw: &[u8]) -> Result<Self, GatewayRequestsError> {
// initial binding
let mut b = raw;
let embedded_private_bytes = ensure_len_and_advance!(b, 4);
let embedded_private_attributes =
u32::from_be_bytes(embedded_private_bytes.try_into().unwrap()) as usize;
let theta_len_bytes = ensure_len_and_advance!(b, 4);
let theta_len = u32::from_be_bytes(theta_len_bytes.try_into().unwrap()) as usize;
let theta_bytes = ensure_len_and_advance!(b, theta_len);
let theta = VerifyCredentialRequest::from_bytes(theta_bytes)
.map_err(GatewayRequestsError::CredentialDeserializationFailureMalformedTheta)?;
let public_bytes = ensure_len_and_advance!(b, 4);
let public = u32::from_be_bytes(public_bytes.try_into().unwrap()) as usize;
let mut public_attributes_plain = Vec::with_capacity(public);
for _ in 0..public {
let element_len_bytes = ensure_len_and_advance!(b, 4);
let element_len = u32::from_be_bytes(element_len_bytes.try_into().unwrap()) as usize;
let element_bytes = ensure_len_and_advance!(b, element_len);
let element = String::from_utf8(element_bytes.to_vec())?;
public_attributes_plain.push(element);
}
let typ_len_bytes = ensure_len_and_advance!(b, 4);
let typ_len = u32::from_be_bytes(typ_len_bytes.try_into().unwrap()) as usize;
let typ_bytes = ensure_len_and_advance!(b, typ_len);
let raw_typ = String::from_utf8(typ_bytes.to_vec())?;
let typ = raw_typ.parse()?;
// tell the linter to chill out in for this last iteration
#[allow(unused_assignments)]
let epoch_id_bytes = ensure_len_and_advance!(b, 8);
let epoch_id = u64::from_be_bytes(epoch_id_bytes.try_into().unwrap());
Ok(CredentialSpendingRequest {
data: CredentialSpendingData {
embedded_private_attributes,
verify_credential_request: theta,
public_attributes_plain,
typ,
epoch_id,
},
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use nym_credentials::coconut::bandwidth::bandwidth_credential_params;
use nym_credentials::IssuanceBandwidthCredential;
use nym_credentials_interface::{
blind_sign, hash_to_scalar, prove_bandwidth_credential, Attribute, Base58, Parameters,
Signature, VerificationKey,
};
#[test]
fn old_v1_coconut_credential_roundtrip() {
let voucher_value = 1000000u64;
let voucher_info = String::from("BandwidthVoucher");
let serial_number =
Attribute::try_from_bs58("7Rp3imcuNX3w9se9wm5th8gSvc2czsnMrGsdt5HsrycA").unwrap();
let binding_number =
Attribute::try_from_bs58("Auf8yVEgyEAWNHaXUZmimS4n9g5YiYnNYqp6F9BtBe9E").unwrap();
let signature = Signature::try_from_bs58(
"ta3pM9ffj5T6YGbwjSBp2W118rcwyP9PXStc\
7ssb91g5GQYMQHhuTNajbdZcjxUFBFL5rhED8EHpRzE8r432ss3qbPBfpNev4CdkfMkQ3wepyM7hy7q1W6Rn9WmFoZL\
ZR9j",
)
.unwrap();
let params = Parameters::new(4).unwrap();
let verification_key = VerificationKey::try_from_bs58("8CFtVVXdwLy4WHMQPE4\
woe89q3DRHoNxBSchftrEjSBPWA4r4xZv4Y9qSvS5x5bMmFtp7BX6ikECAnuXr5EjXWSsgjirZJmpS5XDUynVfht1cD\
FWGDvy2XFrRCuoCMotNXi3PoF6wYqdTR9Rqcfoj3i2H5Nid422WBaLtVoC9QNobvpvaqq6vX5PbsSyPayvU8HCXFxM6\
JjScYpbRTxQtdwefWLrk3LmXyJQBWi7c2VAhSxu9msp7VTBycqdwQNgxHETStZuwXsozxaGQ2KssVUCaaoYPR4g2RqK\
UAvtWwA7pMiAQNcbkXcbsjCgVjWaCpMWC37XA31cLcFf3zbjHD9e5tXjAcqa4M89fbFhuvvSXxowSAZ5NoWrN32kd5d\
wxJm1JW3Tt2h6yDDBe84oMy71462dZn7N78DVk2mFNGwBCibrZWA7oUzRBMfYxiQrksoFcou7QfLLd58zoNYmPQPt84\
1VpQopEBfdQ7Nf9zoXxBt3zMy7g5NsFGvzh7KTbDUyeeXrdkKJPQBs6dqaizr9sS8CPPmR4uk96vDTRh8CJ5FbSsmb8\
nP71dRvvwRZJHGzwYirMo6SXS3ZYxFuiA3mkxYuqDHCwkTWDuRCcAaztrDYRZg7VCMo4Q446AaEso5eqpeWpHZQt53E\
ZRpqmNYKASGwMhTeEHPSLgSmtoAAUcaRWpGRzYfd6kzEma8tdGLwyP4rLXgvSvtDLP37dU7YgF3LEXbGAz57U9ATy46\
6sroLpHPdaCWB8RF11wvB6Tu196JnJd2KyQBP1iUWP3rtZs3GhAF1QVcxquh8BqDZzAcpQ6wCS1P9c5GxKgww77FVF5\
Kp83XtoxSrw3GaYVyKTGxNh3vcKPR31txCjTxPaN2fg7TaPLhoQJX4YaAroFSXqrqbbRsisuHhhCeUP2YwDjHedes9y")
.unwrap();
let theta = prove_bandwidth_credential(
&params,
&verification_key,
&signature,
&serial_number,
&binding_number,
)
.unwrap();
let credential = OldV1Credential {
n_params: 4,
theta,
voucher_value,
voucher_info,
epoch_id: 42,
};
let serialized_credential = credential.as_bytes();
let deserialized_credential = OldV1Credential::from_bytes(&serialized_credential).unwrap();
assert_eq!(credential, deserialized_credential);
}
#[test]
fn credential_roundtrip() {
// make valid request
let params = bandwidth_credential_params();
let keypair = nym_credentials_interface::keygen(params);
let issuance = IssuanceBandwidthCredential::new_freepass(None);
let sig_req = issuance.prepare_for_signing();
let pub_attrs_hashed = sig_req
.public_attributes_plain
.iter()
.map(hash_to_scalar)
.collect::<Vec<_>>();
let pub_attrs = pub_attrs_hashed.iter().collect::<Vec<_>>();
let blind_sig = blind_sign(
params,
keypair.secret_key(),
&sig_req.blind_sign_request,
&pub_attrs,
)
.unwrap();
let sig = blind_sig
.unblind(
keypair.verification_key(),
&sig_req.pedersen_commitments_openings,
)
.unwrap();
let issued = issuance.into_issued_credential(sig, 42);
let spending = issued
.prepare_for_spending(keypair.verification_key())
.unwrap();
let with_epoch = CredentialSpendingRequest { data: spending };
let bytes = with_epoch.to_bytes();
let recovered = CredentialSpendingRequest::try_from_bytes(&bytes).unwrap();
assert_eq!(with_epoch, recovered);
}
}
+85 -50
View File
@@ -1,12 +1,14 @@
// Copyright 2020 - Nym Technologies SA <contact@nymtech.net>
// Copyright 2020-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::authentication::encrypted_address::EncryptedAddressBytes;
use crate::iv::IV;
use crate::models::{CredentialSpendingRequest, OldV1Credential};
use crate::registration::handshake::SharedKeys;
use crate::{GatewayMacSize, PROTOCOL_VERSION};
use crate::{GatewayMacSize, CURRENT_PROTOCOL_VERSION};
use log::error;
use nym_coconut_interface::Credential;
use nym_credentials::coconut::bandwidth::CredentialSpendingData;
use nym_credentials_interface::{CoconutError, UnknownCredentialType};
use nym_crypto::generic_array::typenum::Unsigned;
use nym_crypto::hmac::recompute_keyed_hmac_and_verify_tag;
use nym_crypto::symmetric::stream_cipher;
@@ -16,10 +18,9 @@ use nym_sphinx::params::packet_sizes::PacketSize;
use nym_sphinx::params::{GatewayEncryptionAlgorithm, GatewayIntegrityHmacAlgorithm};
use nym_sphinx::DestinationAddressBytes;
use serde::{Deserialize, Serialize};
use std::{
convert::{TryFrom, TryInto},
fmt::{self, Error, Formatter},
};
use std::convert::{TryFrom, TryInto};
use std::string::FromUtf8Error;
use thiserror::Error;
use tungstenite::protocol::Message;
#[derive(Serialize, Deserialize, Debug)]
@@ -38,7 +39,7 @@ pub enum RegistrationHandshake {
impl RegistrationHandshake {
pub fn new_payload(data: Vec<u8>) -> Self {
RegistrationHandshake::HandshakePayload {
protocol_version: Some(PROTOCOL_VERSION),
protocol_version: Some(CURRENT_PROTOCOL_VERSION),
data,
}
}
@@ -66,53 +67,58 @@ impl TryInto<String> for RegistrationHandshake {
}
}
#[derive(Debug)]
#[derive(Debug, Error)]
pub enum GatewayRequestsError {
#[error("the request is too short")]
TooShortRequest,
#[error("provided MAC is invalid")]
InvalidMac,
IncorrectlyEncodedAddress,
#[error("address field was incorrectly encoded: {source}")]
IncorrectlyEncodedAddress {
#[from]
source: NymNodeRoutingAddressError,
},
#[error("received request had invalid size. (actual: {0}, but expected one of: {} (ACK), {} (REGULAR), {}, {}, {} (EXTENDED))",
PacketSize::AckPacket.size(),
PacketSize::RegularPacket.size(),
PacketSize::ExtendedPacket8.size(),
PacketSize::ExtendedPacket16.size(),
PacketSize::ExtendedPacket32.size())
]
RequestOfInvalidSize(usize),
#[error("received sphinx packet was malformed")]
MalformedSphinxPacket,
#[error("the received encrypted data was malformed")]
MalformedEncryption,
#[error("provided packet mode is invalid")]
InvalidPacketMode,
InvalidMixPacket(MixPacketFormattingError),
}
impl fmt::Display for GatewayRequestsError {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> {
use GatewayRequestsError::*;
match self {
TooShortRequest => write!(f, "the request is too short"),
InvalidMac => write!(f, "provided MAC is invalid"),
IncorrectlyEncodedAddress => write!(f, "address field was incorrectly encoded"),
RequestOfInvalidSize(actual) =>
write!(
f,
"received request had invalid size. (actual: {}, but expected one of: {} (ACK), {} (REGULAR), {}, {}, {} (EXTENDED))",
actual, PacketSize::AckPacket.size(), PacketSize::RegularPacket.size(),
PacketSize::ExtendedPacket8.size(), PacketSize::ExtendedPacket16.size(),
PacketSize::ExtendedPacket32.size()
),
MalformedSphinxPacket => write!(f, "received sphinx packet was malformed"),
MalformedEncryption => write!(f, "the received encrypted data was malformed"),
InvalidPacketMode => write!(f, "provided packet mode is invalid"),
InvalidMixPacket(err) => write!(f, "provided mix packet was malformed - {err}")
}
}
}
#[error("provided mix packet was malformed: {source}")]
InvalidMixPacket {
#[from]
source: MixPacketFormattingError,
},
impl std::error::Error for GatewayRequestsError {}
#[error("failed to deserialize provided credential: EOF")]
CredentialDeserializationFailureEOF,
impl From<NymNodeRoutingAddressError> for GatewayRequestsError {
fn from(_: NymNodeRoutingAddressError) -> Self {
GatewayRequestsError::IncorrectlyEncodedAddress
}
}
#[error("failed to deserialize provided credential: malformed string: {0}")]
CredentialDeserializationFailureMalformedString(#[from] FromUtf8Error),
impl From<MixPacketFormattingError> for GatewayRequestsError {
fn from(err: MixPacketFormattingError) -> Self {
GatewayRequestsError::InvalidMixPacket(err)
}
#[error("failed to deserialize provided credential: {0}")]
CredentialDeserializationFailureUnknownType(#[from] UnknownCredentialType),
#[error("failed to deserialize provided credential: malformed verify request: {0}")]
CredentialDeserializationFailureMalformedTheta(CoconutError),
#[error("the provided [v1] credential has invalid number of parameters - {0}")]
InvalidNumberOfEmbededParameters(u32),
}
#[derive(Serialize, Deserialize, Debug)]
@@ -137,6 +143,10 @@ pub enum ClientControlRequest {
enc_credential: Vec<u8>,
iv: Vec<u8>,
},
BandwidthCredentialV2 {
enc_credential: Vec<u8>,
iv: Vec<u8>,
},
ClaimFreeTestnetBandwidth,
}
@@ -147,15 +157,15 @@ impl ClientControlRequest {
iv: IV,
) -> Self {
ClientControlRequest::Authenticate {
protocol_version: Some(PROTOCOL_VERSION),
protocol_version: Some(CURRENT_PROTOCOL_VERSION),
address: address.as_base58_string(),
enc_address: enc_address.to_base58_string(),
iv: iv.to_base58_string(),
}
}
pub fn new_enc_coconut_bandwidth_credential(
credential: &Credential,
pub fn new_enc_coconut_bandwidth_credential_v1(
credential: &OldV1Credential,
shared_key: &SharedKeys,
iv: IV,
) -> Self {
@@ -168,13 +178,38 @@ impl ClientControlRequest {
}
}
pub fn try_from_enc_coconut_bandwidth_credential(
pub fn try_from_enc_coconut_bandwidth_credential_v1(
enc_credential: Vec<u8>,
shared_key: &SharedKeys,
iv: IV,
) -> Result<Credential, GatewayRequestsError> {
) -> Result<OldV1Credential, GatewayRequestsError> {
let credential_bytes = shared_key.decrypt_tagged(&enc_credential, Some(iv.inner()))?;
Credential::from_bytes(&credential_bytes)
OldV1Credential::from_bytes(&credential_bytes)
.map_err(|_| GatewayRequestsError::MalformedEncryption)
}
pub fn new_enc_coconut_bandwidth_credential_v2(
credential: CredentialSpendingData,
shared_key: &SharedKeys,
iv: IV,
) -> Self {
let cred = CredentialSpendingRequest::new(credential);
let serialized_credential = cred.to_bytes();
let enc_credential = shared_key.encrypt_and_tag(&serialized_credential, Some(iv.inner()));
ClientControlRequest::BandwidthCredentialV2 {
enc_credential,
iv: iv.to_bytes(),
}
}
pub fn try_from_enc_coconut_bandwidth_credential_v2(
enc_credential: Vec<u8>,
shared_key: &SharedKeys,
iv: IV,
) -> Result<CredentialSpendingRequest, GatewayRequestsError> {
let credential_bytes = shared_key.decrypt_tagged(&enc_credential, Some(iv.inner()))?;
CredentialSpendingRequest::try_from_bytes(&credential_bytes)
.map_err(|_| GatewayRequestsError::MalformedEncryption)
}
}
@@ -0,0 +1,11 @@
/*
* Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
* SPDX-License-Identifier: Apache-2.0
*/
CREATE TABLE spent_credential
(
blinded_serial_number_bs58 TEXT NOT NULL PRIMARY KEY UNIQUE,
was_freepass BOOLEAN NOT NULL,
client_address_bs58 TEXT NOT NULL REFERENCES shared_keys (client_address_bs58)
);
+80 -12
View File
@@ -1,24 +1,92 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// Copyright 2021-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use nym_coconut_interface::Credential;
use log::{error, warn};
use nym_credentials::coconut::bandwidth::CredentialType;
use std::num::ParseIntError;
use thiserror::Error;
use time::error::ComponentRange;
use time::OffsetDateTime;
#[derive(Debug, Error)]
pub enum BandwidthError {
#[error("Provided bandwidth credential asks for more bandwidth than it is supported to add at once (credential value: {0}, supported: {}). Try to split it before attempting again", i64::MAX)]
UnsupportedBandwidthValue(u64),
#[error("the provided free pass has already expired (expiry was on {expiry_date})")]
ExpiredFreePass { expiry_date: OffsetDateTime },
#[error("failed to parse the bandwidth voucher value: {source}")]
VoucherValueParsingFailure {
#[source]
source: ParseIntError,
},
#[error("failed to parse the free pass expiry date: {source}")]
ExpiryDateParsingFailure {
#[source]
source: ParseIntError,
},
#[error("failed to parse expiry timestamp into proper datetime: {source}")]
InvalidExpiryDate {
unix_timestamp: i64,
#[source]
source: ComponentRange,
},
}
pub struct Bandwidth {
value: u64,
}
impl Bandwidth {
pub const fn new(value: u64) -> Bandwidth {
Bandwidth { value }
}
pub fn try_from_raw_value(value: &str, typ: CredentialType) -> Result<Self, BandwidthError> {
let bandwidth_value =
match typ {
CredentialType::Voucher => {
let token_value: u64 = value
.parse()
.map_err(|source| BandwidthError::VoucherValueParsingFailure { source })?;
token_value * nym_network_defaults::BYTES_PER_UTOKEN
}
CredentialType::FreePass => {
let expiry_timestamp: i64 = value
.parse()
.map_err(|source| BandwidthError::ExpiryDateParsingFailure { source })?;
let expiry_date = OffsetDateTime::from_unix_timestamp(expiry_timestamp)
.map_err(|source| BandwidthError::InvalidExpiryDate {
unix_timestamp: expiry_timestamp,
source,
})?;
let now = OffsetDateTime::now_utc();
if expiry_date < now {
return Err(BandwidthError::ExpiredFreePass { expiry_date });
}
nym_network_defaults::BYTES_PER_FREEPASS
}
};
if bandwidth_value > i64::MAX as u64 {
// note that this would have represented more than 1 exabyte,
// which is like 125,000 worth of hard drives, so I don't think we have
// to worry about it for now...
warn!("Somehow we received bandwidth value higher than 9223372036854775807. We don't really want to deal with this now");
return Err(BandwidthError::UnsupportedBandwidthValue(bandwidth_value));
}
Ok(Bandwidth {
value: bandwidth_value,
})
}
pub fn value(&self) -> u64 {
self.value
}
}
impl From<Credential> for Bandwidth {
fn from(credential: Credential) -> Self {
let token_value = credential.voucher_value();
let bandwidth_bytes = token_value * nym_network_defaults::BYTES_PER_UTOKEN;
Bandwidth {
value: bandwidth_bytes,
}
}
}
+4 -2
View File
@@ -1,9 +1,11 @@
// Copyright 2020 - Nym Technologies SA <contact@nymtech.net>
// Copyright 2020-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::node::client_handling::bandwidth::Bandwidth;
pub(crate) mod active_clients;
mod bandwidth;
pub(crate) mod embedded_network_requester;
pub(crate) mod websocket;
pub(crate) const FREE_TESTNET_BANDWIDTH_VALUE: i64 = 64 * 1024 * 1024 * 1024; // 64GB
pub(crate) const FREE_TESTNET_BANDWIDTH_VALUE: Bandwidth = Bandwidth::new(64 * 1024 * 1024 * 1024); // 64GB
@@ -1,26 +1,7 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use futures::{
future::{FusedFuture, OptionFuture},
FutureExt, StreamExt,
};
use log::*;
use nym_gateway_requests::{
iv::{IVConversionError, IV},
types::{BinaryRequest, ServerResponse},
ClientControlRequest, GatewayRequestsError,
};
use nym_sphinx::forwarding::packet::MixPacket;
use nym_task::TaskClient;
use nym_validator_client::coconut::CoconutApiError;
use rand::{CryptoRng, Rng};
use thiserror::Error;
use tokio::io::{AsyncRead, AsyncWrite};
use tokio_tungstenite::tungstenite::{protocol::Message, Error as WsError};
use std::{convert::TryFrom, process, time::Duration};
use crate::node::client_handling::bandwidth::BandwidthError;
use crate::node::{
client_handling::{
bandwidth::Bandwidth,
@@ -34,6 +15,27 @@ use crate::node::{
},
storage::{error::StorageError, Storage},
};
use futures::{
future::{FusedFuture, OptionFuture},
FutureExt, StreamExt,
};
use log::*;
use nym_credentials::coconut::bandwidth::{bandwidth_credential_params, CredentialType};
use nym_credentials_interface::CoconutError;
use nym_gateway_requests::models::CredentialSpendingRequest;
use nym_gateway_requests::{
iv::{IVConversionError, IV},
types::{BinaryRequest, ServerResponse},
ClientControlRequest, GatewayRequestsError,
};
use nym_sphinx::forwarding::packet::MixPacket;
use nym_task::TaskClient;
use nym_validator_client::coconut::CoconutApiError;
use rand::{CryptoRng, Rng};
use std::{convert::TryFrom, process, time::Duration};
use thiserror::Error;
use tokio::io::{AsyncRead, AsyncWrite};
use tokio_tungstenite::tungstenite::{protocol::Message, Error as WsError};
#[derive(Debug, Error)]
pub(crate) enum RequestHandlingError {
@@ -52,12 +54,12 @@ pub(crate) enum RequestHandlingError {
#[error("The received request is not valid in the current context")]
IllegalRequest,
#[error("Provided bandwidth credential asks for more bandwidth than it is supported to add at once (credential value: {0}, supported: {}). Try to split it before attempting again", i64::MAX)]
UnsupportedBandwidthValue(u64),
#[error("Provided bandwidth credential did not verify correctly on {0}")]
InvalidBandwidthCredential(String),
#[error("the provided bandwidth credential has already been spent before at this gateway")]
BandwidthCredentialAlreadySpent,
#[error("This gateway is only accepting coconut credentials for bandwidth")]
OnlyCoconutCredentials,
@@ -73,14 +75,23 @@ pub(crate) enum RequestHandlingError {
#[error("There was a problem with the proposal id: {reason}")]
ProposalIdError { reason: String },
#[error("Coconut interface error - {0}")]
CoconutInterfaceError(#[from] nym_coconut_interface::error::CoconutInterfaceError),
#[error("coconut failure: {0}")]
CoconutError(#[from] CoconutError),
#[error("coconut api query failure: {0}")]
CoconutApiError(#[from] CoconutApiError),
#[error("Credential error - {0}")]
CredentialError(#[from] nym_credentials::error::Error),
#[error("failed to recover bandwidth value: {0}")]
BandwidthRecoveryFailure(#[from] BandwidthError),
#[error("the provided credential did not contain a valid type attribute")]
InvalidTypeAttribute,
#[error("the provided credential did not have a bandwidth attribute")]
MissingBandwidthAttribute,
}
impl RequestHandlingError {
@@ -177,10 +188,10 @@ where
/// # Arguments
///
/// * `amount`: amount to increase the available bandwidth by.
async fn increase_bandwidth(&self, amount: i64) -> Result<(), RequestHandlingError> {
async fn increase_bandwidth(&self, bandwidth: Bandwidth) -> Result<(), RequestHandlingError> {
self.inner
.storage
.increase_bandwidth(self.client.address, amount)
.increase_bandwidth(self.client.address, bandwidth.value() as i64)
.await?;
Ok(())
}
@@ -210,6 +221,102 @@ where
}
}
async fn handle_bandwidth_request(
&mut self,
credential: CredentialSpendingRequest,
) -> Result<ServerResponse, RequestHandlingError> {
// check if the credential hasn't been spent before
let serial_number = credential.data.blinded_serial_number();
let already_spent = self
.inner
.storage
.contains_credential(&serial_number)
.await?;
if already_spent {
return Err(RequestHandlingError::BandwidthCredentialAlreadySpent);
}
let aggregated_verification_key = self
.inner
.coconut_verifier
.verification_key(credential.data.epoch_id)
.await?;
if !credential.data.validate_type_attribute() {
return Err(RequestHandlingError::InvalidTypeAttribute);
}
let Some(bandwidth_attribute) = credential.data.get_bandwidth_attribute() else {
return Err(RequestHandlingError::MissingBandwidthAttribute);
};
// this will extract token amounts out of bandwidth vouchers and validate expiry of free passes
let bandwidth = Bandwidth::try_from_raw_value(bandwidth_attribute, credential.data.typ)?;
// locally verify the credential
let params = bandwidth_credential_params();
if !credential.data.verify(params, &aggregated_verification_key) {
return Err(RequestHandlingError::InvalidBandwidthCredential(
String::from("credential failed to verify on gateway"),
));
}
let was_freepass = match credential.data.typ {
CredentialType::Voucher => {
let api_clients = self
.inner
.coconut_verifier
.api_clients(credential.data.epoch_id)
.await?;
self.inner
.coconut_verifier
.release_bandwidth_voucher_funds(&api_clients, credential)
.await?;
false
}
CredentialType::FreePass => {
// no need to do anything special here, we already extracted the bandwidth amount and checked expiry
info!("received a free pass credential");
true
}
};
// technically this is not atomic, i.e. checking for the spending and then marking as spent,
// but because we have the `UNIQUE` constraint on the database table
// if somebody attempts to spend the same credential in another, parallel request,
// one of them will fail
//
// mark the credential as spent
// TODO: technically this should be done under a storage transaction so that if we experience any
// failures later on, it'd get reverted
self.inner
.storage
.insert_spent_credential(serial_number, was_freepass, self.client.address)
.await?;
self.increase_bandwidth(bandwidth).await?;
let available_total = self.get_available_bandwidth().await?;
Ok(ServerResponse::Bandwidth { available_total })
}
async fn handle_bandwidth_v1(
&mut self,
enc_credential: Vec<u8>,
iv: Vec<u8>,
) -> Result<ServerResponse, RequestHandlingError> {
let iv = IV::try_from_bytes(&iv)?;
let credential = ClientControlRequest::try_from_enc_coconut_bandwidth_credential_v1(
enc_credential,
&self.client.shared_keys,
iv,
)?;
self.handle_bandwidth_request(credential.try_into()?).await
}
/// Tries to handle the received bandwidth request by checking correctness of the received data
/// and if successful, increases client's bandwidth by an appropriate amount.
///
@@ -217,58 +324,19 @@ where
///
/// * `enc_credential`: raw encrypted bandwidth credential to verify.
/// * `iv`: fresh iv used for the credential.
async fn handle_bandwidth(
async fn handle_bandwidth_v2(
&mut self,
enc_credential: Vec<u8>,
iv: Vec<u8>,
) -> Result<ServerResponse, RequestHandlingError> {
let iv = IV::try_from_bytes(&iv)?;
let credential = ClientControlRequest::try_from_enc_coconut_bandwidth_credential(
let credential = ClientControlRequest::try_from_enc_coconut_bandwidth_credential_v2(
enc_credential,
&self.client.shared_keys,
iv,
)?;
let aggregated_verification_key = self
.inner
.coconut_verifier
.verification_key(*credential.epoch_id())
.await?;
if !credential.verify(&aggregated_verification_key) {
return Err(RequestHandlingError::InvalidBandwidthCredential(
String::from("credential failed to verify on gateway"),
));
}
let api_clients = self
.inner
.coconut_verifier
.api_clients(*credential.epoch_id())
.await?;
self.inner
.coconut_verifier
.release_funds(&api_clients, &credential)
.await?;
let bandwidth = Bandwidth::from(credential);
let bandwidth_value = bandwidth.value();
if bandwidth_value > i64::MAX as u64 {
// note that this would have represented more than 1 exabyte,
// which is like 125,000 worth of hard drives so I don't think we have
// to worry about it for now...
warn!("Somehow we received bandwidth value higher than 9223372036854775807. We don't really want to deal with this now");
return Err(RequestHandlingError::UnsupportedBandwidthValue(
bandwidth_value,
));
}
self.increase_bandwidth(bandwidth_value as i64).await?;
let available_total = self.get_available_bandwidth().await?;
Ok(ServerResponse::Bandwidth { available_total })
self.handle_bandwidth_request(credential).await
}
async fn handle_claim_testnet_bandwidth(
@@ -349,7 +417,11 @@ where
Err(e) => RequestHandlingError::InvalidTextRequest(e).into_error_message(),
Ok(request) => match request {
ClientControlRequest::BandwidthCredential { enc_credential, iv } => self
.handle_bandwidth(enc_credential, iv)
.handle_bandwidth_v1(enc_credential, iv)
.await
.into_ws_message(),
ClientControlRequest::BandwidthCredentialV2 { enc_credential, iv } => self
.handle_bandwidth_v2(enc_credential, iv)
.await
.into_ws_message(),
ClientControlRequest::ClaimFreeTestnetBandwidth => self
@@ -3,7 +3,8 @@
use super::authenticated::RequestHandlingError;
use log::*;
use nym_coconut_interface::{Credential, VerificationKey};
use nym_credentials_interface::VerificationKey;
use nym_gateway_requests::models::CredentialSpendingRequest;
use nym_validator_client::coconut::all_coconut_api_clients;
use nym_validator_client::nym_api::EpochId;
use nym_validator_client::nyxd::contract_traits::MultisigQueryClient;
@@ -63,7 +64,7 @@ impl CoconutVerifier {
});
}
let aggregated_verification_key =
nym_credentials::obtain_aggregate_verification_key(&epoch_api_clients).await?;
nym_credentials::obtain_aggregate_verification_key(&epoch_api_clients)?;
api_clients.insert(current_epoch.epoch_id, epoch_api_clients);
master_keys.insert(current_epoch.epoch_id, aggregated_verification_key);
@@ -129,7 +130,7 @@ impl CoconutVerifier {
let api_clients = self.api_clients(epoch_id).await?;
let aggregated_verification_key =
nym_credentials::obtain_aggregate_verification_key(&api_clients).await?;
nym_credentials::obtain_aggregate_verification_key(&api_clients)?;
let mut guard = self.master_keys.write().await;
guard.insert(epoch_id, aggregated_verification_key);
@@ -151,21 +152,31 @@ impl CoconutVerifier {
Ok(all_coconut_api_clients(self.nyxd_client.read().await.deref(), epoch_id).await?)
}
pub async fn release_funds(
pub async fn release_bandwidth_voucher_funds(
&self,
api_clients: &[CoconutApiClient],
credential: &Credential,
credential: CredentialSpendingRequest,
) -> Result<(), RequestHandlingError> {
if !credential.data.typ.is_voucher() {
unimplemented!()
}
// safety: the voucher funds are released after the credential has already been verified locally
// and the underlying bandwidth value has been extracted, so the below MUST succeed
let voucher_amount = credential.unchecked_voucher_value() as u128;
let blinded_serial_number = credential
.data
.verify_credential_request
.blinded_serial_number_bs58();
let res = self
.nyxd_client
.write()
.await
.spend_credential(
Coin::new(
credential.voucher_value().into(),
self.mix_denom_base.clone(),
),
credential.blinded_serial_number(),
Coin::new(voucher_amount, &self.mix_denom_base),
blinded_serial_number,
self.address.to_string(),
None,
)
@@ -186,27 +197,28 @@ impl CoconutVerifier {
.await
.query_proposal(proposal_id)
.await?;
if !credential.has_blinded_serial_number(&proposal.description)? {
if !credential.matches_blinded_serial_number(&proposal.description)? {
return Err(RequestHandlingError::ProposalIdError {
reason: String::from("proposal has different serial number"),
});
}
let req = nym_api_requests::coconut::VerifyCredentialBody::new(
credential.clone(),
credential.data,
proposal_id,
self.address.clone(),
);
for client in api_clients {
let ret = client.api_client.verify_bandwidth_credential(&req).await;
let client_url = client.api_client.nym_api.current_url();
match ret {
Ok(res) => {
if !res.verification_result {
debug!("Validator {} didn't accept the credential. It will probably vote No on the spending proposal", client.api_client.nym_api.current_url());
warn!("Validator at {client_url} didn't accept the credential. It will probably vote No on the spending proposal");
}
}
Err(e) => {
warn!("Validator {} could not be reached. There might be a problem with the coconut endpoint - {:?}", client.api_client.nym_api.current_url(), e);
Err(err) => {
warn!("Validator at {client_url} could not be reached. There might be a problem with the coconut endpoint: {err}");
}
}
}
@@ -15,7 +15,7 @@ use nym_gateway_requests::{
iv::{IVConversionError, IV},
registration::handshake::{error::HandshakeError, gateway_handshake, SharedKeys},
types::{ClientControlRequest, ServerResponse},
BinaryResponse, PROTOCOL_VERSION,
BinaryResponse, CURRENT_PROTOCOL_VERSION, INITIAL_PROTOCOL_VERSION,
};
use nym_mixnet_client::forwarder::MixForwardingSender;
use nym_sphinx::DestinationAddressBytes;
@@ -95,6 +95,9 @@ pub(crate) struct FreshHandler<R, S, St> {
pub(crate) socket_connection: SocketStream<S>,
pub(crate) storage: St,
pub(crate) coconut_verifier: Arc<CoconutVerifier>,
// currently unused (but populated)
pub(crate) negotiated_protocol: Option<u8>,
}
impl<R, S, St> FreshHandler<R, S, St>
@@ -126,6 +129,7 @@ where
local_identity,
storage,
coconut_verifier,
negotiated_protocol: None,
}
}
@@ -310,7 +314,7 @@ where
/// Checks whether the stored shared keys match the received data, i.e. whether the upon decryption
/// the provided encrypted address matches the expected unencrypted address.
///
/// Returns the the retrieved shared keys if the check was successful.
/// Returns the retrieved shared keys if the check was successful.
///
/// # Arguments
///
@@ -355,30 +359,33 @@ where
}
}
fn check_client_protocol(
fn negotiate_client_protocol(
&self,
client_protocol: Option<u8>,
) -> Result<(), InitialAuthenticationError> {
// right now there are no failure cases here, but this might change in the future
match client_protocol {
None => {
warn!("the client we're connected to has not specified its protocol version. It's probably running version < 1.1.X, but that's still fine for now. It will become a hard error in 1.2.0");
// note: in +1.2.0 we will have to return a hard error here
Ok(())
}
Some(v) if v != PROTOCOL_VERSION => {
let err = InitialAuthenticationError::IncompatibleProtocol {
client: Some(v),
current: PROTOCOL_VERSION,
};
error!("{err}");
Err(err)
}
) -> Result<u8, InitialAuthenticationError> {
let Some(client_protocol_version) = client_protocol else {
warn!("the client we're connected to has not specified its protocol version. It's probably running version < 1.1.X, but that's still fine for now. It will become a hard error in 1.2.0");
// note: in +1.2.0 we will have to return a hard error here
return Ok(INITIAL_PROTOCOL_VERSION);
};
Some(_) => {
info!("the client is using exactly the same protocol version as we are. We're good to continue!");
Ok(())
}
// a v2 gateway will understand v1 requests, but v1 client will not understand v2 responses
if client_protocol_version == 1 {
return Ok(1);
}
// we can't handle clients with higher protocol than ours
// (perhaps we could try to negotiate downgrade on our end? sounds like a nice future improvement)
if client_protocol_version <= CURRENT_PROTOCOL_VERSION {
info!("the client is using exactly the same (or older) protocol version as we are. We're good to continue!");
Ok(CURRENT_PROTOCOL_VERSION)
} else {
let err = InitialAuthenticationError::IncompatibleProtocol {
client: client_protocol,
current: CURRENT_PROTOCOL_VERSION,
};
error!("{err}");
Err(err)
}
}
@@ -490,7 +497,9 @@ where
where
S: AsyncRead + AsyncWrite + Unpin,
{
self.check_client_protocol(client_protocol_version)?;
let negotiated_protocol = self.negotiate_client_protocol(client_protocol_version)?;
// populate the negotiated protocol for future uses
self.negotiated_protocol = Some(negotiated_protocol);
let address = DestinationAddressBytes::try_from_base58_string(address)
.map_err(|err| InitialAuthenticationError::MalformedClientAddress(err.to_string()))?;
@@ -519,7 +528,7 @@ where
Ok(InitialAuthResult::new(
client_details,
ServerResponse::Authenticate {
protocol_version: Some(PROTOCOL_VERSION),
protocol_version: Some(negotiated_protocol),
status,
bandwidth_remaining,
},
@@ -580,7 +589,9 @@ where
where
S: AsyncRead + AsyncWrite + Unpin + Send,
{
self.check_client_protocol(client_protocol_version)?;
let negotiated_protocol = self.negotiate_client_protocol(client_protocol_version)?;
// populate the negotiated protocol for future uses
self.negotiated_protocol = Some(negotiated_protocol);
let remote_identity = Self::extract_remote_identity_from_register_init(&init_data)?;
let remote_address = remote_identity.derive_destination_address();
@@ -597,7 +608,7 @@ where
Ok(InitialAuthResult::new(
Some(client_details),
ServerResponse::Register {
protocol_version: Some(PROTOCOL_VERSION),
protocol_version: Some(negotiated_protocol),
status,
},
))
+51 -1
View File
@@ -1,7 +1,7 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::node::storage::models::PersistedBandwidth;
use crate::node::storage::models::{PersistedBandwidth, SpentCredential};
#[derive(Clone)]
pub(crate) struct BandwidthManager {
@@ -103,4 +103,54 @@ impl BandwidthManager {
.await?;
Ok(())
}
/// Mark received credential as spent and insert it into the storage.
///
/// # Arguments
///
/// * `blinded_serial_number_bs58`: the unique blinded serial number embedded in the credential
/// * `was_freepass`: indicates whether the spent credential was a freepass
/// * `client_address_bs58`: address of the client that spent the credential
pub(crate) async fn insert_spent_credential(
&self,
blinded_serial_number_bs58: &str,
was_freepass: bool,
client_address_bs58: &str,
) -> Result<(), sqlx::Error> {
sqlx::query!(
r#"
INSERT INTO spent_credential
(blinded_serial_number_bs58, was_freepass, client_address_bs58)
VALUES (?, ?, ?)
"#,
blinded_serial_number_bs58,
was_freepass,
client_address_bs58
)
.execute(&self.connection_pool)
.await?;
Ok(())
}
/// Retrieve the spent credential with the provided blinded serial number from the storage.
///
/// # Arguments
///
/// * `blinded_serial_number_bs58`: the unique blinded serial number embedded in the credential
pub(crate) async fn retrieve_spent_credential(
&self,
blinded_serial_number_bs58: &str,
) -> Result<Option<SpentCredential>, sqlx::Error> {
sqlx::query_as!(
SpentCredential,
r#"
SELECT * FROM spent_credential
WHERE blinded_serial_number_bs58 = ?
LIMIT 1
"#,
blinded_serial_number_bs58,
)
.fetch_optional(&self.connection_pool)
.await
}
}
+68
View File
@@ -8,6 +8,7 @@ use crate::node::storage::models::{PersistedSharedKeys, StoredMessage};
use crate::node::storage::shared_keys::SharedKeysManager;
use async_trait::async_trait;
use log::{debug, error};
use nym_credentials_interface::{Base58, BlindedSerialNumber};
use nym_gateway_requests::registration::handshake::SharedKeys;
use nym_sphinx::DestinationAddressBytes;
use sqlx::ConnectOptions;
@@ -134,6 +135,29 @@ pub(crate) trait Storage: Send + Sync {
client_address: DestinationAddressBytes,
amount: i64,
) -> Result<(), StorageError>;
/// Mark received credential as spent and insert it into the storage.
///
/// # Arguments
///
/// * `blinded_serial_number`: the unique blinded serial number embedded in the credential
/// * `client_address`: address of the client that spent the credential
async fn insert_spent_credential(
&self,
blinded_serial_number: BlindedSerialNumber,
was_freepass: bool,
client_address: DestinationAddressBytes,
) -> Result<(), StorageError>;
/// Check if the credential with the provided blinded serial number if already present in the storage.
///
/// # Arguments
///
/// * `blinded_serial_number`: the unique blinded serial number embedded in the credential
async fn contains_credential(
&self,
blinded_serial_number: &BlindedSerialNumber,
) -> Result<bool, StorageError>;
}
// note that clone here is fine as upon cloning the same underlying pool will be used
@@ -304,6 +328,34 @@ impl Storage for PersistentStorage {
.await?;
Ok(())
}
async fn insert_spent_credential(
&self,
blinded_serial_number: BlindedSerialNumber,
was_freepass: bool,
client_address: DestinationAddressBytes,
) -> Result<(), StorageError> {
self.bandwidth_manager
.insert_spent_credential(
&blinded_serial_number.to_bs58(),
was_freepass,
&client_address.as_base58_string(),
)
.await?;
Ok(())
}
async fn contains_credential(
&self,
blinded_serial_number: &BlindedSerialNumber,
) -> Result<bool, StorageError> {
let cred = self
.bandwidth_manager
.retrieve_spent_credential(&blinded_serial_number.to_bs58())
.await?;
Ok(cred.is_some())
}
}
/// In-memory implementation of `Storage`. The intention is primarily in testing environments.
@@ -393,4 +445,20 @@ impl Storage for InMemStorage {
) -> Result<(), StorageError> {
todo!()
}
async fn insert_spent_credential(
&self,
_blinded_serial_number: BlindedSerialNumber,
_was_freepass: bool,
_client_address: DestinationAddressBytes,
) -> Result<(), StorageError> {
todo!()
}
async fn contains_credential(
&self,
_blinded_serial_number: &BlindedSerialNumber,
) -> Result<bool, StorageError> {
todo!()
}
}
+13 -1
View File
@@ -1,6 +1,8 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// Copyright 2021-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use sqlx::FromRow;
pub(crate) struct PersistedSharedKeys {
pub(crate) client_address_bs58: String,
pub(crate) derived_aes128_ctr_blake3_hmac_keys_bs58: String,
@@ -18,3 +20,13 @@ pub(crate) struct PersistedBandwidth {
pub(crate) client_address_bs58: String,
pub(crate) available: i64,
}
#[derive(Debug, Clone, FromRow)]
pub(crate) struct SpentCredential {
#[allow(dead_code)]
pub(crate) blinded_serial_number_bs58: String,
#[allow(dead_code)]
pub(crate) was_freepass: bool,
#[allow(dead_code)]
pub(crate) client_address_bs58: String,
}
+1 -1
View File
@@ -26,6 +26,7 @@ dirs = "4.0"
futures = { workspace = true }
itertools = "0.12.0"
humantime-serde = "1.0"
k256 = { version = "*", features = ["ecdsa-core"] } # needed for the Verifier trait; pull whatever version is used by other dependencies
lazy_static = "1.4.0"
log = { workspace = true }
pin-project = "1.0"
@@ -79,7 +80,6 @@ ephemera = { path = "../ephemera" }
nym-bandwidth-controller = { path = "../common/bandwidth-controller" }
nym-coconut-bandwidth-contract-common = { path = "../common/cosmwasm-smart-contracts/coconut-bandwidth-contract" }
nym-coconut-dkg-common = { path = "../common/cosmwasm-smart-contracts/coconut-dkg" }
nym-coconut-interface = { path = "../common/coconut-interface" }
nym-ephemera-common = { path = "../common/cosmwasm-smart-contracts/ephemera" }
nym-config = { path = "../common/config" }
cosmwasm-std = { workspace = true }
@@ -0,0 +1,12 @@
/*
* Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
* SPDX-License-Identifier: Apache-2.0
*/
CREATE TABLE issued_freepass
(
id INTEGER PRIMARY KEY CHECK (id = 0),
current_nonce INTEGER NOT NULL
);
INSERT INTO issued_freepass(id, current_nonce) VALUES (0,0);
+5 -2
View File
@@ -16,10 +16,13 @@ serde = { workspace = true, features = ["derive"] }
ts-rs = { workspace = true, optional = true }
tendermint = { workspace = true }
nym-coconut-interface = { path = "../../common/coconut-interface" }
# for serde on secp256k1 signatures
ecdsa = { version = "0.16", features = ["serde"] }
nym-credentials-interface = { path = "../../common/credentials-interface" }
nym-crypto = { path = "../../common/crypto", features = ["serde", "asymmetric"]}
nym-mixnet-contract-common = { path= "../../common/cosmwasm-smart-contracts/mixnet-contract" }
nym-mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract" }
nym-node-requests = { path = "../../nym-node/nym-node-requests", default-features = false }
[features]
@@ -1,7 +1,7 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_coconut_interface::BlindedSignature;
use nym_credentials_interface::BlindedSignature;
use tendermint::hash::Hash;
// recomputes plaintext on the credential nym-api has used for signing
+1 -1
View File
@@ -5,6 +5,6 @@ pub mod helpers;
pub mod models;
pub use models::{
BlindSignRequestBody, BlindedSignatureResponse, CredentialsRequestBody,
BlindSignRequestBody, BlindedSignatureResponse, CredentialsRequestBody, FreePassRequest,
VerificationKeyResponse, VerifyCredentialBody, VerifyCredentialResponse,
};
+69 -11
View File
@@ -1,11 +1,11 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// Copyright 2023-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::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_credentials_interface::{
hash_to_scalar, Attribute, BlindSignRequest, BlindedSignature, Bytable, CoconutError,
CredentialSpendingData, VerificationKey,
};
use nym_crypto::asymmetric::identity;
use serde::{Deserialize, Serialize};
@@ -14,21 +14,24 @@ use tendermint::hash::Hash;
#[derive(Serialize, Deserialize)]
pub struct VerifyCredentialBody {
pub credential: Credential,
/// The cryptographic material required for spending the underlying credential.
pub credential_data: CredentialSpendingData,
/// Multisig proposal for releasing funds for the provided bandwidth credential
pub proposal_id: u64,
/// Cosmos address of the spender of the credential
pub gateway_cosmos_addr: AccountId,
}
impl VerifyCredentialBody {
pub fn new(
credential: Credential,
credential_data: CredentialSpendingData,
proposal_id: u64,
gateway_cosmos_addr: AccountId,
) -> VerifyCredentialBody {
VerifyCredentialBody {
credential,
credential_data,
proposal_id,
gateway_cosmos_addr,
}
@@ -59,7 +62,6 @@ pub struct BlindSignRequestBody {
/// Signature on the inner sign request and the tx hash
pub signature: identity::Signature,
// public_attributes: Vec<String>,
pub public_attributes_plain: Vec<String>,
}
@@ -91,7 +93,7 @@ impl BlindSignRequestBody {
}
pub fn encode_commitments(&self) -> Vec<String> {
use nym_coconut_interface::Base58;
use nym_credentials_interface::Base58;
self.inner_sign_request
.get_private_attributes_pedersen_commitments()
@@ -101,6 +103,11 @@ impl BlindSignRequestBody {
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct FreePassNonceResponse {
pub current_nonce: u32,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct BlindedSignatureResponse {
pub blinded_signature: BlindedSignature,
@@ -115,7 +122,7 @@ impl BlindedSignatureResponse {
bs58::encode(&self.to_bytes()).into_string()
}
pub fn from_base58_string<I: AsRef<[u8]>>(val: I) -> Result<Self, CoconutInterfaceError> {
pub fn from_base58_string<I: AsRef<[u8]>>(val: I) -> Result<Self, CoconutError> {
let bytes = bs58::decode(val).into_vec()?;
Self::from_bytes(&bytes)
}
@@ -124,13 +131,64 @@ impl BlindedSignatureResponse {
self.blinded_signature.to_byte_vec()
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, CoconutInterfaceError> {
pub fn from_bytes(bytes: &[u8]) -> Result<Self, CoconutError> {
Ok(BlindedSignatureResponse {
blinded_signature: BlindedSignature::from_bytes(bytes)?,
})
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct FreePassRequest {
// secp256k1 key associated with the admin account
pub cosmos_pubkey: cosmrs::crypto::PublicKey,
pub inner_sign_request: BlindSignRequest,
// we need to include a nonce here to prevent replay attacks
// (and not making the nym-api store the serial numbers of all issued credential)
pub used_nonce: u32,
/// Signature on the nonce
/// to prove the possession of the cosmos key/address
pub nonce_signature: cosmrs::crypto::secp256k1::Signature,
pub public_attributes_plain: Vec<String>,
}
impl FreePassRequest {
pub fn new(
cosmos_pubkey: cosmrs::crypto::PublicKey,
inner_sign_request: BlindSignRequest,
used_nonce: u32,
nonce_signature: cosmrs::crypto::secp256k1::Signature,
public_attributes_plain: Vec<String>,
) -> Self {
FreePassRequest {
cosmos_pubkey,
inner_sign_request,
used_nonce,
nonce_signature,
public_attributes_plain,
}
}
pub fn tendermint_pubkey(&self) -> tendermint::PublicKey {
self.cosmos_pubkey.into()
}
pub fn nonce_plaintext(&self) -> [u8; 4] {
self.used_nonce.to_be_bytes()
}
pub fn public_attributes_hashed(&self) -> Vec<Attribute> {
self.public_attributes_plain
.iter()
.map(hash_to_scalar)
.collect()
}
}
#[derive(Serialize, Deserialize)]
pub struct VerificationKeyResponse {
pub key: VerificationKey,
+175 -20
View File
@@ -1,4 +1,4 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// Copyright 2023-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::coconut::api_routes::helpers::build_credentials_response;
@@ -6,9 +6,10 @@ 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 k256::ecdsa::signature::Verifier;
use nym_api_requests::coconut::models::{
CredentialsRequestBody, EpochCredentialsResponse, IssuedCredentialResponse,
IssuedCredentialsResponse,
CredentialsRequestBody, EpochCredentialsResponse, FreePassNonceResponse, FreePassRequest,
IssuedCredentialResponse, IssuedCredentialsResponse,
};
use nym_api_requests::coconut::{
BlindSignRequestBody, BlindedSignatureResponse, VerifyCredentialBody, VerifyCredentialResponse,
@@ -17,13 +18,155 @@ 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_credentials::coconut::bandwidth::freepass::MAX_FREE_PASS_VALIDITY;
use nym_credentials::coconut::bandwidth::{
bandwidth_credential_params, CredentialType, IssuanceBandwidthCredential,
};
use nym_validator_client::nyxd::Coin;
use rocket::serde::json::Json;
use rocket::State as RocketState;
use std::ops::Deref;
use time::OffsetDateTime;
mod helpers;
fn validate_freepass_public_attributes(res: &FreePassRequest) -> Result<()> {
let public_attributes = &res.public_attributes_plain;
if public_attributes.len() != IssuanceBandwidthCredential::PUBLIC_ATTRIBUTES as usize {
return Err(CoconutError::InvalidFreePassAttributes {
got: public_attributes.len(),
expected: IssuanceBandwidthCredential::PUBLIC_ATTRIBUTES as usize,
});
}
// SAFETY: we just ensured correct number of attributes
let expiry_raw = public_attributes.first().unwrap();
let type_raw = public_attributes.get(1).unwrap();
let parsed_type = type_raw.parse::<CredentialType>()?;
if parsed_type != CredentialType::FreePass {
return Err(CoconutError::InvalidFreePassTypeAttribute { got: parsed_type });
}
let expiry_timestamp: i64 = expiry_raw
.parse()
.map_err(|source| CoconutError::ExpiryDateParsingFailure { source })?;
let expiry_date = OffsetDateTime::from_unix_timestamp(expiry_timestamp).map_err(|source| {
CoconutError::InvalidExpiryDate {
unix_timestamp: expiry_timestamp,
source,
}
})?;
let now = OffsetDateTime::now_utc();
if expiry_date > now + MAX_FREE_PASS_VALIDITY {
return Err(CoconutError::TooLongFreePass { expiry_date });
}
Ok(())
}
#[get("/free-pass-nonce")]
pub async fn get_current_free_pass_nonce(
state: &RocketState<State>,
) -> Result<Json<FreePassNonceResponse>> {
debug!("Received free pass nonce request");
let current_nonce = state.storage.get_current_freepass_nonce().await?;
debug!("the current expected nonce is {current_nonce}");
Ok(Json(FreePassNonceResponse { current_nonce }))
}
#[post("/free-pass", data = "<freepass_request_body>")]
pub async fn post_free_pass(
freepass_request_body: Json<FreePassRequest>,
state: &RocketState<State>,
) -> Result<Json<BlindedSignatureResponse>> {
debug!("Received free pass sign request");
trace!("body: {:?}", freepass_request_body);
validate_freepass_public_attributes(&freepass_request_body)?;
// grab the admin of the bandwidth contract
let Some(authorised_admin) = state.get_bandwidth_contract_admin().await? else {
error!("our bandwidth contract does not have an admin set! We won't be able to migrate the contract! We should redeploy it ASAP");
return Err(CoconutError::MissingBandwidthContractAdmin);
};
// derive the address out of the provided pubkey
let requester = match freepass_request_body
.cosmos_pubkey
.account_id(authorised_admin.prefix())
{
Ok(address) => address,
Err(err) => {
return Err(CoconutError::AdminAccountDerivationFailure {
formatted_source: err.to_string(),
})
}
};
debug!("derived the following address out of the provided public key: {requester}. Going to check it against the authorised admin ({authorised_admin})");
if &requester != authorised_admin {
return Err(CoconutError::UnauthorisedFreePassAccount {
requester,
authorised_admin: authorised_admin.clone(),
});
}
let current_nonce = state.storage.get_current_freepass_nonce().await?;
debug!("the current expected nonce is {current_nonce}");
if current_nonce != freepass_request_body.used_nonce {
return Err(CoconutError::InvalidNonce {
current: current_nonce,
received: freepass_request_body.used_nonce,
});
}
// check if we have the signing key available
debug!("checking if we actually have coconut keys derived...");
let maybe_keypair_guard = state.coconut_keypair.get().await;
let Some(keypair_guard) = maybe_keypair_guard.as_ref() else {
return Err(CoconutError::KeyPairNotDerivedYet);
};
let Some(signing_key) = keypair_guard.as_ref() else {
return Err(CoconutError::KeyPairNotDerivedYet);
};
let tm_pubkey = freepass_request_body.tendermint_pubkey();
// currently accounts (excluding validators) don't use ed25519 and are secp256k1-based
let Some(secp256k1_pubkey) = tm_pubkey.secp256k1() else {
return Err(CoconutError::UnsupportedNonSecp256k1Key);
};
// make sure the signature actually verifies
secp256k1_pubkey
.verify(
&freepass_request_body.nonce_plaintext(),
&freepass_request_body.nonce_signature,
)
.map_err(|_| CoconutError::FreePassSignatureVerificationFailure)?;
// produce the partial signature
debug!("producing the partial credential");
let blinded_signature =
blind_sign(freepass_request_body.deref(), signing_key.keys.secret_key())?;
// update the nonce in storage (and also check if a parallel request hasn't updated it; if so we return an error. no race conditions allowed)
state
.storage
.update_and_validate_freepass_nonce(current_nonce + 1)
.await?;
// finally return the credential to the client
Ok(Json(BlindedSignatureResponse { blinded_signature }))
}
#[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(
@@ -36,7 +179,7 @@ pub async fn post_blind_sign(
// 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
!= IssuanceBandwidthCredential::PUBLIC_ATTRIBUTES as usize
{
return Err(CoconutError::InconsistentPublicAttributes);
}
@@ -75,7 +218,10 @@ pub async fn post_blind_sign(
// produce the partial signature
debug!("producing the partial credential");
let blinded_signature = blind_sign(&blind_sign_request_body, signing_key.keys.secret_key())?;
let blinded_signature = blind_sign(
blind_sign_request_body.deref(),
signing_key.keys.secret_key(),
)?;
// store the information locally
debug!("storing the issued credential in the database");
@@ -93,15 +239,28 @@ pub async fn verify_bandwidth_credential(
state: &RocketState<State>,
) -> Result<Json<VerifyCredentialResponse>> {
let proposal_id = verify_credential_body.proposal_id;
let proposal = state.client.get_proposal(proposal_id).await?;
let credential_data = &verify_credential_body.credential_data;
let epoch_id = credential_data.epoch_id;
let theta = &credential_data.verify_credential_request;
let voucher_value: u64 = if credential_data.typ.is_voucher() {
credential_data
.get_bandwidth_attribute()
.ok_or(CoconutError::MissingBandwidthValue)?
.parse()
.map_err(|source| CoconutError::VoucherValueParsingFailure { source })?
} else {
return Err(CoconutError::NotABandwidthVoucher {
typ: credential_data.typ,
});
};
// TODO: introduce a check to make sure we haven't already voted for this proposal to prevent DDOS
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)?
{
if !theta.has_blinded_serial_number(&proposal.description)? {
return Err(CoconutError::IncorrectProposal {
reason: String::from("incorrect blinded serial number in description"),
});
@@ -113,7 +272,7 @@ pub async fn verify_bandwidth_credential(
// 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())
.get_spent_credential(theta.blinded_serial_number_bs58())
.await?
.spend_credential
.ok_or(CoconutError::InvalidCredentialStatus {
@@ -125,16 +284,12 @@ pub async fn verify_bandwidth_credential(
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);
let verification_key = state.verification_key(epoch_id).await?;
let params = bandwidth_credential_params();
let mut vote_yes = credential_data.verify(params, &verification_key);
vote_yes &= Coin::from(proposed_release_funds)
== Coin::new(
verify_credential_body.credential.voucher_value() as u128,
state.mix_denom.clone(),
);
== Coin::new(voucher_value as u128, state.mix_denom.clone());
// Vote yes or no on the proposal based on the verification result
let ret = state
+2
View File
@@ -26,6 +26,8 @@ pub trait Client {
async fn dkg_contract_address(&self) -> Result<AccountId>;
async fn bandwidth_contract_admin(&self) -> Result<Option<AccountId>>;
async fn get_tx(&self, tx_hash: Hash) -> Result<TxResponse>;
async fn get_proposal(&self, proposal_id: u64) -> Result<ProposalResponse>;
+2 -2
View File
@@ -4,8 +4,8 @@
use crate::coconut::error::Result;
use crate::nyxd;
use crate::support::nyxd::ClientInner;
use nym_coconut::VerificationKey;
use nym_coconut_dkg_common::types::{Epoch, 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;
@@ -110,7 +110,7 @@ impl APICommunicationChannel for QueryCommunicationChannel {
ClientInner::Signing(client) => all_coconut_api_clients(client, epoch_id).await?,
};
let vk = obtain_aggregate_verification_key(&coconut_api_clients).await?;
let vk = obtain_aggregate_verification_key(&coconut_api_clients)?;
guard.insert(epoch_id, vk.clone());
+17 -9
View File
@@ -7,13 +7,16 @@ use nym_coconut_bandwidth_contract_common::events::{
COSMWASM_DEPOSITED_FUNDS_EVENT_TYPE, DEPOSIT_ENCRYPTION_KEY, DEPOSIT_IDENTITY_KEY,
DEPOSIT_INFO, DEPOSIT_VALUE,
};
use nym_credentials::coconut::bandwidth::BandwidthVoucher;
use nym_credentials::coconut::bandwidth::voucher::BandwidthVoucherIssuanceData;
use nym_credentials::coconut::bandwidth::IssuanceBandwidthCredential;
use nym_crypto::asymmetric::identity;
use nym_validator_client::nyxd::helpers::find_tx_attribute;
use nym_validator_client::nyxd::TxResponse;
pub async fn validate_deposit_tx(request: &BlindSignRequestBody, tx: TxResponse) -> Result<()> {
if request.public_attributes_plain.len() != BandwidthVoucher::PUBLIC_ATTRIBUTES as usize {
if request.public_attributes_plain.len()
!= IssuanceBandwidthCredential::PUBLIC_ATTRIBUTES as usize
{
return Err(CoconutError::InconsistentPublicAttributes);
}
@@ -58,8 +61,10 @@ pub async fn validate_deposit_tx(request: &BlindSignRequestBody, tx: TxResponse)
// verify signature
let x25519 = identity::PublicKey::from_base58_string(x25519_raw)?;
let plaintext =
BandwidthVoucher::signable_plaintext(&request.inner_sign_request, request.tx_hash);
let plaintext = BandwidthVoucherIssuanceData::request_plaintext(
&request.inner_sign_request,
request.tx_hash,
);
x25519.verify(plaintext, &request.signature)?;
Ok(())
@@ -68,17 +73,20 @@ pub async fn validate_deposit_tx(request: &BlindSignRequestBody, tx: TxResponse)
#[cfg(test)]
mod test {
use super::*;
use crate::coconut::tests::{tx_entry_fixture, voucher_request_fixture};
use crate::coconut::tests::{tx_entry_fixture, voucher_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_credentials::coconut::bandwidth::CredentialType;
use nym_validator_client::nyxd::{Event, EventAttribute};
#[tokio::test]
async fn validate_deposit_tx_test() {
let (voucher, correct_request) = voucher_request_fixture(coin(1234, "unym"), None);
let voucher = voucher_fixture(coin(1234, "unym"), None);
let signing_data = voucher.prepare_for_signing();
let voucher_data = voucher.get_variant_data().voucher_data().unwrap();
let correct_request = voucher_data.create_blind_sign_request_body(&signing_data);
let mut tx_entry = tx_entry_fixture(correct_request.tx_hash);
let good_deposit_attribute = EventAttribute {
@@ -166,7 +174,7 @@ mod test {
err.to_string(),
CoconutError::InconsistentDepositInfo {
on_chain: "bandwidth deposit info".to_string(),
request: VOUCHER_INFO.to_string(),
request: CredentialType::Voucher.to_string(),
}
.to_string(),
);
@@ -307,7 +315,7 @@ mod test {
.parse()
.unwrap(),
"3vUCc6MCN5AC2LNgDYjRB1QeErZSN1S8f6K14JHjpUcKWXbjGYFExA8DbwQQBki9gyUqrpBF94Drttb4eMcGQXkp".parse().unwrap(),
voucher.get_public_attributes_plain(),
voucher.get_plain_public_attributes(),
);
tx_entry.tx_result.events.get_mut(0).unwrap().attributes = vec![
good_deposit_attribute.clone(),
+4 -4
View File
@@ -7,13 +7,13 @@ use crate::coconut::dkg::controller::DkgController;
use crate::coconut::dkg::state::key_derivation::{DealerRejectionReason, DerivationFailure};
use crate::coconut::error::CoconutError;
use crate::coconut::keys::KeyPairWithEpoch;
use crate::coconut::state::bandwidth_voucher_params;
use crate::coconut::state::bandwidth_credential_params;
use cosmwasm_std::Addr;
use log::debug;
use nym_coconut::KeyPair as CoconutKeyPair;
use nym_coconut::{check_vk_pairing, Base58, SecretKey, VerificationKey};
use nym_coconut_dkg_common::event_attributes::DKG_PROPOSAL_ID;
use nym_coconut_dkg_common::types::{DealingIndex, EpochId, NodeIndex};
use nym_coconut_interface::KeyPair as CoconutKeyPair;
use nym_dkg::{
bte::{self, decrypt_share},
combine_shares, try_recover_verification_keys, Dealing,
@@ -421,7 +421,7 @@ impl<R: RngCore + CryptoRng> DkgController<R> {
// we know we had a non-empty map of dealings and thus, at the very least, we must have derived a single secret
// (i.e. the x-element)
let sk = SecretKey::create_from_raw(derived_x.unwrap(), derived_secrets);
let derived_vk = sk.verification_key(bandwidth_voucher_params());
let derived_vk = sk.verification_key(bandwidth_credential_params());
// make the key we derived out of the decrypted shares matches the partial key
// (cryptographically there shouldn't be any reason for the mismatch,
@@ -432,7 +432,7 @@ impl<R: RngCore + CryptoRng> DkgController<R> {
.derived_partials_for(receiver_index)
.ok_or(KeyDerivationError::NoSelfPartialKey { receiver_index })?;
if !check_vk_pairing(bandwidth_voucher_params(), &derived_partial, &derived_vk) {
if !check_vk_pairing(bandwidth_credential_params(), &derived_partial, &derived_vk) {
// can't do anything, we got all dealings, we derived all keys, but somehow they don't match
error!("our derived key does not match the expected partials!");
return Ok(Err(DerivationFailure::MismatchedPartialKey));
+2 -2
View File
@@ -3,7 +3,7 @@
use crate::coconut::dkg::controller::DkgController;
use crate::coconut::error::CoconutError;
use crate::coconut::state::bandwidth_voucher_params;
use crate::coconut::state::bandwidth_credential_params;
use cosmwasm_std::Addr;
use cw3::Vote;
use nym_coconut::{check_vk_pairing, Base58, VerificationKey};
@@ -119,7 +119,7 @@ impl<R: RngCore + CryptoRng> DkgController<R> {
});
};
if !check_vk_pairing(bandwidth_voucher_params(), &self_derived, &recovered_key) {
if !check_vk_pairing(bandwidth_credential_params(), &self_derived, &recovered_key) {
return reject(ShareRejectionReason::InconsistentKeys {
epoch_id,
owner,
+70 -3
View File
@@ -3,6 +3,7 @@
use crate::node_status_api::models::NymApiStorageError;
use nym_coconut_dkg_common::types::{ChunkIndex, DealingIndex, EpochId};
use nym_credentials::coconut::bandwidth::{CredentialType, UnknownCredentialType};
use nym_crypto::asymmetric::{
encryption::KeyRecoveryError,
identity::{Ed25519RecoveryError, SignatureError},
@@ -10,11 +11,15 @@ use nym_crypto::asymmetric::{
use nym_dkg::error::DkgError;
use nym_validator_client::coconut::CoconutApiError;
use nym_validator_client::nyxd::error::{NyxdError, TendermintError};
use nym_validator_client::nyxd::AccountId;
use rocket::http::{ContentType, Status};
use rocket::response::Responder;
use rocket::{response, Request, Response};
use std::io::Cursor;
use std::num::ParseIntError;
use thiserror::Error;
use time::error::ComponentRange;
use time::OffsetDateTime;
pub type Result<T> = std::result::Result<T, CoconutError>;
@@ -23,6 +28,71 @@ pub enum CoconutError {
#[error(transparent)]
IOError(#[from] std::io::Error),
#[error("the address of the bandwidth contract hasn't been set")]
MissingBandwidthContractAddress,
#[error("the current bandwidth contract does not have any admin address set")]
MissingBandwidthContractAdmin,
#[error("failed to derive the admin account from the provided public key: {formatted_source}")]
AdminAccountDerivationFailure { formatted_source: String },
#[error("the requester of the free pass ({requester}) is not authorised. the only allowed account is {authorised_admin}.")]
UnauthorisedFreePassAccount {
requester: AccountId,
authorised_admin: AccountId,
},
#[error("failed to verify signature on the provided free pass request")]
FreePassSignatureVerificationFailure,
#[error("the provided signing nonce is invalid. the current value is: {current}. got {received} instead")]
InvalidNonce { current: u32, received: u32 },
#[error("only secp256k1 keys are supported for free pass issuance")]
UnsupportedNonSecp256k1Key,
#[error("received credential request for an unknown type: {0}")]
UnknownCredentialType(#[from] UnknownCredentialType),
#[error("the provided free pass request had an unexpected number of public attributes. got {got} but expected {expected}")]
InvalidFreePassAttributes { got: usize, expected: usize },
#[error("the provided free pass request had an invalid type attribute (got: '{got}')")]
InvalidFreePassTypeAttribute { got: CredentialType },
#[error("failed to parse the free pass expiry date: {source}")]
ExpiryDateParsingFailure {
#[source]
source: ParseIntError,
},
#[error("failed to parse expiry timestamp into proper datetime: {source}")]
InvalidExpiryDate {
unix_timestamp: i64,
#[source]
source: ComponentRange,
},
#[error(
"the provided free pass request has too long expiry (expiry is set to on {expiry_date})"
)]
TooLongFreePass { expiry_date: OffsetDateTime },
#[error("the received bandwidth voucher did not contain deposit value")]
MissingBandwidthValue,
#[error(
"the received bandwidth credential is not a bandwidth voucher. the encoded type is: {typ}"
)]
NotABandwidthVoucher { typ: CredentialType },
#[error("failed to parse the bandwidth voucher value: {source}")]
VoucherValueParsingFailure {
#[source]
source: ParseIntError,
},
#[error("coconut api query failure: {0}")]
CoconutApiError(#[from] CoconutApiError),
@@ -87,9 +157,6 @@ pub enum CoconutError {
#[error("public attributes in request differ from the ones in deposit: Expected {0}, got {1}")]
DifferentPublicAttributes(String, String),
#[error("error in coconut interface: {0}")]
CoconutInterfaceError(#[from] nym_coconut_interface::error::CoconutInterfaceError),
#[error("storage error: {0}")]
StorageError(#[from] NymApiStorageError),
+35 -8
View File
@@ -2,9 +2,10 @@
// SPDX-License-Identifier: GPL-3.0-only
use crate::coconut::error::CoconutError;
use crate::coconut::state::bandwidth_voucher_params;
use crate::coconut::state::bandwidth_credential_params;
use nym_api_requests::coconut::models::FreePassRequest;
use nym_api_requests::coconut::BlindSignRequestBody;
use nym_coconut::{BlindedSignature, SecretKey};
use nym_coconut::{Attribute, BlindSignRequest, BlindedSignature, SecretKey};
use nym_validator_client::nyxd::error::NyxdError::AbciError;
// If the result is already established, the vote might be redundant and
@@ -21,17 +22,43 @@ pub(crate) fn accepted_vote_err(ret: Result<(), CoconutError>) -> Result<(), Coc
Ok(())
}
pub(crate) fn blind_sign(
request: &BlindSignRequestBody,
pub(crate) trait CredentialRequest {
fn blind_sign_request(&self) -> &BlindSignRequest;
fn public_attributes(&self) -> Vec<Attribute>;
}
impl CredentialRequest for BlindSignRequestBody {
fn blind_sign_request(&self) -> &BlindSignRequest {
&self.inner_sign_request
}
fn public_attributes(&self) -> Vec<Attribute> {
self.public_attributes_hashed()
}
}
impl CredentialRequest for FreePassRequest {
fn blind_sign_request(&self) -> &BlindSignRequest {
&self.inner_sign_request
}
fn public_attributes(&self) -> Vec<Attribute> {
self.public_attributes_hashed()
}
}
pub(crate) fn blind_sign<C: CredentialRequest>(
request: &C,
signing_key: &SecretKey,
) -> Result<BlindedSignature, CoconutError> {
let public_attributes = request.public_attributes_hashed();
let public_attributes = request.public_attributes();
let attributes_ref = public_attributes.iter().collect::<Vec<_>>();
Ok(nym_coconut_interface::blind_sign(
bandwidth_voucher_params(),
Ok(nym_coconut::blind_sign(
bandwidth_credential_params(),
signing_key,
&request.inner_sign_request,
request.blind_sign_request(),
&attributes_ref,
)?)
}
+2 -2
View File
@@ -18,12 +18,12 @@ pub struct KeyPair {
#[derive(Debug)]
pub struct KeyPairWithEpoch {
pub(crate) keys: nym_coconut_interface::KeyPair,
pub(crate) keys: nym_coconut::KeyPair,
pub(crate) issued_for_epoch: EpochId,
}
impl KeyPairWithEpoch {
pub(crate) fn new(keys: nym_coconut_interface::KeyPair, issued_for_epoch: EpochId) -> Self {
pub(crate) fn new(keys: nym_coconut::KeyPair, issued_for_epoch: EpochId) -> Self {
KeyPairWithEpoch {
keys,
issued_for_epoch,
+2 -2
View File
@@ -2,7 +2,7 @@
// SPDX-License-Identifier: GPL-3.0-only
use crate::coconut::keys::KeyPairWithEpoch;
use crate::coconut::state::bandwidth_voucher_params;
use crate::coconut::state::bandwidth_credential_params;
use nym_coconut::{CoconutError, KeyPair, SecretKey};
use nym_coconut_dkg_common::types::EpochId;
use nym_pemstore::traits::PemStorableKey;
@@ -33,7 +33,7 @@ impl PemStorableKey for KeyPairWithEpoch {
]);
let sk = SecretKey::from_bytes(&bytes[mem::size_of::<EpochId>()..])?;
let vk = sk.verification_key(bandwidth_voucher_params());
let vk = sk.verification_key(bandwidth_credential_params());
Ok(KeyPairWithEpoch {
keys: KeyPair::from_keys(sk, vk),
+2
View File
@@ -52,6 +52,8 @@ where
// this format! is so ugly...
format!("/{NYM_API_VERSION}/{COCONUT_ROUTES}/{BANDWIDTH}"),
routes![
api_routes::get_current_free_pass_nonce,
api_routes::post_free_pass,
api_routes::post_blind_sign,
api_routes::verify_bandwidth_credential,
api_routes::epoch_credentials,
+13 -14
View File
@@ -10,26 +10,18 @@ use crate::coconut::storage::CoconutStorageExt;
use crate::support::storage::NymApiStorage;
use nym_api_requests::coconut::helpers::issued_credential_plaintext;
use nym_api_requests::coconut::BlindSignRequestBody;
use nym_coconut::Parameters;
use nym_coconut::{BlindedSignature, VerificationKey};
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, OnceLock};
use nym_validator_client::nyxd::{AccountId, Hash, TxResponse};
use std::sync::Arc;
use tokio::sync::OnceCell;
// 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)
pub(crate) fn bandwidth_voucher_params() -> &'static Parameters {
static BANDWIDTH_CREDENTIAL_PARAMS: OnceLock<Parameters> = OnceLock::new();
BANDWIDTH_CREDENTIAL_PARAMS.get_or_init(BandwidthVoucher::default_parameters)
}
pub use nym_credentials::coconut::bandwidth::bandwidth_credential_params;
pub struct State {
pub(crate) client: Arc<dyn LocalClient + Send + Sync>,
pub(crate) bandwidth_contract_admin: OnceCell<Option<AccountId>>,
pub(crate) mix_denom: String,
pub(crate) coconut_keypair: KeyPair,
pub(crate) identity_keypair: identity::KeyPair,
@@ -55,6 +47,7 @@ impl State {
Self {
client,
bandwidth_contract_admin: OnceCell::new(),
mix_denom,
coconut_keypair: key_pair,
identity_keypair,
@@ -77,6 +70,12 @@ impl State {
self.client.get_tx(tx_hash).await
}
pub async fn get_bandwidth_contract_admin(&self) -> Result<&Option<AccountId>> {
self.bandwidth_contract_admin
.get_or_try_init(|| async { self.client.bandwidth_contract_admin().await })
.await
}
pub async fn validate_request(
&self,
request: &BlindSignRequestBody,
+57
View File
@@ -4,6 +4,7 @@
use crate::coconut::storage::models::{EpochCredentials, IssuedCredential};
use crate::support::storage::manager::StorageManager;
use nym_coconut_dkg_common::types::EpochId;
use thiserror::Error;
#[async_trait]
pub trait CoconutStorageManagerExt {
@@ -120,6 +121,17 @@ pub trait CoconutStorageManagerExt {
start_after: i64,
limit: u32,
) -> Result<Vec<IssuedCredential>, sqlx::Error>;
/// Attempts to retrieve the current value of the freepass nonce.
async fn get_current_freepass_nonce(&self) -> Result<u32, sqlx::Error>;
/// Attempt to update the currently stored nonce to the provided value whilst ensuring
/// it's strictly equal the current value plus 1
///
/// # Arguments
///
/// * `new`: the new value of the free pass nonce
async fn update_and_validate_freepass_nonce(&self, new: u32) -> Result<(), sqlx::Error>;
}
#[async_trait]
@@ -378,4 +390,49 @@ impl CoconutStorageManagerExt for StorageManager {
.fetch_all(&self.connection_pool)
.await
}
/// Attempts to retrieve the current value of the freepass nonce.
async fn get_current_freepass_nonce(&self) -> Result<u32, sqlx::Error> {
sqlx::query!("SELECT current_nonce as 'current_nonce: u32' FROM issued_freepass")
.fetch_one(&self.connection_pool)
.await
.map(|row| row.current_nonce)
}
/// Attempt to update the currently stored nonce to the provided value whilst ensuring
/// it's strictly equal the current value plus 1
///
/// # Arguments
///
/// * `new`: the new value of the free pass nonce
async fn update_and_validate_freepass_nonce(&self, new: u32) -> Result<(), sqlx::Error> {
let mut tx = self.connection_pool.begin().await?;
let currently_stored =
sqlx::query!("SELECT current_nonce as 'current_nonce: u32' FROM issued_freepass")
.fetch_one(&mut tx)
.await?
.current_nonce;
if currently_stored + 1 != new {
// this is not the best error but I really don't want to be creating a new enum
return Err(sqlx::Error::Decode(Box::new(UnexpectedNonce {
current: currently_stored,
got: new,
})));
}
sqlx::query!("UPDATE issued_freepass SET current_nonce = ?", new)
.execute(&mut tx)
.await?;
tx.commit().await
}
}
#[derive(Debug, Error)]
#[error("tried to store an invalid nonce. the received value is {got} while current is {current}. expected {current} + 1")]
pub struct UnexpectedNonce {
current: u32,
got: u32,
}
+12
View File
@@ -63,6 +63,10 @@ pub trait CoconutStorageExt {
&self,
pagination: Pagination<i64>,
) -> Result<Vec<IssuedCredential>, NymApiStorageError>;
async fn get_current_freepass_nonce(&self) -> Result<u32, NymApiStorageError>;
async fn update_and_validate_freepass_nonce(&self, new: u32) -> Result<(), NymApiStorageError>;
}
#[async_trait]
@@ -163,4 +167,12 @@ impl CoconutStorageExt for NymApiStorage {
.get_issued_credentials_paged(start_after, limit)
.await?)
}
async fn get_current_freepass_nonce(&self) -> Result<u32, NymApiStorageError> {
Ok(self.manager.get_current_freepass_nonce().await?)
}
async fn update_and_validate_freepass_nonce(&self, new: u32) -> Result<(), NymApiStorageError> {
Ok(self.manager.update_and_validate_freepass_nonce(new).await?)
}
}
+21 -26
View File
@@ -1,13 +1,12 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::coconut::tests::{voucher_request_fixture, TestFixture};
use crate::coconut::tests::{voucher_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;
@@ -102,12 +101,20 @@ async fn issued_credential() {
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 voucher1 = voucher_fixture(coin(1234, "unym"), Some(hash1.clone()));
let voucher2 = voucher_fixture(coin(1234, "unym"), Some(hash2.clone()));
let signing_data1 = voucher1.prepare_for_signing();
let voucher_data1 = voucher1.get_variant_data().voucher_data().unwrap();
let request1 = voucher_data1.create_blind_sign_request_body(&signing_data1);
let signing_data2 = voucher2.prepare_for_signing();
let voucher_data2 = voucher2.get_variant_data().voucher_data().unwrap();
let request2 = voucher_data2.create_blind_sign_request_body(&signing_data2);
let test_fixture = TestFixture::new().await;
test_fixture.add_deposit_tx(&voucher1);
test_fixture.add_deposit_tx(&voucher2);
test_fixture.add_deposit_tx(voucher_data1);
test_fixture.add_deposit_tx(voucher_data2);
// random credential that was never issued
let response = test_fixture.rocket.get(route(42)).dispatch().await;
@@ -116,10 +123,10 @@ async fn issued_credential() {
serde_json::from_str(&response.into_string().await.unwrap()).unwrap();
assert!(parsed_response.credential.is_none());
let cred1 = test_fixture.issue_credential(req1).await;
let cred1 = test_fixture.issue_credential(request1.clone()).await;
test_fixture.set_epoch(3);
let cred2 = test_fixture.issue_credential(req2).await;
let cred2 = test_fixture.issue_credential(request2.clone()).await;
let response = test_fixture.rocket.get(route(1)).dispatch().await;
assert_eq!(response.status(), Status::Ok);
@@ -136,49 +143,37 @@ async fn issued_credential() {
// 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!(voucher_data1.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,
request1.encode_commitments(),
issued1
.credential
.bs58_encoded_private_attributes_commitments
);
assert_eq!(
voucher1.get_public_attributes_plain(),
voucher1.get_plain_public_attributes(),
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!(voucher_data2.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,
request2.encode_commitments(),
issued2
.credential
.bs58_encoded_private_attributes_commitments
);
assert_eq!(
voucher2.get_public_attributes_plain(),
voucher2.get_plain_public_attributes(),
issued2.credential.public_attributes
);
}
+316 -282
View File
@@ -15,7 +15,7 @@ use cw3::{Proposal, ProposalResponse, Vote, VoteInfo, VoteResponse, Votes};
use cw4::{Cw4Contract, MemberResponse};
use nym_api_requests::coconut::models::{IssuedCredentialBody, IssuedCredentialResponse};
use nym_api_requests::coconut::{BlindSignRequestBody, BlindedSignatureResponse};
use nym_coconut::{BlindedSignature, Parameters};
use nym_coconut::{BlindedSignature, Parameters, VerificationKey};
use nym_coconut_bandwidth_contract_common::events::{
DEPOSITED_FUNDS_EVENT_TYPE, DEPOSIT_ENCRYPTION_KEY, DEPOSIT_IDENTITY_KEY, DEPOSIT_INFO,
DEPOSIT_VALUE,
@@ -32,10 +32,10 @@ use nym_coconut_dkg_common::types::{
InitialReplacementData, PartialContractDealingData, State as ContractState,
};
use nym_coconut_dkg_common::verification_key::{ContractVKShare, VerificationKeyShare};
use nym_coconut_interface::VerificationKey;
use nym_config::defaults::VOUCHER_INFO;
use nym_contracts_common::IdentityKey;
use nym_credentials::coconut::bandwidth::BandwidthVoucher;
use nym_credentials::coconut::bandwidth::voucher::BandwidthVoucherIssuanceData;
use nym_credentials::coconut::bandwidth::CredentialType;
use nym_credentials::IssuanceBandwidthCredential;
use nym_crypto::asymmetric::{encryption, identity};
use nym_dkg::{NodeIndex, Threshold};
use nym_mixnet_contract_common::BlockHeight;
@@ -228,6 +228,7 @@ impl FakeMultisigContractState {
#[derive(Debug)]
pub(crate) struct FakeBandwidthContractState {
pub(crate) address: Addr,
pub(crate) admin: Option<AccountId>,
pub(crate) spent_credentials: HashMap<String, SpendCredentialResponse>,
}
@@ -266,6 +267,11 @@ impl Default for FakeChainState {
let bandwidth_contract =
Addr::unchecked("n16a32stm6kknhq5cc8rx77elr66pygf2hfszw7wvpq746x3uffylqkjar4l");
let bandwidth_contract_admin =
"n1ahg0erc2fs6xx3j5m8sfx3ryuzdjh6kf6qm9plsf865fltekyrfsesac6a"
.parse()
.unwrap();
FakeChainState {
_counters: Default::default(),
@@ -300,6 +306,7 @@ impl Default for FakeChainState {
},
bandwidth_contract: FakeBandwidthContractState {
address: bandwidth_contract,
admin: Some(bandwidth_contract_admin),
spent_credentials: Default::default(),
},
}
@@ -550,6 +557,10 @@ impl super::client::Client for DummyClient {
Ok(self.state.lock().unwrap().dkg_contract.address.clone())
}
async fn bandwidth_contract_admin(&self) -> Result<Option<AccountId>> {
Ok(self.state.lock().unwrap().bandwidth_contract.admin.clone())
}
async fn get_tx(&self, tx_hash: Hash) -> Result<TxResponse> {
Ok(self
.state
@@ -684,6 +695,52 @@ impl super::client::Client for DummyClient {
})
}
async fn get_dealer_dealings_status(
&self,
epoch_id: EpochId,
dealer: String,
) -> Result<DealerDealingsStatusResponse> {
let guard = self.state.lock().unwrap();
let key_size = guard.dkg_contract.contract_state.key_size;
let dealer_addr = Addr::unchecked(&dealer);
let Some(epoch_dealings) = guard.dkg_contract.dealings.get(&epoch_id) else {
return Ok(DealerDealingsStatusResponse {
epoch_id,
dealer: dealer_addr,
all_dealings_fully_submitted: false,
dealing_submission_status: Default::default(),
});
};
let Some(dealer_dealings) = epoch_dealings.get(&dealer) else {
return Ok(DealerDealingsStatusResponse {
epoch_id,
dealer: dealer_addr,
all_dealings_fully_submitted: false,
dealing_submission_status: Default::default(),
});
};
let mut dealing_submission_status: BTreeMap<DealingIndex, DealingStatus> = BTreeMap::new();
for dealing_index in 0..key_size {
let metadata = dealer_dealings
.get(&dealing_index)
.map(|d| d.metadata.clone());
dealing_submission_status.insert(dealing_index, metadata.into());
}
Ok(DealerDealingsStatusResponse {
epoch_id,
dealer: Addr::unchecked(&dealer),
all_dealings_fully_submitted: dealing_submission_status
.values()
.all(|d| d.fully_submitted),
dealing_submission_status,
})
}
async fn get_dealing_status(
&self,
epoch_id: EpochId,
@@ -720,6 +777,53 @@ impl super::client::Client for DummyClient {
.collect())
}
async fn get_dealing_metadata(
&self,
epoch_id: EpochId,
dealer: String,
dealing_index: DealingIndex,
) -> Result<Option<DealingMetadata>> {
let guard = self.state.lock().unwrap();
let Some(epoch_dealings) = guard.dkg_contract.dealings.get(&epoch_id) else {
return Ok(None);
};
let Some(dealer_dealings) = epoch_dealings.get(&dealer) else {
return Ok(None);
};
let Some(dealing) = dealer_dealings.get(&dealing_index) else {
return Ok(None);
};
Ok(Some(dealing.metadata.clone()))
}
async fn get_dealing_chunk(
&self,
epoch_id: EpochId,
dealer: &str,
dealing_index: DealingIndex,
chunk_index: ChunkIndex,
) -> Result<Option<PartialContractDealingData>> {
let guard = self.state.lock().unwrap();
let Some(epoch_dealings) = guard.dkg_contract.dealings.get(&epoch_id) else {
return Ok(None);
};
let Some(dealer_dealings) = epoch_dealings.get(dealer) else {
return Ok(None);
};
let Some(dealing) = dealer_dealings.get(&dealing_index) else {
return Ok(None);
};
Ok(dealing.chunks.get(&chunk_index).cloned())
}
async fn get_verification_key_share(
&self,
epoch_id: EpochId,
@@ -743,7 +847,6 @@ impl super::client::Client for DummyClient {
Some(epoch_shares) => Ok(epoch_shares.values().cloned().collect()),
}
}
async fn vote_proposal(
&self,
proposal_id: u64,
@@ -879,6 +982,78 @@ impl super::client::Client for DummyClient {
gas_info: Default::default(),
})
}
async fn submit_dealing_metadata(
&self,
dealing_index: DealingIndex,
chunks: Vec<DealingChunkInfo>,
_resharing: bool,
) -> Result<ExecuteResult> {
let mut guard = self.state.lock().unwrap();
let current_epoch = guard.dkg_contract.epoch.epoch_id;
let epoch_dealings = guard
.dkg_contract
.dealings
.entry(current_epoch)
.or_default();
let dealer_dealings = epoch_dealings
.entry(self.validator_address.to_string())
.or_default();
dealer_dealings.insert(
dealing_index,
Dealing::new_metadata_submission(dealing_index, chunks),
);
let transaction_hash = guard._counters.next_tx_hash();
Ok(ExecuteResult {
logs: vec![],
data: Default::default(),
transaction_hash,
gas_info: Default::default(),
})
}
async fn submit_dealing_chunk(
&self,
chunk: PartialContractDealing,
_resharing: bool,
) -> Result<ExecuteResult> {
let mut guard = self.state.lock().unwrap();
let current_epoch = guard.dkg_contract.epoch.epoch_id;
let current_height = guard.block_info.height;
// normally we should do checks for existence, etc.
// but since this is a testing code, we assume everything is sent in order and the appropriate entries exist
let epoch_dealings = guard.dkg_contract.dealings.get_mut(&current_epoch).unwrap();
let dealer_dealings = epoch_dealings
.get_mut(self.validator_address.as_ref())
.unwrap();
let dealing_chunks = dealer_dealings.get_mut(&chunk.dealing_index).unwrap();
dealing_chunks.chunks.insert(chunk.chunk_index, chunk.data);
dealing_chunks
.metadata
.submitted_chunks
.get_mut(&chunk.chunk_index)
.unwrap()
.status
.submission_height = Some(current_height);
let transaction_hash = guard._counters.next_tx_hash();
Ok(ExecuteResult {
logs: vec![],
data: Default::default(),
transaction_hash,
gas_info: Default::default(),
})
}
async fn submit_verification_key_share(
&self,
share: VerificationKeyShare,
@@ -954,170 +1129,6 @@ impl super::client::Client for DummyClient {
gas_info: Default::default(),
})
}
async fn get_dealer_dealings_status(
&self,
epoch_id: EpochId,
dealer: String,
) -> Result<DealerDealingsStatusResponse> {
let guard = self.state.lock().unwrap();
let key_size = guard.dkg_contract.contract_state.key_size;
let dealer_addr = Addr::unchecked(&dealer);
let Some(epoch_dealings) = guard.dkg_contract.dealings.get(&epoch_id) else {
return Ok(DealerDealingsStatusResponse {
epoch_id,
dealer: dealer_addr,
all_dealings_fully_submitted: false,
dealing_submission_status: Default::default(),
});
};
let Some(dealer_dealings) = epoch_dealings.get(&dealer) else {
return Ok(DealerDealingsStatusResponse {
epoch_id,
dealer: dealer_addr,
all_dealings_fully_submitted: false,
dealing_submission_status: Default::default(),
});
};
let mut dealing_submission_status: BTreeMap<DealingIndex, DealingStatus> = BTreeMap::new();
for dealing_index in 0..key_size {
let metadata = dealer_dealings
.get(&dealing_index)
.map(|d| d.metadata.clone());
dealing_submission_status.insert(dealing_index, metadata.into());
}
Ok(DealerDealingsStatusResponse {
epoch_id,
dealer: Addr::unchecked(&dealer),
all_dealings_fully_submitted: dealing_submission_status
.values()
.all(|d| d.fully_submitted),
dealing_submission_status,
})
}
async fn get_dealing_metadata(
&self,
epoch_id: EpochId,
dealer: String,
dealing_index: DealingIndex,
) -> Result<Option<DealingMetadata>> {
let guard = self.state.lock().unwrap();
let Some(epoch_dealings) = guard.dkg_contract.dealings.get(&epoch_id) else {
return Ok(None);
};
let Some(dealer_dealings) = epoch_dealings.get(&dealer) else {
return Ok(None);
};
let Some(dealing) = dealer_dealings.get(&dealing_index) else {
return Ok(None);
};
Ok(Some(dealing.metadata.clone()))
}
async fn get_dealing_chunk(
&self,
epoch_id: EpochId,
dealer: &str,
dealing_index: DealingIndex,
chunk_index: ChunkIndex,
) -> Result<Option<PartialContractDealingData>> {
let guard = self.state.lock().unwrap();
let Some(epoch_dealings) = guard.dkg_contract.dealings.get(&epoch_id) else {
return Ok(None);
};
let Some(dealer_dealings) = epoch_dealings.get(dealer) else {
return Ok(None);
};
let Some(dealing) = dealer_dealings.get(&dealing_index) else {
return Ok(None);
};
Ok(dealing.chunks.get(&chunk_index).cloned())
}
async fn submit_dealing_metadata(
&self,
dealing_index: DealingIndex,
chunks: Vec<DealingChunkInfo>,
_resharing: bool,
) -> Result<ExecuteResult> {
let mut guard = self.state.lock().unwrap();
let current_epoch = guard.dkg_contract.epoch.epoch_id;
let epoch_dealings = guard
.dkg_contract
.dealings
.entry(current_epoch)
.or_default();
let dealer_dealings = epoch_dealings
.entry(self.validator_address.to_string())
.or_default();
dealer_dealings.insert(
dealing_index,
Dealing::new_metadata_submission(dealing_index, chunks),
);
let transaction_hash = guard._counters.next_tx_hash();
Ok(ExecuteResult {
logs: vec![],
data: Default::default(),
transaction_hash,
gas_info: Default::default(),
})
}
async fn submit_dealing_chunk(
&self,
chunk: PartialContractDealing,
_resharing: bool,
) -> Result<ExecuteResult> {
let mut guard = self.state.lock().unwrap();
let current_epoch = guard.dkg_contract.epoch.epoch_id;
let current_height = guard.block_info.height;
// normally we should do checks for existence, etc.
// but since this is a testing code, we assume everything is sent in order and the appropriate entries exist
let epoch_dealings = guard.dkg_contract.dealings.get_mut(&current_epoch).unwrap();
let dealer_dealings = epoch_dealings
.get_mut(self.validator_address.as_ref())
.unwrap();
let dealing_chunks = dealer_dealings.get_mut(&chunk.dealing_index).unwrap();
dealing_chunks.chunks.insert(chunk.chunk_index, chunk.data);
dealing_chunks
.metadata
.submitted_chunks
.get_mut(&chunk.chunk_index)
.unwrap()
.status
.submission_height = Some(current_height);
let transaction_hash = guard._counters.next_tx_hash();
Ok(ExecuteResult {
logs: vec![],
data: Default::default(),
transaction_hash,
gas_info: Default::default(),
})
}
}
#[derive(Clone, Debug)]
@@ -1171,9 +1182,9 @@ pub fn tx_entry_fixture(hash: Hash) -> TxResponse {
}
}
pub fn deposit_tx_fixture(voucher: &BandwidthVoucher) -> TxResponse {
pub fn deposit_tx_fixture(voucher_data: &BandwidthVoucherIssuanceData) -> TxResponse {
TxResponse {
hash: voucher.tx_hash(),
hash: voucher_data.tx_hash(),
height: Default::default(),
index: 0,
tx_result: ExecTxResult {
@@ -1188,22 +1199,25 @@ pub fn deposit_tx_fixture(voucher: &BandwidthVoucher) -> TxResponse {
attributes: vec![
EventAttribute {
key: DEPOSIT_VALUE.to_string(),
value: voucher.get_voucher_value(),
value: voucher_data.value_plain(),
index: false,
},
EventAttribute {
key: DEPOSIT_INFO.to_string(),
value: VOUCHER_INFO.to_string(),
value: CredentialType::Voucher.to_string(),
index: false,
},
EventAttribute {
key: DEPOSIT_IDENTITY_KEY.to_string(),
value: voucher.identity_key().public_key().to_base58_string(),
value: voucher_data.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(),
value: voucher_data
.encryption_key()
.public_key()
.to_base58_string(),
index: false,
},
],
@@ -1231,11 +1245,10 @@ pub fn blinded_signature_fixture() -> BlindedSignature {
BlindedSignature::from_bytes(&dummy_bytes).unwrap()
}
pub fn voucher_request_fixture<C: Into<Coin>>(
pub fn voucher_fixture<C: Into<Coin>>(
amount: C,
tx_hash: Option<String>,
) -> (BandwidthVoucher, BlindSignRequestBody) {
let params = Parameters::new(4).unwrap();
) -> IssuanceBandwidthCredential {
let mut rng = OsRng;
let tx_hash = if let Some(provided) = &tx_hash {
provided.parse().unwrap()
@@ -1250,23 +1263,7 @@ pub fn voucher_request_fixture<C: Into<Coin>>(
let enc_priv =
encryption::PrivateKey::from_bytes(&encryption_keypair.private_key().to_bytes()).unwrap();
let voucher = BandwidthVoucher::new(
&params,
amount.into().amount.to_string(),
VOUCHER_INFO.to_string(),
tx_hash,
id_priv,
enc_priv,
);
let request = BlindSignRequestBody::new(
voucher.blind_sign_request().clone(),
tx_hash,
voucher.sign(),
voucher.get_public_attributes_plain(),
);
(voucher, request)
IssuanceBandwidthCredential::new_voucher(amount.into(), tx_hash, id_priv, enc_priv)
}
fn dummy_signature() -> identity::Signature {
@@ -1344,9 +1341,10 @@ impl TestFixture {
self.chain_state.lock().unwrap().txs.insert(hash, tx);
}
fn add_deposit_tx(&self, voucher: &BandwidthVoucher) {
fn add_deposit_tx(&self, voucher: &BandwidthVoucherIssuanceData) {
let mut guard = self.chain_state.lock().unwrap();
let fixture = deposit_tx_fixture(voucher);
guard.txs.insert(voucher.tx_hash(), fixture);
}
@@ -1356,9 +1354,13 @@ impl TestFixture {
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);
let voucher = voucher_fixture(coin(1234, "unym"), Some(tx_hash.to_string()));
let signing_data = voucher.prepare_for_signing();
let voucher_data = voucher.get_variant_data().voucher_data().unwrap();
let req = voucher_data.create_blind_sign_request_body(&signing_data);
self.add_deposit_tx(voucher_data);
self.issue_credential(req).await;
}
@@ -1402,15 +1404,18 @@ mod credential_tests {
use super::*;
use crate::coconut::tests::helpers::init_chain;
use nym_api_requests::coconut::{VerifyCredentialBody, VerifyCredentialResponse};
use nym_coconut::tests::helpers::theta_from_keys_and_attributes;
use nym_coconut::{hash_to_scalar, ttp_keygen};
use nym_coconut::{blind_sign, hash_to_scalar, ttp_keygen};
use nym_coconut_bandwidth_contract_common::spend_credential::SpendCredential;
use nym_coconut_interface::Credential;
use nym_credentials::coconut::bandwidth::bandwidth_credential_params;
use nym_validator_client::nym_api::routes::COCONUT_VERIFY_BANDWIDTH_CREDENTIAL;
#[tokio::test]
async fn already_issued() {
let (_, request_body) = voucher_request_fixture(coin(1234, TEST_COIN_DENOM), None);
let voucher = voucher_fixture(coin(1234, TEST_COIN_DENOM), None);
let signing_data = voucher.prepare_for_signing();
let voucher_data = voucher.get_variant_data().voucher_data().unwrap();
let request_body = voucher_data.create_blind_sign_request_body(&signing_data);
let tx_hash = request_body.tx_hash;
let tx_entry = tx_entry_fixture(tx_hash);
@@ -1493,7 +1498,11 @@ mod credential_tests {
.unwrap();
assert!(state.already_issued(tx_hash).await.unwrap().is_none());
let (_, request_body) = voucher_request_fixture(coin(1234, TEST_COIN_DENOM), None);
let voucher = voucher_fixture(coin(1234, TEST_COIN_DENOM), None);
let signing_data = voucher.prepare_for_signing();
let voucher_data = voucher.get_variant_data().voucher_data().unwrap();
let request_body = voucher_data.create_blind_sign_request_body(&signing_data);
let commitments = request_body.encode_commitments();
let public = request_body.public_attributes_plain.clone();
let sig = blinded_signature_fixture();
@@ -1583,10 +1592,8 @@ mod credential_tests {
let identity_keypair = identity::KeyPair::new(&mut rng);
let encryption_keypair = encryption::KeyPair::new(&mut rng);
let voucher = BandwidthVoucher::new(
&params,
"1234".to_string(),
VOUCHER_INFO.to_string(),
let voucher = IssuanceBandwidthCredential::new_voucher(
coin(1234, "unym"),
tx_hash,
identity::PrivateKey::from_base58_string(
identity_keypair.private_key().to_base58_string(),
@@ -1604,7 +1611,9 @@ mod credential_tests {
let chain = init_chain();
let tx_entry = deposit_tx_fixture(&voucher);
let voucher_data = voucher.get_variant_data().voucher_data().unwrap();
let tx_entry = deposit_tx_fixture(voucher_data);
chain.lock().unwrap().txs.insert(tx_hash, tx_entry.clone());
let nyxd_client = DummyClient::new(
@@ -1634,14 +1643,9 @@ mod credential_tests {
.await
.expect("valid rocket instance");
let request_signature = voucher.sign();
let request_body = BlindSignRequestBody::new(
voucher.blind_sign_request().clone(),
tx_hash,
request_signature,
voucher.get_public_attributes_plain(),
);
let signing_data = voucher.prepare_for_signing();
let voucher_data = voucher.get_variant_data().voucher_data().unwrap();
let request_body = voucher_data.create_blind_sign_request_body(&signing_data);
let response = client
.post(format!(
@@ -1673,24 +1677,40 @@ mod credential_tests {
let nyxd_client = DummyClient::new(validator_address.clone(), chain.clone());
let db_dir = tempdir().unwrap();
let params = Parameters::new(4).unwrap();
let mut key_pairs = ttp_keygen(&params, 1, 1).unwrap();
let voucher_value = 1234u64;
let voucher_info = "voucher info";
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
// generate all the credential requests
let params = bandwidth_credential_params();
let key_pair = nym_coconut::keygen(params);
let epoch = 1;
let voucher_amount = coin(1234, "unym");
let issuance = voucher_fixture(coin(1234, "unym"), None);
let sig_req = issuance.prepare_for_signing();
let pub_attrs_hashed = sig_req
.public_attributes_plain
.iter()
.enumerate()
.map(|(idx, _)| (idx + 1) as u64)
.collect();
let theta =
theta_from_keys_and_attributes(&params, &key_pairs, &indices, &public_attributes_ref)
.unwrap();
let key_pair = key_pairs.remove(0);
.map(hash_to_scalar)
.collect::<Vec<_>>();
let pub_attrs = pub_attrs_hashed.iter().collect::<Vec<_>>();
let blind_sig = blind_sign(
params,
key_pair.secret_key(),
&sig_req.blind_sign_request,
&pub_attrs,
)
.unwrap();
let sig = blind_sig
.unblind(
key_pair.verification_key(),
&sig_req.pedersen_commitments_openings,
)
.unwrap();
let issued = issuance.into_issued_credential(sig, epoch);
let spending = issued
.prepare_for_spending(key_pair.verification_key())
.unwrap();
let storage1 = NymApiStorage::init(db_dir.path().join("storage.db"))
.await
.unwrap();
@@ -1699,7 +1719,7 @@ mod credential_tests {
staged_key_pair
.set(KeyPairWithEpoch {
keys: key_pair,
issued_for_epoch: 1,
issued_for_epoch: epoch,
})
.await;
staged_key_pair.validate();
@@ -1719,13 +1739,11 @@ mod credential_tests {
.await
.expect("valid rocket instance");
let credential =
Credential::new(4, theta.clone(), voucher_value, voucher_info.to_string(), 0);
let proposal_id = 42;
// The address is not used, so we can use a duplicate
let gateway_cosmos_addr = validator_address.clone();
let req =
VerifyCredentialBody::new(credential.clone(), proposal_id, gateway_cosmos_addr.clone());
VerifyCredentialBody::new(spending.clone(), proposal_id, gateway_cosmos_addr.clone());
// Test endpoint with not proposal for the proposal id
let response = client
@@ -1787,7 +1805,9 @@ mod credential_tests {
);
// Test the endpoint with no msg in the proposal action
proposal.description = credential.blinded_serial_number();
proposal.description = spending
.verify_credential_request
.blinded_serial_number_bs58();
chain
.lock()
.unwrap()
@@ -1812,9 +1832,9 @@ mod credential_tests {
);
// Test the endpoint without any credential recorded in the Coconut Bandwidth Contract
let funds = Coin::new(voucher_value as u128, TEST_COIN_DENOM);
let funds = voucher_amount.clone();
let msg = nym_coconut_bandwidth_contract_common::msg::ExecuteMsg::ReleaseFunds {
funds: funds.clone().into(),
funds: funds.clone(),
};
let cosmos_msg = CosmosMsg::Wasm(WasmMsg::Execute {
contract_addr: String::new(),
@@ -1851,7 +1871,9 @@ mod credential_tests {
.bandwidth_contract
.spent_credentials
.insert(
credential.blinded_serial_number(),
spending
.verify_credential_request
.blinded_serial_number_bs58(),
SpendCredentialResponse::new(None),
);
@@ -1874,8 +1896,10 @@ mod credential_tests {
// Test the endpoint with a credential that doesn't verify correctly
let mut spent_credential = SpendCredential::new(
funds.clone().into(),
credential.blinded_serial_number(),
funds.clone(),
spending
.verify_credential_request
.blinded_serial_number_bs58(),
Addr::unchecked("unimportant"),
);
chain
@@ -1884,47 +1908,55 @@ mod credential_tests {
.bandwidth_contract
.spent_credentials
.insert(
credential.blinded_serial_number(),
spending
.verify_credential_request
.blinded_serial_number_bs58(),
SpendCredentialResponse::new(Some(spent_credential.clone())),
);
let bad_credential = Credential::new(
4,
theta.clone(),
voucher_value,
String::from("bad voucher info"),
0,
);
let bad_req =
VerifyCredentialBody::new(bad_credential, proposal_id, gateway_cosmos_addr.clone());
let response = client
.post(format!(
"/{}/{}/{}/{}",
API_VERSION, COCONUT_ROUTES, BANDWIDTH, COCONUT_VERIFY_BANDWIDTH_CREDENTIAL
))
.json(&bad_req)
.dispatch()
.await;
assert_eq!(response.status(), Status::Ok);
let verify_credential_response = serde_json::from_str::<VerifyCredentialResponse>(
&response.into_string().await.unwrap(),
)
.unwrap();
assert!(!verify_credential_response.verification_result);
assert_eq!(
cw3::Status::Rejected,
chain
.lock()
.unwrap()
.multisig_contract
.proposals
.get(&proposal_id)
.unwrap()
.status
);
// TODO: somehow restore that test
// let bad_credential = Credential::new(
// 4,
// theta.clone(),
// voucher_value,
// String::from("bad voucher info"),
// 0,
// );
// let bad_req = VerifyCredentialBody::new(
// bad_credential,
// epoch_id,
// proposal_id,
// gateway_cosmos_addr.clone(),
// );
// let response = client
// .post(format!(
// "/{}/{}/{}/{}",
// API_VERSION, COCONUT_ROUTES, BANDWIDTH, COCONUT_VERIFY_BANDWIDTH_CREDENTIAL
// ))
// .json(&bad_req)
// .dispatch()
// .await;
// assert_eq!(response.status(), Status::Ok);
// let verify_credential_response = serde_json::from_str::<VerifyCredentialResponse>(
// &response.into_string().await.unwrap(),
// )
// .unwrap();
// assert!(!verify_credential_response.verification_result);
// assert_eq!(
// cw3::Status::Rejected,
// chain
// .lock()
// .unwrap()
// .multisig_contract
// .proposals
// .get(&proposal_id)
// .unwrap()
// .status
// );
// Test the endpoint with a proposal that has a different value for the funds to be released
// then what's in the credential
let funds = Coin::new((voucher_value + 10) as u128, TEST_COIN_DENOM);
let funds = Coin::new(voucher_amount.amount.u128() + 10, TEST_COIN_DENOM);
let msg = nym_coconut_bandwidth_contract_common::msg::ExecuteMsg::ReleaseFunds {
funds: funds.clone().into(),
};
@@ -1968,9 +2000,9 @@ mod credential_tests {
);
// Test the endpoint with every dependency met
let funds = Coin::new(voucher_value as u128, TEST_COIN_DENOM);
let funds = voucher_amount;
let msg = nym_coconut_bandwidth_contract_common::msg::ExecuteMsg::ReleaseFunds {
funds: funds.clone().into(),
funds: funds.clone(),
};
let cosmos_msg = CosmosMsg::Wasm(WasmMsg::Execute {
contract_addr: String::new(),
@@ -2019,7 +2051,9 @@ mod credential_tests {
.bandwidth_contract
.spent_credentials
.insert(
credential.blinded_serial_number(),
spending
.verify_credential_request
.blinded_serial_number_bs58(),
SpendCredentialResponse::new(Some(spent_credential)),
);
let response = client
+14
View File
@@ -358,6 +358,20 @@ impl crate::coconut::client::Client for Client {
)
}
async fn bandwidth_contract_admin(&self) -> crate::coconut::error::Result<Option<AccountId>> {
let guard = self.inner.read().await;
let bandwidth_contract = query_guard!(
guard,
coconut_bandwidth_contract_address()
.ok_or(CoconutError::MissingBandwidthContractAddress)
)?;
let contract = query_guard!(guard, get_contract(bandwidth_contract)).await?;
Ok(contract.contract_info.admin)
}
async fn get_tx(&self, tx_hash: Hash) -> crate::coconut::error::Result<nyxd::TxResponse> {
nyxd_query!(self, get_tx(tx_hash).await).map_err(|source| {
CoconutError::TxRetrievalFailure {
+51 -17
View File
@@ -958,6 +958,12 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f"
[[package]]
name = "const-str"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aca749d3d3f5b87a0d6100509879f9cf486ab510803a4a4e1001da1ff61c2bd6"
[[package]]
name = "constant_time_eq"
version = "0.3.0"
@@ -1723,6 +1729,7 @@ dependencies = [
"digest 0.10.7",
"elliptic-curve 0.13.5",
"rfc6979 0.4.0",
"serdect",
"signature 2.1.0",
"spki 0.7.2",
]
@@ -1831,6 +1838,7 @@ dependencies = [
"pkcs8 0.10.2",
"rand_core 0.6.4",
"sec1 0.7.3",
"serdect",
"subtle 2.4.1",
"zeroize",
]
@@ -3687,8 +3695,9 @@ dependencies = [
"bs58 0.4.0",
"cosmrs",
"cosmwasm-std",
"ecdsa 0.16.8",
"getset",
"nym-coconut-interface",
"nym-credentials-interface",
"nym-crypto",
"nym-mixnet-contract-common",
"nym-node-requests",
@@ -3702,9 +3711,11 @@ name = "nym-bandwidth-controller"
version = "0.1.0"
dependencies = [
"bip39",
"nym-coconut-interface",
"log",
"nym-coconut",
"nym-credential-storage",
"nym-credentials",
"nym-credentials-interface",
"nym-crypto",
"nym-network-defaults",
"nym-validator-client",
@@ -3722,6 +3733,7 @@ dependencies = [
"clap",
"clap_complete",
"clap_complete_fig",
"const-str",
"log",
"pretty_env_logger",
"schemars",
@@ -3762,6 +3774,7 @@ dependencies = [
"serde",
"serde_json",
"sha2 0.10.8",
"si-scale",
"sqlx",
"tap",
"thiserror",
@@ -3820,17 +3833,6 @@ dependencies = [
"nym-multisig-contract-common",
]
[[package]]
name = "nym-coconut-interface"
version = "0.1.0"
dependencies = [
"bs58 0.4.0",
"getset",
"nym-coconut",
"serde",
"thiserror",
]
[[package]]
name = "nym-config"
version = "0.1.0"
@@ -3916,23 +3918,37 @@ dependencies = [
"sqlx",
"thiserror",
"tokio",
"zeroize",
]
[[package]]
name = "nym-credentials"
version = "0.1.0"
dependencies = [
"bincode",
"bls12_381",
"cosmrs",
"log",
"nym-api-requests",
"nym-coconut-interface",
"nym-credentials-interface",
"nym-crypto",
"nym-validator-client",
"serde",
"thiserror",
"time",
"zeroize",
]
[[package]]
name = "nym-credentials-interface"
version = "0.1.0"
dependencies = [
"bls12_381",
"nym-coconut",
"serde",
"thiserror",
]
[[package]]
name = "nym-crypto"
version = "0.4.0"
@@ -4030,8 +4046,8 @@ dependencies = [
"gloo-utils",
"log",
"nym-bandwidth-controller",
"nym-coconut-interface",
"nym-credential-storage",
"nym-credentials",
"nym-crypto",
"nym-gateway-requests",
"nym-network-defaults",
@@ -4061,8 +4077,8 @@ dependencies = [
"futures",
"generic-array 0.14.7",
"log",
"nym-coconut-interface",
"nym-credentials",
"nym-credentials-interface",
"nym-crypto",
"nym-pemstore",
"nym-sphinx",
@@ -4138,6 +4154,7 @@ dependencies = [
"cfg-if",
"dotenvy",
"hex-literal",
"log",
"once_cell",
"schemars",
"serde",
@@ -4491,9 +4508,9 @@ dependencies = [
"itertools",
"log",
"nym-api-requests",
"nym-coconut",
"nym-coconut-bandwidth-contract-common",
"nym-coconut-dkg-common",
"nym-coconut-interface",
"nym-config",
"nym-contracts-common",
"nym-ephemera-common",
@@ -5820,6 +5837,7 @@ dependencies = [
"der 0.7.8",
"generic-array 0.14.7",
"pkcs8 0.10.2",
"serdect",
"subtle 2.4.1",
"zeroize",
]
@@ -6145,6 +6163,16 @@ dependencies = [
"syn 2.0.28",
]
[[package]]
name = "serdect"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177"
dependencies = [
"base16ct 0.2.0",
"serde",
]
[[package]]
name = "serialize-to-javascript"
version = "0.1.1"
@@ -6221,6 +6249,12 @@ dependencies = [
"lazy_static",
]
[[package]]
name = "si-scale"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44beb68bf488343b13ddbd74d1d5d5e6559a58b6dfaee74eb8d5ed4f7ed7666f"
[[package]]
name = "signal-hook"
version = "0.3.17"
@@ -1,4 +1,4 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// Copyright 2023-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::config;
@@ -11,25 +11,18 @@ use crate::rewarder::nyxd_client::NyxdClient;
use bip39::rand::prelude::SliceRandom;
use bip39::rand::thread_rng;
use nym_coconut::{
hash_to_scalar, verify_partial_blind_signature, Base58, G1Projective, Parameters,
VerificationKey,
hash_to_scalar, verify_partial_blind_signature, Base58, G1Projective, VerificationKey,
};
use nym_coconut_dkg_common::types::EpochId;
use nym_credentials::coconut::bandwidth::BandwidthVoucher;
use nym_credentials::coconut::bandwidth::bandwidth_credential_params;
use nym_task::TaskClient;
use nym_validator_client::nym_api::{IssuedCredential, IssuedCredentialBody, NymApiClientExt};
use nym_validator_client::nyxd::Hash;
use std::cmp::max;
use std::collections::HashMap;
use std::sync::OnceLock;
use tokio::time::interval;
use tracing::{debug, error, info, instrument, trace, warn};
pub(crate) fn bandwidth_voucher_params() -> &'static Parameters {
static BANDWIDTH_CREDENTIAL_PARAMS: OnceLock<Parameters> = OnceLock::new();
BANDWIDTH_CREDENTIAL_PARAMS.get_or_init(BandwidthVoucher::default_parameters)
}
pub struct CredentialIssuanceMonitor {
nyxd_client: NyxdClient,
monitoring_results: MonitoringResults,
@@ -161,7 +154,7 @@ impl CredentialIssuanceMonitor {
// actually do verify the credential now
if !verify_partial_blind_signature(
bandwidth_voucher_params(),
bandwidth_credential_params(),
&public_attribute_commitments,
&attributes_refs,
&credential.blinded_partial_credential,
+34 -15
View File
@@ -750,6 +750,12 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f"
[[package]]
name = "const-str"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aca749d3d3f5b87a0d6100509879f9cf486ab510803a4a4e1001da1ff61c2bd6"
[[package]]
name = "convert_case"
version = "0.4.0"
@@ -1474,6 +1480,7 @@ dependencies = [
"digest 0.10.7",
"elliptic-curve 0.13.5",
"rfc6979 0.4.0",
"serdect",
"signature 2.1.0",
"spki 0.7.2",
]
@@ -1582,6 +1589,7 @@ dependencies = [
"pkcs8 0.10.2",
"rand_core 0.6.4",
"sec1 0.7.3",
"serdect",
"subtle 2.4.1",
"zeroize",
]
@@ -3091,8 +3099,9 @@ dependencies = [
"bs58 0.4.0",
"cosmrs 0.15.0 (git+https://github.com/jstuczyn/cosmos-rust?branch=nym-temp/all-validator-features)",
"cosmwasm-std",
"ecdsa 0.16.8",
"getset",
"nym-coconut-interface",
"nym-credentials-interface",
"nym-crypto",
"nym-mixnet-contract-common",
"nym-node-requests",
@@ -3109,6 +3118,7 @@ dependencies = [
"clap",
"clap_complete",
"clap_complete_fig",
"const-str",
"log",
"pretty_env_logger",
"schemars",
@@ -3159,17 +3169,6 @@ dependencies = [
"nym-multisig-contract-common",
]
[[package]]
name = "nym-coconut-interface"
version = "0.1.0"
dependencies = [
"bs58 0.4.0",
"getset",
"nym-coconut",
"serde",
"thiserror",
]
[[package]]
name = "nym-config"
version = "0.1.0"
@@ -3195,6 +3194,16 @@ dependencies = [
"thiserror",
]
[[package]]
name = "nym-credentials-interface"
version = "0.1.0"
dependencies = [
"bls12_381",
"nym-coconut",
"serde",
"thiserror",
]
[[package]]
name = "nym-crypto"
version = "0.4.0"
@@ -3317,6 +3326,7 @@ dependencies = [
"cfg-if",
"dotenvy",
"hex-literal",
"log",
"once_cell",
"schemars",
"serde",
@@ -3392,7 +3402,6 @@ dependencies = [
"hmac 0.12.1",
"itertools 0.11.0",
"log",
"nym-coconut-interface",
"nym-config",
"nym-crypto",
"nym-mixnet-contract-common",
@@ -3433,9 +3442,9 @@ dependencies = [
"itertools 0.10.5",
"log",
"nym-api-requests",
"nym-coconut",
"nym-coconut-bandwidth-contract-common",
"nym-coconut-dkg-common",
"nym-coconut-interface",
"nym-config",
"nym-contracts-common",
"nym-ephemera-common",
@@ -3538,7 +3547,6 @@ dependencies = [
"itertools 0.10.5",
"k256 0.13.1",
"log",
"nym-coconut-interface",
"nym-config",
"nym-contracts-common",
"nym-crypto",
@@ -4695,6 +4703,7 @@ dependencies = [
"der 0.7.8",
"generic-array 0.14.7",
"pkcs8 0.10.2",
"serdect",
"subtle 2.4.1",
"zeroize",
]
@@ -4883,6 +4892,16 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "serdect"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177"
dependencies = [
"base16ct 0.2.0",
"serde",
]
[[package]]
name = "serialize-to-javascript"
version = "0.1.1"

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