Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3eb862790c | |||
| 7bce29e9ac | |||
| b9195ae40d | |||
| 825a138cf3 | |||
| ecf85049c6 | |||
| 0890407886 |
@@ -13,8 +13,9 @@ use error::CoconutInterfaceError;
|
||||
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,
|
||||
BlindedSerialNumber, BlindedSignature, Bytable, CoconutError, KeyPair, Parameters,
|
||||
PrivateAttribute, PublicAttribute, SecretKey, Signature, SignatureShare, Theta,
|
||||
VerificationKey,
|
||||
};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Getters, CopyGetters, Clone, PartialEq, Eq)]
|
||||
@@ -49,7 +50,11 @@ impl Credential {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn blinded_serial_number(&self) -> String {
|
||||
pub fn blinded_serial_number(&self) -> &BlindedSerialNumber {
|
||||
&self.theta.blinded_serial_number
|
||||
}
|
||||
|
||||
pub fn blinded_serial_number_bs58(&self) -> String {
|
||||
self.theta.blinded_serial_number_bs58()
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ 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::BlindedSerialNumber;
|
||||
pub use scheme::verification::Theta;
|
||||
pub use scheme::BlindedSignature;
|
||||
pub use scheme::Signature;
|
||||
|
||||
@@ -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,22 +1,21 @@
|
||||
// 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
|
||||
@@ -25,7 +24,7 @@ pub struct Theta {
|
||||
// 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
|
||||
@@ -53,15 +52,10 @@ 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..])?;
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -112,10 +106,7 @@ impl Theta {
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,7 +189,7 @@ pub fn prove_bandwidth_credential(
|
||||
|
||||
Ok(Theta {
|
||||
blinded_message,
|
||||
blinded_serial_number,
|
||||
blinded_serial_number: blinded_serial_number.into(),
|
||||
credential: signature_prime,
|
||||
pi_v,
|
||||
})
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
/*
|
||||
* 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,
|
||||
client_address_bs58 TEXT NOT NULL REFERENCES shared_keys(client_address_bs58)
|
||||
);
|
||||
@@ -19,8 +19,10 @@ use thiserror::Error;
|
||||
use tokio::io::{AsyncRead, AsyncWrite};
|
||||
use tokio_tungstenite::tungstenite::{protocol::Message, Error as WsError};
|
||||
|
||||
use std::cmp::max;
|
||||
use std::{convert::TryFrom, process, time::Duration};
|
||||
|
||||
use crate::node::client_handling::websocket::connection_handler::coconut::BANDWIDTH_PER_CREDENTIAL;
|
||||
use crate::node::{
|
||||
client_handling::{
|
||||
bandwidth::Bandwidth,
|
||||
@@ -58,6 +60,9 @@ pub(crate) enum RequestHandlingError {
|
||||
#[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,
|
||||
|
||||
@@ -229,6 +234,17 @@ where
|
||||
iv,
|
||||
)?;
|
||||
|
||||
// check if the credential hasn't been spent before
|
||||
let already_spent = self
|
||||
.inner
|
||||
.storage
|
||||
.contains_credential(credential.blinded_serial_number())
|
||||
.await?;
|
||||
if already_spent {
|
||||
return Err(RequestHandlingError::BandwidthCredentialAlreadySpent);
|
||||
}
|
||||
|
||||
// locally verify the credential
|
||||
let aggregated_verification_key = self
|
||||
.inner
|
||||
.coconut_verifier
|
||||
@@ -241,19 +257,36 @@ where
|
||||
));
|
||||
}
|
||||
|
||||
let api_clients = self
|
||||
.inner
|
||||
.coconut_verifier
|
||||
.api_clients(*credential.epoch_id())
|
||||
// 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(*credential.blinded_serial_number(), self.client.address)
|
||||
.await?;
|
||||
|
||||
self.inner
|
||||
.coconut_verifier
|
||||
.release_funds(&api_clients, &credential)
|
||||
.await?;
|
||||
// OLD CODE FOR RELEASING FUNDS
|
||||
// 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 somebody decided to use a credential with bunch of tokens in it, sure, grant them that bandwidth
|
||||
// otherwise use the default value
|
||||
let bandwidth_value = max(bandwidth.value(), BANDWIDTH_PER_CREDENTIAL);
|
||||
|
||||
if bandwidth_value > i64::MAX as u64 {
|
||||
// note that this would have represented more than 1 exabyte,
|
||||
|
||||
@@ -20,7 +20,10 @@ use std::collections::HashMap;
|
||||
use std::ops::Deref;
|
||||
use tokio::sync::{RwLock, RwLockReadGuard};
|
||||
|
||||
pub(crate) const BANDWIDTH_PER_CREDENTIAL: u64 = 1024 * 1024 * 1024; // 1GB
|
||||
|
||||
pub(crate) struct CoconutVerifier {
|
||||
#[allow(dead_code)]
|
||||
address: AccountId,
|
||||
nyxd_client: RwLock<DirectSigningHttpRpcNyxdClient>,
|
||||
|
||||
@@ -29,6 +32,8 @@ pub(crate) struct CoconutVerifier {
|
||||
|
||||
// keys never change during epochs
|
||||
master_keys: RwLock<HashMap<EpochId, VerificationKey>>,
|
||||
|
||||
#[allow(dead_code)]
|
||||
mix_denom_base: String,
|
||||
}
|
||||
|
||||
@@ -151,6 +156,7 @@ impl CoconutVerifier {
|
||||
Ok(all_coconut_api_clients(self.nyxd_client.read().await.deref(), epoch_id).await?)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn release_funds(
|
||||
&self,
|
||||
api_clients: &[CoconutApiClient],
|
||||
@@ -165,7 +171,7 @@ impl CoconutVerifier {
|
||||
credential.voucher_value().into(),
|
||||
self.mix_denom_base.clone(),
|
||||
),
|
||||
credential.blinded_serial_number(),
|
||||
credential.blinded_serial_number_bs58(),
|
||||
self.address.to_string(),
|
||||
None,
|
||||
)
|
||||
|
||||
@@ -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,51 @@ 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
|
||||
/// * `client_address_bs58`: address of the client that spent the credential
|
||||
pub(crate) async fn insert_spent_credential(
|
||||
&self,
|
||||
blinded_serial_number_bs58: &str,
|
||||
client_address_bs58: &str,
|
||||
) -> Result<(), sqlx::Error> {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO spent_credential
|
||||
(blinded_serial_number_bs58, client_address_bs58)
|
||||
VALUES (?, ?)
|
||||
"#,
|
||||
blinded_serial_number_bs58,
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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_coconut_interface::{Base58, BlindedSerialNumber};
|
||||
use nym_gateway_requests::registration::handshake::SharedKeys;
|
||||
use nym_sphinx::DestinationAddressBytes;
|
||||
use sqlx::ConnectOptions;
|
||||
@@ -134,6 +135,28 @@ 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,
|
||||
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 +327,32 @@ impl Storage for PersistentStorage {
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn insert_spent_credential(
|
||||
&self,
|
||||
blinded_serial_number: BlindedSerialNumber,
|
||||
client_address: DestinationAddressBytes,
|
||||
) -> Result<(), StorageError> {
|
||||
self.bandwidth_manager
|
||||
.insert_spent_credential(
|
||||
&blinded_serial_number.to_bs58(),
|
||||
&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 +442,19 @@ impl Storage for InMemStorage {
|
||||
) -> Result<(), StorageError> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn insert_spent_credential(
|
||||
&self,
|
||||
_blinded_serial_number: BlindedSerialNumber,
|
||||
_client_address: DestinationAddressBytes,
|
||||
) -> Result<(), StorageError> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn contains_credential(
|
||||
&self,
|
||||
_blinded_serial_number: &BlindedSerialNumber,
|
||||
) -> Result<bool, StorageError> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,11 @@ 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) client_address_bs58: String,
|
||||
}
|
||||
|
||||
@@ -113,7 +113,11 @@ 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(
|
||||
verify_credential_body
|
||||
.credential
|
||||
.blinded_serial_number_bs58(),
|
||||
)
|
||||
.await?
|
||||
.spend_credential
|
||||
.ok_or(CoconutError::InvalidCredentialStatus {
|
||||
|
||||
@@ -1787,7 +1787,7 @@ mod credential_tests {
|
||||
);
|
||||
|
||||
// Test the endpoint with no msg in the proposal action
|
||||
proposal.description = credential.blinded_serial_number();
|
||||
proposal.description = credential.blinded_serial_number_bs58();
|
||||
chain
|
||||
.lock()
|
||||
.unwrap()
|
||||
@@ -1851,7 +1851,7 @@ mod credential_tests {
|
||||
.bandwidth_contract
|
||||
.spent_credentials
|
||||
.insert(
|
||||
credential.blinded_serial_number(),
|
||||
credential.blinded_serial_number_bs58(),
|
||||
SpendCredentialResponse::new(None),
|
||||
);
|
||||
|
||||
@@ -1875,7 +1875,7 @@ 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(),
|
||||
credential.blinded_serial_number_bs58(),
|
||||
Addr::unchecked("unimportant"),
|
||||
);
|
||||
chain
|
||||
@@ -1884,7 +1884,7 @@ mod credential_tests {
|
||||
.bandwidth_contract
|
||||
.spent_credentials
|
||||
.insert(
|
||||
credential.blinded_serial_number(),
|
||||
credential.blinded_serial_number_bs58(),
|
||||
SpendCredentialResponse::new(Some(spent_credential.clone())),
|
||||
);
|
||||
let bad_credential = Credential::new(
|
||||
@@ -2019,7 +2019,7 @@ mod credential_tests {
|
||||
.bandwidth_contract
|
||||
.spent_credentials
|
||||
.insert(
|
||||
credential.blinded_serial_number(),
|
||||
credential.blinded_serial_number_bs58(),
|
||||
SpendCredentialResponse::new(Some(spent_credential)),
|
||||
);
|
||||
let response = client
|
||||
|
||||
Reference in New Issue
Block a user