#![doc = include_str!("./README.md")]
use std::collections::HashMap;
use std::fmt::Debug;
use std::str::FromStr;
use std::sync::Arc;
use std::time::Duration;
use bitcoin::bip32::{ChildNumber, DerivationPath, Xpriv};
use bitcoin::Network;
use cdk_common::amount::FeeAndAmounts;
use cdk_common::database::{self, WalletDatabase};
use cdk_common::parking_lot::RwLock;
use cdk_common::subscription::WalletParams;
use cdk_common::wallet::ProofInfo;
use cdk_common::{PublicKey, SecretKey, SECP256K1};
use getrandom::getrandom;
pub use mint_connector::http_client::{
AuthHttpClient as BaseAuthHttpClient, HttpClient as BaseHttpClient,
};
use subscription::{ActiveSubscription, SubscriptionManager};
use tokio::sync::RwLock as TokioRwLock;
use tracing::instrument;
use zeroize::Zeroize;
use crate::amount::SplitTarget;
use crate::dhke::construct_proofs;
use crate::error::Error;
use crate::fees::calculate_fee;
use crate::mint_url::MintUrl;
use crate::nuts::nut00::token::Token;
use crate::nuts::nut17::Kind;
use crate::nuts::{
nut10, CurrencyUnit, Id, Keys, MintInfo, MintQuoteState, PreMintSecrets, Proofs,
RestoreRequest, SpendingConditions, State,
};
use crate::wallet::mint_metadata_cache::MintMetadataCache;
use crate::wallet::p2pk::{P2PK_ACCOUNT, P2PK_PURPOSE};
use crate::Amount;
mod auth;
pub mod bip321;
#[cfg(feature = "nostr")]
mod nostr_backup;
#[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
pub use mint_connector::TorHttpClient;
mod balance;
mod builder;
mod issue;
mod keysets;
mod melt;
mod mint_connector;
mod mint_metadata_cache;
#[cfg(feature = "npubcash")]
mod npubcash;
mod p2pk;
pub mod payment_request;
mod proofs;
mod receive;
mod reclaim;
mod recovery;
pub(crate) mod saga;
mod send;
#[cfg(not(target_arch = "wasm32"))]
mod streams;
pub mod subscription;
mod swap;
pub mod test_utils;
mod transactions;
pub mod util;
pub mod wallet_repository;
mod wallet_trait;
pub use auth::{AuthMintConnector, AuthWallet};
#[cfg(all(feature = "bip353", not(target_arch = "wasm32")))]
pub use bip321::resolve_bip353_payment_instruction;
pub use bip321::{
parse_payment_instruction, Bip321UriBuilder, ParsedPaymentInstruction, PaymentRequestBip321Ext,
};
pub use builder::WalletBuilder;
pub use cdk_common::wallet as types;
pub use cdk_common::wallet::{ReceiveOptions, SendMemo, SendOptions};
pub use keysets::KeysetFilter;
pub use melt::{MeltConfirmOptions, MeltOutcome, PendingMelt, PreparedMelt};
pub use mint_connector::transport::Transport as HttpTransport;
pub use mint_connector::{
AuthHttpClient, HttpClient, LnurlPayInvoiceResponse, LnurlPayResponse, MintConnector,
};
#[cfg(feature = "nostr")]
pub use nostr_backup::{BackupOptions, BackupResult, RestoreOptions, RestoreResult};
pub use payment_request::CreateRequestParams;
#[cfg(feature = "nostr")]
pub use payment_request::NostrWaitInfo;
pub use recovery::RecoveryReport;
pub use send::PreparedSend;
#[cfg(all(feature = "npubcash", not(target_arch = "wasm32")))]
pub use streams::npubcash::NpubCashProofStream;
pub use types::{MeltQuote, MintQuote, SendKind};
pub use wallet_repository::{TokenData, WalletConfig, WalletRepository, WalletRepositoryBuilder};
use crate::nuts::nut00::ProofsMethods;
#[derive(Debug, Clone)]
pub struct Wallet {
pub mint_url: MintUrl,
pub unit: CurrencyUnit,
pub localstore: Arc<dyn WalletDatabase<database::Error> + Send + Sync>,
pub metadata_cache: Arc<MintMetadataCache>,
pub target_proof_count: usize,
metadata_cache_ttl: Arc<RwLock<Option<Duration>>>,
auth_wallet: Arc<TokioRwLock<Option<AuthWallet>>>,
#[cfg(feature = "npubcash")]
npubcash_client: Arc<TokioRwLock<Option<Arc<cdk_npubcash::NpubCashClient>>>>,
seed: [u8; 64],
client: Arc<dyn MintConnector + Send + Sync>,
subscription: SubscriptionManager,
}
const ALPHANUMERIC: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
#[derive(Debug, Clone)]
pub enum WalletSubscription {
ProofState(Vec<String>),
Bolt11MintQuoteState(Vec<String>),
Bolt11MeltQuoteState(Vec<String>),
Bolt12MeltQuoteState(Vec<String>),
Bolt12MintQuoteState(Vec<String>),
MeltQuoteCustom(String, Vec<String>),
}
impl From<WalletSubscription> for WalletParams {
fn from(val: WalletSubscription) -> Self {
let mut buffer = vec![0u8; 10];
getrandom(&mut buffer).expect("Failed to generate random bytes");
let id = Arc::new(
buffer
.iter()
.map(|&byte| {
let index = byte as usize % ALPHANUMERIC.len(); ALPHANUMERIC[index] as char
})
.collect::<String>(),
);
match val {
WalletSubscription::ProofState(filters) => WalletParams {
filters,
kind: Kind::ProofState,
id,
},
WalletSubscription::Bolt11MintQuoteState(filters) => WalletParams {
filters,
kind: Kind::Bolt11MintQuote,
id,
},
WalletSubscription::Bolt11MeltQuoteState(filters) => WalletParams {
filters,
kind: Kind::Bolt11MeltQuote,
id,
},
WalletSubscription::Bolt12MintQuoteState(filters) => WalletParams {
filters,
kind: Kind::Bolt12MintQuote,
id,
},
WalletSubscription::Bolt12MeltQuoteState(filters) => WalletParams {
filters,
kind: Kind::Bolt12MeltQuote,
id,
},
WalletSubscription::MeltQuoteCustom(method, filters) => WalletParams {
filters,
kind: Kind::Custom(format!("{}_melt_quote", method)),
id,
},
}
}
}
pub use cdk_common::wallet::Restored;
impl Wallet {
pub fn new(
mint_url: &str,
unit: CurrencyUnit,
localstore: Arc<dyn WalletDatabase<database::Error> + Send + Sync>,
seed: [u8; 64],
target_proof_count: Option<usize>,
) -> Result<Self, Error> {
let mint_url = MintUrl::from_str(mint_url)?;
WalletBuilder::new()
.mint_url(mint_url)
.unit(unit)
.localstore(localstore)
.seed(seed)
.target_proof_count(target_proof_count.unwrap_or(3))
.build()
}
pub async fn subscribe<T: Into<WalletParams>>(
&self,
query: T,
) -> Result<ActiveSubscription, Error> {
self.subscription
.subscribe(self.mint_url.clone(), query.into())
.map_err(|e| Error::SubscriptionError(e.to_string()))
}
#[instrument(skip(self, method))]
pub async fn subscribe_mint_quote_state(
&self,
quote_ids: Vec<String>,
method: cdk_common::PaymentMethod,
) -> Result<ActiveSubscription, Error> {
use cdk_common::nut00::KnownMethod;
let sub = match method {
cdk_common::PaymentMethod::Known(KnownMethod::Bolt11) => {
WalletSubscription::Bolt11MintQuoteState(quote_ids)
}
cdk_common::PaymentMethod::Known(KnownMethod::Bolt12) => {
WalletSubscription::Bolt12MintQuoteState(quote_ids)
}
cdk_common::PaymentMethod::Custom(_) => {
return Err(Error::InvalidPaymentMethod);
}
};
self.subscribe(sub).await
}
#[instrument(skip_all)]
pub async fn get_proofs_fee(
&self,
proofs: &Proofs,
) -> Result<crate::fees::ProofsFeeBreakdown, Error> {
let proofs_per_keyset = proofs.count_by_keyset();
self.get_proofs_fee_by_count(proofs_per_keyset).await
}
pub async fn get_proofs_fee_by_count(
&self,
proofs_per_keyset: HashMap<Id, u64>,
) -> Result<crate::fees::ProofsFeeBreakdown, Error> {
let mut fee_per_keyset = HashMap::new();
let metadata = self
.metadata_cache
.load(&self.localstore, &self.client, {
let ttl = self.metadata_cache_ttl.read();
*ttl
})
.await?;
for keyset_id in proofs_per_keyset.keys() {
let mint_keyset_info = metadata
.keysets
.get(keyset_id)
.ok_or(Error::UnknownKeySet)?;
fee_per_keyset.insert(*keyset_id, mint_keyset_info.input_fee_ppk);
}
let fee_breakdown = calculate_fee(&proofs_per_keyset, &fee_per_keyset)?;
Ok(fee_breakdown)
}
#[instrument(skip_all)]
pub async fn get_keyset_count_fee(&self, keyset_id: &Id, count: u64) -> Result<Amount, Error> {
let input_fee_ppk = self
.metadata_cache
.load(&self.localstore, &self.client, {
let ttl = self.metadata_cache_ttl.read();
*ttl
})
.await?
.keysets
.get(keyset_id)
.ok_or(Error::UnknownKeySet)?
.input_fee_ppk;
let fee = (input_fee_ppk * count).div_ceil(1000);
Ok(Amount::from(fee))
}
#[instrument(skip(self))]
pub async fn calculate_fee(&self, proof_count: u64, keyset_id: Id) -> Result<Amount, Error> {
self.get_keyset_count_fee(&keyset_id, proof_count).await
}
#[instrument(skip(self))]
pub async fn update_mint_url(&mut self, new_mint_url: MintUrl) -> Result<(), Error> {
self.localstore
.update_mint_url(self.mint_url.clone(), new_mint_url.clone())
.await?;
self.mint_url = new_mint_url;
Ok(())
}
#[instrument(skip(self))]
pub async fn fetch_mint_info(&self) -> Result<Option<MintInfo>, Error> {
let mint_info = self
.metadata_cache
.load_from_mint(&self.localstore, &self.client)
.await?
.mint_info
.clone();
if let Some(mint_unix_time) = mint_info.time {
let current_unix_time = crate::util::unix_time();
if current_unix_time.abs_diff(mint_unix_time) > 30 {
tracing::warn!(
"Mint time does match wallet time. Mint: {}, Wallet: {}",
mint_unix_time,
current_unix_time
);
return Err(Error::MintTimeExceedsTolerance);
}
}
{
let mut auth_wallet = self.auth_wallet.write().await;
match &*auth_wallet {
Some(auth_wallet) => {
let mut protected_endpoints = auth_wallet.protected_endpoints.write().await;
*protected_endpoints = mint_info.protected_endpoints();
if let Some(oidc_client) = mint_info
.openid_discovery()
.map(|url| crate::OidcClient::new(url, None))
{
auth_wallet.set_oidc_client(Some(oidc_client)).await;
}
}
None => {
tracing::info!("Mint has auth enabled creating auth wallet");
let oidc_client = mint_info
.openid_discovery()
.map(|url| crate::OidcClient::new(url, None));
let new_auth_wallet = AuthWallet::new(
self.mint_url.clone(),
None,
self.localstore.clone(),
self.metadata_cache.clone(),
mint_info.protected_endpoints(),
oidc_client,
);
*auth_wallet = Some(new_auth_wallet.clone());
self.client
.set_auth_wallet(Some(new_auth_wallet.clone()))
.await;
if let Err(e) = new_auth_wallet.refresh_keysets().await {
tracing::error!("Could not fetch auth keysets: {}", e);
}
}
}
}
tracing::trace!("Mint info updated for {}", self.mint_url);
Ok(Some(mint_info))
}
#[instrument(skip(self))]
pub async fn load_mint_info(&self) -> Result<MintInfo, Error> {
let mint_info = self
.metadata_cache
.load(&self.localstore, &self.client, {
let ttl = self.metadata_cache_ttl.read();
*ttl
})
.await?
.mint_info
.clone();
Ok(mint_info)
}
#[instrument(skip(self))]
pub(crate) async fn amounts_needed_for_state_target(
&self,
fee_and_amounts: &FeeAndAmounts,
) -> Result<Vec<Amount>, Error> {
let unspent_proofs = self
.get_proofs_with(Some(vec![State::Unspent]), None)
.await?;
let amounts_count: HashMap<u64, u64> =
unspent_proofs
.iter()
.fold(HashMap::new(), |mut acc, proof| {
let amount = proof.amount;
let counter = acc.entry(u64::from(amount)).or_insert(0);
*counter += 1;
acc
});
let needed_amounts =
fee_and_amounts
.amounts()
.iter()
.fold(Vec::new(), |mut acc, amount| {
let count_needed = (self.target_proof_count as u64)
.saturating_sub(*amounts_count.get(amount).unwrap_or(&0));
for _i in 0..count_needed {
acc.push(Amount::from(*amount));
}
acc
});
Ok(needed_amounts)
}
#[instrument(skip(self))]
async fn determine_split_target_values(
&self,
change_amount: Amount,
fee_and_amounts: &FeeAndAmounts,
) -> Result<SplitTarget, Error> {
let mut amounts_needed_refill = self
.amounts_needed_for_state_target(fee_and_amounts)
.await?;
amounts_needed_refill.sort();
let mut values = Vec::new();
for amount in amounts_needed_refill {
let values_sum = Amount::try_sum(values.clone().into_iter())?;
if values_sum + amount <= change_amount {
values.push(amount);
}
}
Ok(SplitTarget::Values(values))
}
#[instrument(skip(self))]
pub async fn restore(&self) -> Result<Restored, Error> {
if self
.localstore
.get_mint(self.mint_url.clone())
.await?
.is_none()
{
self.fetch_mint_info().await?;
}
let keysets = self.get_mint_keysets(KeysetFilter::All).await?;
let mut restored_result = Restored::default();
for keyset in keysets {
let keys = self.load_keyset_keys(keyset.id).await?;
let mut empty_batch = 0;
let mut start_counter = 0;
let mut highest_counter: Option<u32> = None;
while empty_batch.lt(&3) {
let premint_secrets = PreMintSecrets::restore_batch(
keyset.id,
&self.seed,
start_counter,
start_counter + 100,
)?;
tracing::debug!(
"Attempting to restore counter {}-{} for mint {} keyset {}",
start_counter,
start_counter + 100,
self.mint_url,
keyset.id
);
let restore_request = RestoreRequest {
outputs: premint_secrets.blinded_messages(),
};
let response = self.client.post_restore(restore_request).await?;
if response.signatures.is_empty() {
empty_batch += 1;
start_counter += 100;
continue;
}
let signature_map: HashMap<_, _> = response
.outputs
.iter()
.zip(response.signatures.iter())
.map(|(output, sig)| (output.blinded_secret, sig.clone()))
.collect();
let matched_secrets: Vec<_> = premint_secrets
.secrets
.iter()
.enumerate()
.filter_map(|(idx, p)| {
signature_map
.get(&p.blinded_message.blinded_secret)
.map(|sig| (idx, p, sig.clone()))
})
.collect();
if let Some(&(max_idx, _, _)) = matched_secrets.last() {
let counter_value = start_counter + max_idx as u32;
highest_counter =
Some(highest_counter.map_or(counter_value, |c| c.max(counter_value)));
}
if response.outputs.len() != matched_secrets.len() {
return Err(Error::InvalidMintResponse(format!(
"restore response outputs ({}) does not match premint secrets ({})",
response.outputs.len(),
matched_secrets.len()
)));
}
let proofs = construct_proofs(
matched_secrets
.iter()
.map(|(_, _, sig)| sig.clone())
.collect(),
matched_secrets
.iter()
.map(|(_, p, _)| p.r.clone())
.collect(),
matched_secrets
.iter()
.map(|(_, p, _)| p.secret.clone())
.collect(),
&keys,
)?;
tracing::debug!("Restored {} proofs", proofs.len());
let states = self.check_proofs_spent(proofs.clone()).await?;
let (unspent_proofs, updated_restored) = proofs
.into_iter()
.zip(states)
.filter_map(|(p, state)| {
ProofInfo::new(p, self.mint_url.clone(), state.state, keyset.unit.clone())
.ok()
})
.try_fold(
(Vec::new(), restored_result),
|(mut proofs, mut restored_result), proof_info| {
match proof_info.state {
State::Spent => {
restored_result.spent += proof_info.proof.amount;
}
State::Unspent => {
restored_result.unspent += proof_info.proof.amount;
proofs.push(proof_info);
}
State::Pending => {
restored_result.pending += proof_info.proof.amount;
proofs.push(proof_info);
}
_ => {
unreachable!("These states are unknown to the mint and cannot be returned")
}
}
Ok::<(Vec<ProofInfo>, Restored), Error>((proofs, restored_result))
},
)?;
restored_result = updated_restored;
self.localstore
.update_proofs(unspent_proofs, vec![])
.await?;
empty_batch = 0;
start_counter += 100;
}
if let Some(highest) = highest_counter {
self.localstore
.increment_keyset_counter(&keyset.id, highest + 1)
.await?;
tracing::debug!(
"Set keyset {} counter to {} after restore",
keyset.id,
highest + 1
);
}
}
Ok(restored_result)
}
#[instrument(skip(self, token))]
pub async fn verify_token_p2pk(
&self,
token: &Token,
spending_conditions: SpendingConditions,
) -> Result<(), Error> {
let (refund_keys, pubkeys, locktime, num_sigs) = match spending_conditions {
SpendingConditions::P2PKConditions { data, conditions } => {
let mut pubkeys = vec![data];
match conditions {
Some(conditions) => {
pubkeys.extend(conditions.pubkeys.unwrap_or_default());
(
conditions.refund_keys,
Some(pubkeys),
conditions.locktime,
conditions.num_sigs,
)
}
None => (None, Some(pubkeys), None, None),
}
}
SpendingConditions::HTLCConditions {
conditions,
data: _,
} => match conditions {
Some(conditions) => (
conditions.refund_keys,
conditions.pubkeys,
conditions.locktime,
conditions.num_sigs,
),
None => (None, None, None, None),
},
};
if refund_keys.is_some() && locktime.is_none() {
tracing::warn!(
"Invalid spending conditions set: Locktime must be set if refund keys are allowed"
);
return Err(Error::InvalidSpendConditions(
"Must set locktime".to_string(),
));
}
if token.mint_url()? != self.mint_url {
return Err(Error::IncorrectWallet(format!(
"Should be {} not {}",
self.mint_url,
token.mint_url()?
)));
}
let keysets_info = self.load_mint_keysets().await?;
let proofs = token.proofs(&keysets_info)?;
for proof in proofs {
let secret: nut10::Secret = (&proof.secret).try_into()?;
let proof_conditions: SpendingConditions = secret.try_into()?;
if num_sigs.ne(&proof_conditions.num_sigs()) {
tracing::debug!(
"Spending condition requires: {:?} sigs proof secret specifies: {:?}",
num_sigs,
proof_conditions.num_sigs()
);
return Err(Error::P2PKConditionsNotMet(
"Num sigs did not match spending condition".to_string(),
));
}
let spending_condition_pubkeys = pubkeys.clone().unwrap_or_default();
let proof_pubkeys = proof_conditions.pubkeys().unwrap_or_default();
if proof_pubkeys.len().ne(&spending_condition_pubkeys.len())
|| !proof_pubkeys
.iter()
.all(|pubkey| spending_condition_pubkeys.contains(pubkey))
{
tracing::debug!("Proof did not included Publickeys meeting condition");
tracing::debug!("{:?}", proof_pubkeys);
tracing::debug!("{:?}", spending_condition_pubkeys);
return Err(Error::P2PKConditionsNotMet(
"Pubkeys in proof not allowed by spending condition".to_string(),
));
}
if let Some(proof_refund_keys) = proof_conditions.refund_keys() {
let proof_locktime = proof_conditions
.locktime()
.ok_or(Error::LocktimeNotProvided)?;
if let (Some(condition_refund_keys), Some(condition_locktime)) =
(&refund_keys, locktime)
{
if proof_locktime.lt(&condition_locktime) {
return Err(Error::P2PKConditionsNotMet(
"Proof locktime less then required".to_string(),
));
}
if !condition_refund_keys.is_empty()
&& !proof_refund_keys
.iter()
.all(|refund_key| condition_refund_keys.contains(refund_key))
{
return Err(Error::P2PKConditionsNotMet(
"Refund Key not allowed".to_string(),
));
}
} else {
return Err(Error::P2PKConditionsNotMet(
"Spending condition does not allow refund keys".to_string(),
));
}
}
}
Ok(())
}
#[instrument(skip(self, token))]
pub async fn verify_token_dleq(&self, token: &Token) -> Result<(), Error> {
let mut keys_cache: HashMap<Id, Keys> = HashMap::new();
let keysets_info = self.load_mint_keysets().await?;
let proofs = token.proofs(&keysets_info)?;
for proof in proofs {
let mint_pubkey = match keys_cache.get(&proof.keyset_id) {
Some(keys) => keys.amount_key(proof.amount),
None => {
let keys = self.load_keyset_keys(proof.keyset_id).await?;
let key = keys.amount_key(proof.amount);
keys_cache.insert(proof.keyset_id, keys);
key
}
}
.ok_or(Error::AmountKey)?;
proof
.verify_dleq(mint_pubkey)
.map_err(|_| Error::CouldNotVerifyDleq)?;
}
Ok(())
}
pub fn set_client(&mut self, client: Arc<dyn MintConnector + Send + Sync>) {
self.client = client;
}
pub fn mint_connector(&self) -> Arc<dyn MintConnector + Send + Sync> {
self.client.clone()
}
pub fn set_target_proof_count(&mut self, count: usize) {
self.target_proof_count = count;
}
pub async fn generate_public_key(&self) -> Result<PublicKey, Error> {
let public_keys = self.localstore.list_p2pk_keys().await?;
let mut last_derivation_index = 0;
for public_key in public_keys {
if public_key.derivation_index >= last_derivation_index {
last_derivation_index = public_key.derivation_index + 1;
}
}
let derivation_path = DerivationPath::from(vec![
ChildNumber::from_hardened_idx(P2PK_PURPOSE)?,
ChildNumber::from_hardened_idx(P2PK_ACCOUNT)?,
ChildNumber::from_hardened_idx(0)?,
ChildNumber::from_hardened_idx(0)?,
ChildNumber::from_normal_idx(last_derivation_index)?,
]);
let pubkey = p2pk::generate_public_key(&derivation_path, &self.seed).await?;
self.localstore
.add_p2pk_key(&pubkey, derivation_path, last_derivation_index)
.await?;
Ok(pubkey)
}
pub async fn get_public_key(
&self,
pubkey: &PublicKey,
) -> Result<Option<cdk_common::wallet::P2PKSigningKey>, database::Error> {
self.localstore.get_p2pk_key(pubkey).await
}
pub async fn get_public_keys(
&self,
) -> Result<Vec<cdk_common::wallet::P2PKSigningKey>, database::Error> {
self.localstore.list_p2pk_keys().await
}
pub async fn get_latest_public_key(
&self,
) -> Result<Option<cdk_common::wallet::P2PKSigningKey>, database::Error> {
self.localstore.latest_p2pk().await
}
async fn get_signing_key(&self, pubkey: &PublicKey) -> Result<Option<SecretKey>, Error> {
let signing = self.localstore.get_p2pk_key(pubkey).await?;
if let Some(signing) = signing {
let xpriv = Xpriv::new_master(Network::Bitcoin, &self.seed)?;
return Ok(Some(SecretKey::from(
xpriv
.derive_priv(&SECP256K1, &signing.derivation_path)?
.private_key,
)));
}
Ok(None)
}
}
impl Drop for Wallet {
fn drop(&mut self) {
self.seed.zeroize();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::nuts::{BlindSignature, BlindedMessage, PreMint, PreMintSecrets};
use crate::secret::Secret;
#[test]
fn test_restore_signature_matching_in_order() {
let keyset_id = Id::from_bytes(&[0u8; 8]).unwrap();
let secret1 = Secret::generate();
let secret2 = Secret::generate();
let secret3 = Secret::generate();
let (blinded1, r1) = crate::dhke::blind_message(&secret1.to_bytes(), None).unwrap();
let (blinded2, r2) = crate::dhke::blind_message(&secret2.to_bytes(), None).unwrap();
let (blinded3, r3) = crate::dhke::blind_message(&secret3.to_bytes(), None).unwrap();
let premint1 = PreMint {
blinded_message: BlindedMessage::new(Amount::from(1), keyset_id, blinded1),
secret: secret1.clone(),
r: r1.clone(),
amount: Amount::from(1),
};
let premint2 = PreMint {
blinded_message: BlindedMessage::new(Amount::from(2), keyset_id, blinded2),
secret: secret2.clone(),
r: r2.clone(),
amount: Amount::from(2),
};
let premint3 = PreMint {
blinded_message: BlindedMessage::new(Amount::from(4), keyset_id, blinded3),
secret: secret3.clone(),
r: r3.clone(),
amount: Amount::from(4),
};
let premint_secrets = PreMintSecrets {
secrets: vec![premint1.clone(), premint2.clone(), premint3.clone()],
keyset_id,
};
let sig1 = BlindSignature {
amount: Amount::from(1),
keyset_id,
c: blinded1, dleq: None,
};
let sig2 = BlindSignature {
amount: Amount::from(2),
keyset_id,
c: blinded2,
dleq: None,
};
let sig3 = BlindSignature {
amount: Amount::from(4),
keyset_id,
c: blinded3,
dleq: None,
};
let response_outputs = [
premint1.blinded_message.clone(),
premint2.blinded_message.clone(),
premint3.blinded_message.clone(),
];
let response_signatures = [sig1.clone(), sig2.clone(), sig3.clone()];
let signature_map: HashMap<_, _> = response_outputs
.iter()
.zip(response_signatures.iter())
.map(|(output, sig)| (output.blinded_secret, sig.clone()))
.collect();
let matched_secrets: Vec<_> = premint_secrets
.secrets
.iter()
.enumerate()
.filter_map(|(idx, p)| {
signature_map
.get(&p.blinded_message.blinded_secret)
.map(|sig| (idx, p, sig.clone()))
})
.collect();
assert_eq!(matched_secrets.len(), 3);
assert_eq!(matched_secrets[0].2.amount, Amount::from(1));
assert_eq!(matched_secrets[1].2.amount, Amount::from(2));
assert_eq!(matched_secrets[2].2.amount, Amount::from(4));
assert_eq!(matched_secrets[0].0, 0);
assert_eq!(matched_secrets[1].0, 1);
assert_eq!(matched_secrets[2].0, 2);
}
#[test]
fn test_restore_signature_matching_out_of_order() {
let keyset_id = Id::from_bytes(&[0u8; 8]).unwrap();
let secret1 = Secret::generate();
let secret2 = Secret::generate();
let secret3 = Secret::generate();
let (blinded1, r1) = crate::dhke::blind_message(&secret1.to_bytes(), None).unwrap();
let (blinded2, r2) = crate::dhke::blind_message(&secret2.to_bytes(), None).unwrap();
let (blinded3, r3) = crate::dhke::blind_message(&secret3.to_bytes(), None).unwrap();
let premint1 = PreMint {
blinded_message: BlindedMessage::new(Amount::from(1), keyset_id, blinded1),
secret: secret1.clone(),
r: r1.clone(),
amount: Amount::from(1),
};
let premint2 = PreMint {
blinded_message: BlindedMessage::new(Amount::from(2), keyset_id, blinded2),
secret: secret2.clone(),
r: r2.clone(),
amount: Amount::from(2),
};
let premint3 = PreMint {
blinded_message: BlindedMessage::new(Amount::from(4), keyset_id, blinded3),
secret: secret3.clone(),
r: r3.clone(),
amount: Amount::from(4),
};
let premint_secrets = PreMintSecrets {
secrets: vec![premint1.clone(), premint2.clone(), premint3.clone()],
keyset_id,
};
let sig1 = BlindSignature {
amount: Amount::from(1),
keyset_id,
c: blinded1,
dleq: None,
};
let sig2 = BlindSignature {
amount: Amount::from(2),
keyset_id,
c: blinded2,
dleq: None,
};
let sig3 = BlindSignature {
amount: Amount::from(4),
keyset_id,
c: blinded3,
dleq: None,
};
let response_outputs = [
premint3.blinded_message.clone(), premint1.blinded_message.clone(), premint2.blinded_message.clone(), ];
let response_signatures = [sig3.clone(), sig1.clone(), sig2.clone()];
let signature_map: HashMap<_, _> = response_outputs
.iter()
.zip(response_signatures.iter())
.map(|(output, sig)| (output.blinded_secret, sig.clone()))
.collect();
let matched_secrets: Vec<_> = premint_secrets
.secrets
.iter()
.enumerate()
.filter_map(|(idx, p)| {
signature_map
.get(&p.blinded_message.blinded_secret)
.map(|sig| (idx, p, sig.clone()))
})
.collect();
assert_eq!(matched_secrets.len(), 3);
assert_eq!(matched_secrets[0].0, 0); assert_eq!(matched_secrets[0].2.amount, Amount::from(1));
assert_eq!(matched_secrets[1].0, 1); assert_eq!(matched_secrets[1].2.amount, Amount::from(2));
assert_eq!(matched_secrets[2].0, 2); assert_eq!(matched_secrets[2].2.amount, Amount::from(4)); }
#[test]
fn test_restore_signature_matching_partial_response() {
let keyset_id = Id::from_bytes(&[0u8; 8]).unwrap();
let secret1 = Secret::generate();
let secret2 = Secret::generate();
let secret3 = Secret::generate();
let (blinded1, r1) = crate::dhke::blind_message(&secret1.to_bytes(), None).unwrap();
let (blinded2, r2) = crate::dhke::blind_message(&secret2.to_bytes(), None).unwrap();
let (blinded3, r3) = crate::dhke::blind_message(&secret3.to_bytes(), None).unwrap();
let premint1 = PreMint {
blinded_message: BlindedMessage::new(Amount::from(1), keyset_id, blinded1),
secret: secret1.clone(),
r: r1.clone(),
amount: Amount::from(1),
};
let premint2 = PreMint {
blinded_message: BlindedMessage::new(Amount::from(2), keyset_id, blinded2),
secret: secret2.clone(),
r: r2.clone(),
amount: Amount::from(2),
};
let premint3 = PreMint {
blinded_message: BlindedMessage::new(Amount::from(4), keyset_id, blinded3),
secret: secret3.clone(),
r: r3.clone(),
amount: Amount::from(4),
};
let premint_secrets = PreMintSecrets {
secrets: vec![premint1.clone(), premint2.clone(), premint3.clone()],
keyset_id,
};
let sig1 = BlindSignature {
amount: Amount::from(1),
keyset_id,
c: blinded1,
dleq: None,
};
let sig3 = BlindSignature {
amount: Amount::from(4),
keyset_id,
c: blinded3,
dleq: None,
};
let response_outputs = [
premint3.blinded_message.clone(),
premint1.blinded_message.clone(),
];
let response_signatures = [sig3.clone(), sig1.clone()];
let signature_map: HashMap<_, _> = response_outputs
.iter()
.zip(response_signatures.iter())
.map(|(output, sig)| (output.blinded_secret, sig.clone()))
.collect();
let matched_secrets: Vec<_> = premint_secrets
.secrets
.iter()
.enumerate()
.filter_map(|(idx, p)| {
signature_map
.get(&p.blinded_message.blinded_secret)
.map(|sig| (idx, p, sig.clone()))
})
.collect();
assert_eq!(matched_secrets.len(), 2);
assert_eq!(matched_secrets[0].0, 0); assert_eq!(matched_secrets[0].2.amount, Amount::from(1));
assert_eq!(matched_secrets[1].0, 2); assert_eq!(matched_secrets[1].2.amount, Amount::from(4));
}
}