use std::cmp::Ordering;
use std::collections::{HashMap, HashSet};
use std::fmt;
use std::hash::{Hash, Hasher};
use std::str::FromStr;
use std::string::FromUtf8Error;
#[cfg(feature = "mint")]
use bitcoin::hashes::Hash as BitcoinHash;
use serde::{Deserialize, Deserializer, Serialize};
use thiserror::Error;
use unicode_normalization::UnicodeNormalization;
use super::nut02::ShortKeysetId;
#[cfg(feature = "wallet")]
use super::nut10;
#[cfg(feature = "wallet")]
use crate::amount::FeeAndAmounts;
#[cfg(feature = "wallet")]
use crate::amount::SplitTarget;
#[cfg(feature = "wallet")]
use crate::dhke::blind_message;
use crate::dhke::hash_to_curve;
use crate::nuts::nut01::PublicKey;
#[cfg(feature = "wallet")]
use crate::nuts::nut01::SecretKey;
use crate::nuts::nut11::{serde_p2pk_witness, P2PKWitness};
use crate::nuts::nut12::BlindSignatureDleq;
use crate::nuts::nut14::{serde_htlc_witness, HTLCWitness};
use crate::nuts::{Id, ProofDleq};
use crate::secret::Secret;
use crate::Amount;
pub mod token;
pub use token::{Token, TokenV3, TokenV4};
pub type Proofs = Vec<Proof>;
pub trait ProofsMethods {
fn count_by_keyset(&self) -> HashMap<Id, u64>;
fn sum_by_keyset(&self) -> HashMap<Id, Amount>;
fn total_amount(&self) -> Result<Amount, Error>;
fn ys(&self) -> Result<Vec<PublicKey>, Error>;
fn without_dleqs(&self) -> Proofs;
fn without_p2pk_e(&self) -> Proofs;
}
impl ProofsMethods for Proofs {
fn count_by_keyset(&self) -> HashMap<Id, u64> {
count_by_keyset(self.iter())
}
fn sum_by_keyset(&self) -> HashMap<Id, Amount> {
sum_by_keyset(self.iter())
}
fn total_amount(&self) -> Result<Amount, Error> {
total_amount(self.iter())
}
fn ys(&self) -> Result<Vec<PublicKey>, Error> {
ys(self.iter())
}
fn without_dleqs(&self) -> Proofs {
self.iter()
.map(|p| {
let mut p = p.clone();
p.dleq = None;
p
})
.collect()
}
fn without_p2pk_e(&self) -> Proofs {
self.iter()
.map(|p| {
let mut p = p.clone();
p.p2pk_e = None;
p
})
.collect()
}
}
impl ProofsMethods for HashSet<Proof> {
fn count_by_keyset(&self) -> HashMap<Id, u64> {
count_by_keyset(self.iter())
}
fn sum_by_keyset(&self) -> HashMap<Id, Amount> {
sum_by_keyset(self.iter())
}
fn total_amount(&self) -> Result<Amount, Error> {
total_amount(self.iter())
}
fn ys(&self) -> Result<Vec<PublicKey>, Error> {
ys(self.iter())
}
fn without_dleqs(&self) -> Proofs {
self.iter()
.map(|p| {
let mut p = p.clone();
p.dleq = None;
p
})
.collect()
}
fn without_p2pk_e(&self) -> Proofs {
self.iter()
.map(|p| {
let mut p = p.clone();
p.p2pk_e = None;
p
})
.collect()
}
}
fn count_by_keyset<'a, I: Iterator<Item = &'a Proof>>(proofs: I) -> HashMap<Id, u64> {
let mut counts = HashMap::new();
for proof in proofs {
*counts.entry(proof.keyset_id).or_insert(0) += 1;
}
counts
}
fn sum_by_keyset<'a, I: Iterator<Item = &'a Proof>>(proofs: I) -> HashMap<Id, Amount> {
let mut sums = HashMap::new();
for proof in proofs {
*sums.entry(proof.keyset_id).or_insert(Amount::ZERO) += proof.amount;
}
sums
}
fn total_amount<'a, I: Iterator<Item = &'a Proof>>(proofs: I) -> Result<Amount, Error> {
Amount::try_sum(proofs.map(|p| p.amount)).map_err(Into::into)
}
fn ys<'a, I: Iterator<Item = &'a Proof>>(proofs: I) -> Result<Vec<PublicKey>, Error> {
proofs.map(Proof::y).collect::<Result<Vec<PublicKey>, _>>()
}
#[derive(Debug, Error)]
pub enum Error {
#[error("Proofs required in token")]
ProofsRequired,
#[error("Unsupported token")]
UnsupportedToken,
#[error("Unsupported unit")]
UnsupportedUnit,
#[error("Unsupported payment method")]
UnsupportedPaymentMethod,
#[error("Duplicate proofs in token")]
DuplicateProofs,
#[error(transparent)]
SerdeJsonError(#[from] serde_json::Error),
#[error(transparent)]
Utf8ParseError(#[from] FromUtf8Error),
#[error(transparent)]
Base64Error(#[from] bitcoin::base64::DecodeError),
#[error(transparent)]
CiboriumError(#[from] ciborium::de::Error<std::io::Error>),
#[error(transparent)]
CiboriumSerError(#[from] ciborium::ser::Error<std::io::Error>),
#[error(transparent)]
Amount(#[from] crate::amount::Error),
#[error(transparent)]
Secret(#[from] crate::secret::Error),
#[error(transparent)]
DHKE(#[from] crate::dhke::Error),
#[error(transparent)]
NUT10(#[from] crate::nuts::nut10::Error),
#[error(transparent)]
NUT11(#[from] crate::nuts::nut11::Error),
#[error(transparent)]
NUT02(#[from] crate::nuts::nut02::Error),
#[cfg(feature = "wallet")]
#[error(transparent)]
NUT28(#[from] crate::nuts::nut28::Error),
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
pub struct BlindedMessage {
pub amount: Amount,
#[serde(rename = "id")]
#[cfg_attr(feature = "swagger", schema(value_type = String))]
pub keyset_id: Id,
#[serde(rename = "B_")]
#[cfg_attr(feature = "swagger", schema(value_type = String))]
pub blinded_secret: PublicKey,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub witness: Option<Witness>,
}
impl BlindedMessage {
#[inline]
pub fn new(amount: Amount, keyset_id: Id, blinded_secret: PublicKey) -> Self {
Self {
amount,
keyset_id,
blinded_secret,
witness: None,
}
}
#[inline]
pub fn witness(&mut self, witness: Witness) {
self.witness = Some(witness);
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
pub struct BlindSignature {
pub amount: Amount,
#[serde(rename = "id")]
#[cfg_attr(feature = "swagger", schema(value_type = String))]
pub keyset_id: Id,
#[serde(rename = "C_")]
#[cfg_attr(feature = "swagger", schema(value_type = String))]
pub c: PublicKey,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub dleq: Option<BlindSignatureDleq>,
}
impl Ord for BlindSignature {
fn cmp(&self, other: &Self) -> Ordering {
self.amount.cmp(&other.amount)
}
}
impl PartialOrd for BlindSignature {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(untagged)]
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
pub enum Witness {
#[serde(with = "serde_htlc_witness")]
HTLCWitness(HTLCWitness),
#[serde(with = "serde_p2pk_witness")]
P2PKWitness(P2PKWitness),
}
impl From<P2PKWitness> for Witness {
fn from(witness: P2PKWitness) -> Self {
Self::P2PKWitness(witness)
}
}
impl From<HTLCWitness> for Witness {
fn from(witness: HTLCWitness) -> Self {
Self::HTLCWitness(witness)
}
}
impl Witness {
pub fn add_signatures(&mut self, signatures: Vec<String>) {
match self {
Self::P2PKWitness(p2pk_witness) => p2pk_witness.signatures.extend(signatures),
Self::HTLCWitness(htlc_witness) => match &mut htlc_witness.signatures {
Some(sigs) => sigs.extend(signatures),
None => htlc_witness.signatures = Some(signatures),
},
}
}
pub fn signatures(&self) -> Option<Vec<String>> {
match self {
Self::P2PKWitness(witness) => Some(witness.signatures.clone()),
Self::HTLCWitness(witness) => witness.signatures.clone(),
}
}
pub fn preimage(&self) -> Option<String> {
match self {
Self::P2PKWitness(_witness) => None,
Self::HTLCWitness(witness) => Some(witness.preimage.clone()),
}
}
}
impl std::fmt::Display for Witness {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
serde_json::to_string(self).map_err(|_| std::fmt::Error)?
)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
pub struct Proof {
pub amount: Amount,
#[serde(rename = "id")]
#[cfg_attr(feature = "swagger", schema(value_type = String))]
pub keyset_id: Id,
#[cfg_attr(feature = "swagger", schema(value_type = String))]
pub secret: Secret,
#[serde(rename = "C")]
#[cfg_attr(feature = "swagger", schema(value_type = String))]
pub c: PublicKey,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub witness: Option<Witness>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub dleq: Option<ProofDleq>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub p2pk_e: Option<PublicKey>,
}
impl Proof {
pub fn new(amount: Amount, keyset_id: Id, secret: Secret, c: PublicKey) -> Self {
Proof {
amount,
keyset_id,
secret,
c,
witness: None,
dleq: None,
p2pk_e: None,
}
}
pub fn is_active(&self, active_keyset_ids: &[Id]) -> bool {
active_keyset_ids.contains(&self.keyset_id)
}
pub fn y(&self) -> Result<PublicKey, Error> {
Ok(hash_to_curve(self.secret.as_bytes())?)
}
}
impl Hash for Proof {
fn hash<H: Hasher>(&self, state: &mut H) {
self.secret.hash(state);
}
}
impl Ord for Proof {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.amount.cmp(&other.amount)
}
}
impl PartialOrd for Proof {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProofV4 {
#[serde(rename = "a")]
pub amount: Amount,
#[serde(rename = "s")]
pub secret: Secret,
#[serde(
serialize_with = "serialize_v4_pubkey",
deserialize_with = "deserialize_v4_pubkey"
)]
pub c: PublicKey,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub witness: Option<Witness>,
#[serde(rename = "d")]
pub dleq: Option<ProofDleq>,
#[serde(rename = "pe", default, skip_serializing_if = "Option::is_none")]
pub p2pk_e: Option<PublicKey>,
}
impl ProofV4 {
pub fn into_proof(&self, keyset_id: &Id) -> Proof {
Proof {
amount: self.amount,
keyset_id: *keyset_id,
secret: self.secret.clone(),
c: self.c,
witness: self.witness.clone(),
dleq: self.dleq.clone(),
p2pk_e: self.p2pk_e,
}
}
}
impl Hash for ProofV4 {
fn hash<H: Hasher>(&self, state: &mut H) {
self.secret.hash(state);
}
}
impl From<Proof> for ProofV4 {
fn from(proof: Proof) -> ProofV4 {
let Proof {
amount,
secret,
c,
witness,
dleq,
p2pk_e,
..
} = proof;
ProofV4 {
amount,
secret,
c,
witness,
dleq,
p2pk_e,
}
}
}
impl From<ProofV3> for ProofV4 {
fn from(proof: ProofV3) -> Self {
Self {
amount: proof.amount,
secret: proof.secret,
c: proof.c,
witness: proof.witness,
dleq: proof.dleq,
p2pk_e: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProofV3 {
pub amount: Amount,
#[serde(rename = "id")]
pub keyset_id: ShortKeysetId,
pub secret: Secret,
#[serde(rename = "C")]
pub c: PublicKey,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub witness: Option<Witness>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub dleq: Option<ProofDleq>,
}
impl ProofV3 {
pub fn into_proof(&self, keyset_id: &Id) -> Proof {
Proof {
amount: self.amount,
keyset_id: *keyset_id,
secret: self.secret.clone(),
c: self.c,
witness: self.witness.clone(),
dleq: self.dleq.clone(),
p2pk_e: None,
}
}
}
impl From<Proof> for ProofV3 {
fn from(proof: Proof) -> ProofV3 {
let Proof {
amount,
keyset_id,
secret,
c,
witness,
dleq,
..
} = proof;
ProofV3 {
amount,
secret,
c,
witness,
dleq,
keyset_id: keyset_id.into(),
}
}
}
impl Hash for ProofV3 {
fn hash<H: Hasher>(&self, state: &mut H) {
self.secret.hash(state);
}
}
fn serialize_v4_pubkey<S>(key: &PublicKey, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_bytes(&key.to_bytes())
}
fn deserialize_v4_pubkey<'de, D>(deserializer: D) -> Result<PublicKey, D::Error>
where
D: serde::Deserializer<'de>,
{
let bytes = Vec::<u8>::deserialize(deserializer)?;
PublicKey::from_slice(&bytes).map_err(serde::de::Error::custom)
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
pub enum CurrencyUnit {
#[default]
Sat,
Msat,
Usd,
Eur,
Auth,
Custom(String),
}
#[cfg(feature = "mint")]
impl CurrencyUnit {
#[deprecated(
since = "0.15.0",
note = "This function is outdated; use `hashed_derivation_index` instead."
)]
pub fn derivation_index(&self) -> Option<u32> {
match self {
Self::Sat => Some(0),
Self::Msat => Some(1),
Self::Usd => Some(2),
Self::Eur => Some(3),
Self::Auth => Some(4),
_ => None,
}
}
pub fn custom<S: AsRef<str>>(value: S) -> Self {
Self::Custom(normalize_custom_unit(value.as_ref()).to_uppercase())
}
pub fn hashed_derivation_index(&self) -> u32 {
use bitcoin::hashes::sha256;
let unit_str = self.to_string().to_uppercase();
let bytes = <sha256::Hash as BitcoinHash>::hash(unit_str.as_bytes());
u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) & !(1 << 31)
}
}
fn normalize_custom_unit(value: &str) -> String {
let trimmed = value.trim_matches(|c: char| matches!(c, ' ' | '\t' | '\r' | '\n'));
trimmed.nfc().collect::<String>()
}
impl FromStr for CurrencyUnit {
type Err = Error;
fn from_str(value: &str) -> Result<Self, Self::Err> {
let upper_value = value.to_uppercase();
match upper_value.as_str() {
"SAT" => Ok(Self::Sat),
"MSAT" => Ok(Self::Msat),
"USD" => Ok(Self::Usd),
"EUR" => Ok(Self::Eur),
"AUTH" => Ok(Self::Auth),
_ => Ok(Self::Custom(normalize_custom_unit(value))),
}
}
}
impl fmt::Display for CurrencyUnit {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
CurrencyUnit::Sat => "SAT",
CurrencyUnit::Msat => "MSAT",
CurrencyUnit::Usd => "USD",
CurrencyUnit::Eur => "EUR",
CurrencyUnit::Auth => "AUTH",
CurrencyUnit::Custom(unit) => unit,
};
if let Some(width) = f.width() {
write!(
f,
"{:width$}",
normalize_custom_unit(s).to_lowercase(),
width = width
)
} else {
write!(f, "{}", normalize_custom_unit(s).to_lowercase())
}
}
}
impl Serialize for CurrencyUnit {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.to_string().to_lowercase())
}
}
impl<'de> Deserialize<'de> for CurrencyUnit {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let currency: String = String::deserialize(deserializer)?;
Self::from_str(¤cy).map_err(|_| serde::de::Error::custom("Unsupported unit"))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
pub enum KnownMethod {
Bolt11,
Bolt12,
}
impl KnownMethod {
pub fn as_str(&self) -> &str {
match self {
Self::Bolt11 => "bolt11",
Self::Bolt12 => "bolt12",
}
}
}
impl fmt::Display for KnownMethod {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
impl FromStr for KnownMethod {
type Err = Error;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value.to_lowercase().as_str() {
"bolt11" => Ok(Self::Bolt11),
"bolt12" => Ok(Self::Bolt12),
_ => Err(Error::UnsupportedPaymentMethod),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
pub enum PaymentMethod {
Known(KnownMethod),
Custom(String),
}
impl PaymentMethod {
pub const BOLT11: Self = Self::Known(KnownMethod::Bolt11);
pub const BOLT12: Self = Self::Known(KnownMethod::Bolt12);
pub fn new(method: String) -> Self {
Self::from_str(&method).unwrap_or_else(|_| Self::Custom(method.to_lowercase()))
}
pub fn as_str(&self) -> &str {
match self {
Self::Known(known) => known.as_str(),
Self::Custom(custom) => custom.as_str(),
}
}
pub fn is_known(&self) -> bool {
matches!(self, Self::Known(_))
}
pub fn is_custom(&self) -> bool {
matches!(self, Self::Custom(_))
}
pub fn is_bolt11(&self) -> bool {
matches!(self, Self::Known(KnownMethod::Bolt11))
}
pub fn is_bolt12(&self) -> bool {
matches!(self, Self::Known(KnownMethod::Bolt12))
}
}
impl FromStr for PaymentMethod {
type Err = Error;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match KnownMethod::from_str(value) {
Ok(known) => Ok(Self::Known(known)),
Err(_) => Ok(Self::Custom(value.to_lowercase())),
}
}
}
impl fmt::Display for PaymentMethod {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
impl From<String> for PaymentMethod {
fn from(s: String) -> Self {
Self::from_str(&s).unwrap_or_else(|_| Self::Custom(s.to_lowercase()))
}
}
impl From<&str> for PaymentMethod {
fn from(s: &str) -> Self {
Self::from_str(s).unwrap_or_else(|_| Self::Custom(s.to_lowercase()))
}
}
impl From<KnownMethod> for PaymentMethod {
fn from(known: KnownMethod) -> Self {
Self::Known(known)
}
}
impl PartialEq<&str> for PaymentMethod {
fn eq(&self, other: &&str) -> bool {
self.as_str() == *other
}
}
impl PartialEq<str> for PaymentMethod {
fn eq(&self, other: &str) -> bool {
self.as_str() == other
}
}
impl PartialEq<PaymentMethod> for &str {
fn eq(&self, other: &PaymentMethod) -> bool {
*self == other.as_str()
}
}
impl PartialEq<KnownMethod> for PaymentMethod {
fn eq(&self, other: &KnownMethod) -> bool {
matches!(self, Self::Known(k) if k == other)
}
}
impl PartialEq<PaymentMethod> for KnownMethod {
fn eq(&self, other: &PaymentMethod) -> bool {
matches!(other, PaymentMethod::Known(k) if k == self)
}
}
impl Serialize for PaymentMethod {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for PaymentMethod {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let payment_method: String = String::deserialize(deserializer)?;
Ok(Self::from_str(&payment_method).unwrap_or(Self::Custom(payment_method)))
}
}
#[cfg(feature = "wallet")]
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct PreMint {
pub blinded_message: BlindedMessage,
pub secret: Secret,
pub r: SecretKey,
pub amount: Amount,
}
#[cfg(feature = "wallet")]
impl Ord for PreMint {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.amount.cmp(&other.amount)
}
}
#[cfg(feature = "wallet")]
impl PartialOrd for PreMint {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
#[cfg(feature = "wallet")]
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct PreMintSecrets {
pub secrets: Vec<PreMint>,
pub keyset_id: Id,
}
#[cfg(feature = "wallet")]
impl PreMintSecrets {
pub fn new(keyset_id: Id) -> Self {
Self {
secrets: Vec::new(),
keyset_id,
}
}
pub fn random(
keyset_id: Id,
amount: Amount,
amount_split_target: &SplitTarget,
fee_and_amounts: &FeeAndAmounts,
) -> Result<Self, Error> {
let amount_split = amount.split_targeted(amount_split_target, fee_and_amounts)?;
let mut output = Vec::with_capacity(amount_split.len());
for amount in amount_split {
let secret = Secret::generate();
let (blinded, r) = blind_message(&secret.to_bytes(), None)?;
let blinded_message = BlindedMessage::new(amount, keyset_id, blinded);
output.push(PreMint {
secret,
blinded_message,
r,
amount,
});
}
Ok(PreMintSecrets {
secrets: output,
keyset_id,
})
}
pub fn from_secrets(
keyset_id: Id,
amounts: Vec<Amount>,
secrets: Vec<Secret>,
) -> Result<Self, Error> {
let mut output = Vec::with_capacity(secrets.len());
for (secret, amount) in secrets.into_iter().zip(amounts) {
let (blinded, r) = blind_message(&secret.to_bytes(), None)?;
let blinded_message = BlindedMessage::new(amount, keyset_id, blinded);
output.push(PreMint {
secret,
blinded_message,
r,
amount,
});
}
Ok(PreMintSecrets {
secrets: output,
keyset_id,
})
}
pub fn blank(keyset_id: Id, fee_reserve: Amount) -> Result<Self, Error> {
let count = ((u64::from(fee_reserve) as f64).log2().ceil() as u64).max(1);
let mut output = Vec::with_capacity(count as usize);
for _i in 0..count {
let secret = Secret::generate();
let (blinded, r) = blind_message(&secret.to_bytes(), None)?;
let blinded_message = BlindedMessage::new(Amount::ZERO, keyset_id, blinded);
output.push(PreMint {
secret,
blinded_message,
r,
amount: Amount::ZERO,
})
}
Ok(PreMintSecrets {
secrets: output,
keyset_id,
})
}
#[cfg(feature = "wallet")]
pub fn with_p2bk(
keyset_id: Id,
amount: Amount,
amount_split_target: &SplitTarget,
receiver_pubkey: PublicKey,
conditions: Option<crate::nuts::nut10::Conditions>,
ephemeral_keys: &[crate::nuts::nut01::SecretKey],
fee_and_amounts: &FeeAndAmounts,
) -> Result<Self, Error> {
use crate::nuts::nut28::{blind_public_key, ecdh_kdf};
let amount_split = amount.split_targeted(amount_split_target, fee_and_amounts)?;
let mut output = Vec::with_capacity(amount_split.len());
let is_sig_all = conditions
.as_ref()
.is_some_and(|c| c.sig_flag == crate::nuts::nut11::SigFlag::SigAll);
if !is_sig_all && ephemeral_keys.len() != amount_split.len() {
return Err(Error::NUT28(
crate::nuts::nut28::Error::InvalidCanonicalSlot(255),
));
}
for (i, amount) in amount_split.into_iter().enumerate() {
let ephemeral_key = if is_sig_all {
&ephemeral_keys[0]
} else {
&ephemeral_keys[i]
};
let r0 = ecdh_kdf(ephemeral_key, &receiver_pubkey, 0).map_err(Error::NUT28)?;
let blinded_pubkey = blind_public_key(&receiver_pubkey, &r0).map_err(Error::NUT28)?;
let mut blinded_conditions = conditions.clone();
if let Some(ref mut cond) = blinded_conditions {
let mut slot_idx = 1;
if let Some(ref mut pubkeys) = cond.pubkeys {
for pk in pubkeys.iter_mut() {
let r = ecdh_kdf(ephemeral_key, pk, slot_idx).map_err(Error::NUT28)?;
*pk = blind_public_key(pk, &r).map_err(Error::NUT28)?;
slot_idx += 1;
}
}
if let Some(ref mut refund_keys) = cond.refund_keys {
for pk in refund_keys.iter_mut() {
let r = ecdh_kdf(ephemeral_key, pk, slot_idx).map_err(Error::NUT28)?;
*pk = blind_public_key(pk, &r).map_err(Error::NUT28)?;
slot_idx += 1;
}
}
}
let p2pk_conditions = crate::nuts::SpendingConditions::P2PKConditions {
data: blinded_pubkey,
conditions: blinded_conditions,
};
let secret: crate::nuts::nut10::Secret = p2pk_conditions.into();
let secret: Secret = secret.try_into()?;
let (blinded, rs) = blind_message(&secret.to_bytes(), None)?;
let blinded_message = BlindedMessage::new(amount, keyset_id, blinded);
output.push(PreMint {
secret,
blinded_message,
r: rs,
amount,
});
}
Ok(PreMintSecrets {
secrets: output,
keyset_id,
})
}
pub fn with_conditions(
keyset_id: Id,
amount: Amount,
amount_split_target: &SplitTarget,
conditions: &nut10::SpendingConditions,
fee_and_amounts: &FeeAndAmounts,
) -> Result<Self, Error> {
let amount_split = amount.split_targeted(amount_split_target, fee_and_amounts)?;
let mut output = Vec::with_capacity(amount_split.len());
for amount in amount_split {
let secret: nut10::Secret = conditions.clone().into();
let secret: Secret = secret.try_into()?;
let (blinded, r) = blind_message(&secret.to_bytes(), None)?;
let blinded_message = BlindedMessage::new(amount, keyset_id, blinded);
output.push(PreMint {
secret,
blinded_message,
r,
amount,
});
}
Ok(PreMintSecrets {
secrets: output,
keyset_id,
})
}
#[inline]
pub fn iter(&self) -> impl Iterator<Item = &PreMint> {
self.secrets.iter()
}
#[inline]
pub fn len(&self) -> usize {
self.secrets.len()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.secrets.is_empty()
}
pub fn total_amount(&self) -> Result<Amount, Error> {
Ok(Amount::try_sum(
self.secrets.iter().map(|PreMint { amount, .. }| *amount),
)?)
}
#[inline]
pub fn blinded_messages(&self) -> Vec<BlindedMessage> {
self.iter().map(|pm| pm.blinded_message.clone()).collect()
}
#[inline]
pub fn secrets(&self) -> Vec<Secret> {
self.iter().map(|pm| pm.secret.clone()).collect()
}
#[inline]
pub fn rs(&self) -> Vec<SecretKey> {
self.iter().map(|pm| pm.r.clone()).collect()
}
#[inline]
pub fn amounts(&self) -> Vec<Amount> {
self.iter().map(|pm| pm.amount).collect()
}
#[inline]
pub fn combine(&mut self, mut other: Self) {
self.secrets.append(&mut other.secrets)
}
#[inline]
pub fn sort_secrets(&mut self) {
self.secrets.sort();
}
}
#[cfg(feature = "wallet")]
impl Iterator for PreMintSecrets {
type Item = PreMint;
fn next(&mut self) -> Option<Self::Item> {
if self.secrets.is_empty() {
return None;
}
Some(self.secrets.remove(0))
}
}
#[cfg(feature = "wallet")]
impl Ord for PreMintSecrets {
fn cmp(&self, other: &Self) -> Ordering {
self.secrets.cmp(&other.secrets)
}
}
#[cfg(feature = "wallet")]
impl PartialOrd for PreMintSecrets {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
#[cfg(test)]
mod tests {
use std::collections::hash_map::DefaultHasher;
use std::collections::HashSet;
use std::hash::{Hash, Hasher};
use std::str::FromStr;
use super::*;
#[test]
fn test_proof_serialize() {
let proof = "[{\"id\":\"009a1f293253e41e\",\"amount\":2,\"secret\":\"407915bc212be61a77e3e6d2aeb4c727980bda51cd06a6afc29e2861768a7837\",\"C\":\"02bc9097997d81afb2cc7346b5e4345a9346bd2a506eb7958598a72f0cf85163ea\"},{\"id\":\"009a1f293253e41e\",\"amount\":8,\"secret\":\"fe15109314e61d7756b0f8ee0f23a624acaa3f4e042f61433c728c7057b931be\",\"C\":\"029e8e5050b890a7d6c0968db16bc1d5d5fa040ea1de284f6ec69d61299f671059\"}]";
let proof: Proofs = serde_json::from_str(proof).unwrap();
assert_eq!(
proof[0].clone().keyset_id,
Id::from_str("009a1f293253e41e").unwrap()
);
assert_eq!(proof.len(), 2);
}
#[test]
#[cfg(feature = "wallet")]
fn test_blank_blinded_messages() {
let b = PreMintSecrets::blank(
Id::from_str("009a1f293253e41e").unwrap(),
Amount::from(1000),
)
.unwrap();
assert_eq!(b.len(), 10);
let b = PreMintSecrets::blank(Id::from_str("009a1f293253e41e").unwrap(), Amount::from(1))
.unwrap();
assert_eq!(b.len(), 1);
}
#[test]
#[cfg(feature = "wallet")]
fn test_premint_secrets_accessors_and_total_amount() {
let keyset_id = Id::from_str("009a1f293253e41e").unwrap();
let amounts = vec![
Amount::from(1_u64),
Amount::from(2_u64),
Amount::from(4_u64),
];
let secrets = vec![Secret::generate(), Secret::generate(), Secret::generate()];
let premint_secrets =
PreMintSecrets::from_secrets(keyset_id, amounts.clone(), secrets.clone()).unwrap();
assert_eq!(premint_secrets.total_amount().unwrap(), Amount::from(7_u64));
assert_eq!(premint_secrets.amounts(), amounts);
assert_eq!(premint_secrets.secrets(), secrets);
let blinded_messages = premint_secrets.blinded_messages();
assert_eq!(blinded_messages.len(), 3);
assert_eq!(blinded_messages[0].amount, Amount::from(1_u64));
assert_eq!(blinded_messages[1].amount, Amount::from(2_u64));
assert_eq!(blinded_messages[2].amount, Amount::from(4_u64));
assert!(blinded_messages
.iter()
.all(|message| message.keyset_id == keyset_id));
let rs = premint_secrets.rs();
assert_eq!(rs.len(), 3);
assert_eq!(rs[0], premint_secrets.secrets[0].r);
assert_eq!(rs[1], premint_secrets.secrets[1].r);
assert_eq!(rs[2], premint_secrets.secrets[2].r);
}
#[test]
#[cfg(feature = "wallet")]
fn test_premint_secrets_combine_and_sort() {
let keyset_id = Id::from_str("009a1f293253e41e").unwrap();
let mut combined = PreMintSecrets::from_secrets(
keyset_id,
vec![Amount::from(8_u64), Amount::from(2_u64)],
vec![Secret::generate(), Secret::generate()],
)
.unwrap();
let other = PreMintSecrets::from_secrets(
keyset_id,
vec![Amount::from(4_u64), Amount::from(1_u64)],
vec![Secret::generate(), Secret::generate()],
)
.unwrap();
combined.combine(other);
assert_eq!(combined.len(), 4);
assert_eq!(
combined.amounts(),
vec![
Amount::from(8_u64),
Amount::from(2_u64),
Amount::from(4_u64),
Amount::from(1_u64)
]
);
combined.sort_secrets();
assert_eq!(
combined.amounts(),
vec![
Amount::from(1_u64),
Amount::from(2_u64),
Amount::from(4_u64),
Amount::from(8_u64)
]
);
}
#[test]
#[cfg(feature = "wallet")]
fn test_premint_secrets_iterator_next_yields_all_items() {
let keyset_id = Id::from_str("009a1f293253e41e").unwrap();
let premint_secrets = PreMintSecrets::from_secrets(
keyset_id,
vec![
Amount::from(1_u64),
Amount::from(2_u64),
Amount::from(4_u64),
],
vec![Secret::generate(), Secret::generate(), Secret::generate()],
)
.unwrap();
let expected = premint_secrets.secrets.clone();
let mut iterated = premint_secrets.clone();
assert_eq!(iterated.next(), Some(expected[0].clone()));
assert_eq!(iterated.next(), Some(expected[1].clone()));
assert_eq!(iterated.next(), Some(expected[2].clone()));
assert_eq!(iterated.next(), None);
assert!(iterated.is_empty());
}
#[test]
fn custom_unit_ser_der() {
let unit = CurrencyUnit::Custom(String::from("test"));
let serialized = serde_json::to_string(&unit).unwrap();
let deserialized: CurrencyUnit = serde_json::from_str(&serialized).unwrap();
assert_eq!(unit, deserialized)
}
#[test]
#[cfg(feature = "mint")]
fn test_currency_unit_custom_normalizes_and_stays_custom() {
let unit = CurrencyUnit::custom(" usd\n");
assert_eq!(unit, CurrencyUnit::Custom("USD".to_string()));
assert_ne!(unit, CurrencyUnit::default());
assert_eq!(unit.to_string(), "usd");
}
#[test]
fn test_currency_unit_parsing() {
assert_eq!(CurrencyUnit::from_str("sat").unwrap(), CurrencyUnit::Sat);
assert_eq!(CurrencyUnit::from_str("SAT").unwrap(), CurrencyUnit::Sat);
assert_eq!(CurrencyUnit::from_str("msat").unwrap(), CurrencyUnit::Msat);
assert_eq!(CurrencyUnit::from_str("MSAT").unwrap(), CurrencyUnit::Msat);
assert_eq!(CurrencyUnit::from_str("usd").unwrap(), CurrencyUnit::Usd);
assert_eq!(CurrencyUnit::from_str("USD").unwrap(), CurrencyUnit::Usd);
assert_eq!(CurrencyUnit::from_str("eur").unwrap(), CurrencyUnit::Eur);
assert_eq!(CurrencyUnit::from_str("EUR").unwrap(), CurrencyUnit::Eur);
assert_eq!(CurrencyUnit::from_str("auth").unwrap(), CurrencyUnit::Auth);
assert_eq!(CurrencyUnit::from_str("AUTH").unwrap(), CurrencyUnit::Auth);
assert_eq!(
CurrencyUnit::from_str("custom").unwrap(),
CurrencyUnit::Custom("custom".to_string())
);
}
#[test]
#[cfg(feature = "mint")]
fn four_bytes_hash_currency_unit() {
let unit = CurrencyUnit::Sat;
let index = unit.hashed_derivation_index();
assert_eq!(index, 1967237907);
let unit = CurrencyUnit::Msat;
let index = unit.hashed_derivation_index();
assert_eq!(index, 142929756);
let unit = CurrencyUnit::Eur;
let index = unit.hashed_derivation_index();
assert_eq!(index, 1473545324);
let unit = CurrencyUnit::Usd;
let index = unit.hashed_derivation_index();
assert_eq!(index, 577560378);
let unit = CurrencyUnit::Auth;
let index = unit.hashed_derivation_index();
assert_eq!(index, 1222349093)
}
#[test]
fn test_payment_method_parsing() {
assert_eq!(
PaymentMethod::from_str("bolt11").unwrap(),
PaymentMethod::BOLT11
);
assert_eq!(
PaymentMethod::from_str("BOLT11").unwrap(),
PaymentMethod::BOLT11
);
assert_eq!(
PaymentMethod::from_str("Bolt11").unwrap(),
PaymentMethod::Known(KnownMethod::Bolt11)
);
assert_eq!(
PaymentMethod::from_str("bolt12").unwrap(),
PaymentMethod::BOLT12
);
assert_eq!(
PaymentMethod::from_str("BOLT12").unwrap(),
PaymentMethod::Known(KnownMethod::Bolt12)
);
assert_eq!(
PaymentMethod::from_str("custom").unwrap(),
PaymentMethod::Custom("custom".to_string())
);
assert_eq!(
PaymentMethod::from_str("PAYPAL").unwrap(),
PaymentMethod::Custom("paypal".to_string())
);
assert_eq!(PaymentMethod::BOLT11.as_str(), "bolt11");
assert_eq!(PaymentMethod::BOLT12.as_str(), "bolt12");
assert_eq!(PaymentMethod::from("paypal").as_str(), "paypal");
assert!(PaymentMethod::BOLT11 == "bolt11");
assert!(PaymentMethod::BOLT12 == "bolt12");
assert!(PaymentMethod::Custom("paypal".to_string()) == "paypal");
assert!(PaymentMethod::BOLT11 == KnownMethod::Bolt11);
assert!(PaymentMethod::BOLT12 == KnownMethod::Bolt12);
let methods = vec![
PaymentMethod::BOLT11,
PaymentMethod::BOLT12,
PaymentMethod::Custom("test".to_string()),
];
for method in methods {
let serialized = serde_json::to_string(&method).unwrap();
let deserialized: PaymentMethod = serde_json::from_str(&serialized).unwrap();
assert_eq!(method, deserialized);
}
}
#[test]
fn test_is_bolt12_with_bolt12() {
let method = PaymentMethod::BOLT12;
assert!(method.is_bolt12());
let method = PaymentMethod::Known(KnownMethod::Bolt12);
assert!(method.is_bolt12());
}
#[test]
fn test_is_bolt12_with_non_bolt12() {
let method = PaymentMethod::BOLT11;
assert!(!method.is_bolt12());
let method = PaymentMethod::Known(KnownMethod::Bolt11);
assert!(!method.is_bolt12());
let method = PaymentMethod::Custom("paypal".to_string());
assert!(!method.is_bolt12());
let method = PaymentMethod::Custom("bolt12".to_string());
assert!(!method.is_bolt12()); }
#[test]
fn test_is_bolt12_comprehensive() {
assert!(PaymentMethod::BOLT12.is_bolt12());
assert!(PaymentMethod::Known(KnownMethod::Bolt12).is_bolt12());
assert!(!PaymentMethod::BOLT11.is_bolt12());
assert!(!PaymentMethod::Known(KnownMethod::Bolt11).is_bolt12());
assert!(!PaymentMethod::Custom("anything".to_string()).is_bolt12());
assert!(!PaymentMethod::Custom("bolt12".to_string()).is_bolt12());
}
#[test]
fn test_witness_serialization() {
let htlc_witness = HTLCWitness {
preimage: "preimage".to_string(),
signatures: Some(vec!["sig1".to_string()]),
};
let witness = Witness::HTLCWitness(htlc_witness);
let serialized = serde_json::to_string(&witness).unwrap();
let deserialized: Witness = serde_json::from_str(&serialized).unwrap();
assert!(matches!(deserialized, Witness::HTLCWitness(_)));
let p2pk_witness = P2PKWitness {
signatures: vec!["sig1".to_string(), "sig2".to_string()],
};
let witness = Witness::P2PKWitness(p2pk_witness);
let serialized = serde_json::to_string(&witness).unwrap();
let deserialized: Witness = serde_json::from_str(&serialized).unwrap();
assert!(matches!(deserialized, Witness::P2PKWitness(_)));
}
#[test]
fn test_proofs_methods_count_by_keyset() {
let proofs: Proofs = serde_json::from_str(
r#"[
{"id":"009a1f293253e41e","amount":2,"secret":"secret1","C":"02bc9097997d81afb2cc7346b5e4345a9346bd2a506eb7958598a72f0cf85163ea"},
{"id":"009a1f293253e41e","amount":8,"secret":"secret2","C":"029e8e5050b890a7d6c0968db16bc1d5d5fa040ea1de284f6ec69d61299f671059"},
{"id":"00ad268c4d1f5826","amount":4,"secret":"secret3","C":"02bc9097997d81afb2cc7346b5e4345a9346bd2a506eb7958598a72f0cf85163ea"}
]"#,
).unwrap();
let counts = proofs.count_by_keyset();
assert_eq!(counts.len(), 2);
assert_eq!(counts[&Id::from_str("009a1f293253e41e").unwrap()], 2);
assert_eq!(counts[&Id::from_str("00ad268c4d1f5826").unwrap()], 1);
}
#[test]
fn test_proofs_methods_sum_by_keyset() {
let proofs: Proofs = serde_json::from_str(
r#"[
{"id":"009a1f293253e41e","amount":2,"secret":"secret1","C":"02bc9097997d81afb2cc7346b5e4345a9346bd2a506eb7958598a72f0cf85163ea"},
{"id":"009a1f293253e41e","amount":8,"secret":"secret2","C":"029e8e5050b890a7d6c0968db16bc1d5d5fa040ea1de284f6ec69d61299f671059"},
{"id":"00ad268c4d1f5826","amount":4,"secret":"secret3","C":"02bc9097997d81afb2cc7346b5e4345a9346bd2a506eb7958598a72f0cf85163ea"}
]"#,
).unwrap();
let sums = proofs.sum_by_keyset();
assert_eq!(sums.len(), 2);
assert_eq!(
sums[&Id::from_str("009a1f293253e41e").unwrap()],
Amount::from(10)
);
assert_eq!(
sums[&Id::from_str("00ad268c4d1f5826").unwrap()],
Amount::from(4)
);
}
#[test]
fn test_proofs_methods_total_amount() {
let proofs: Proofs = serde_json::from_str(
r#"[
{"id":"009a1f293253e41e","amount":2,"secret":"secret1","C":"02bc9097997d81afb2cc7346b5e4345a9346bd2a506eb7958598a72f0cf85163ea"},
{"id":"009a1f293253e41e","amount":8,"secret":"secret2","C":"029e8e5050b890a7d6c0968db16bc1d5d5fa040ea1de284f6ec69d61299f671059"},
{"id":"00ad268c4d1f5826","amount":4,"secret":"secret3","C":"02bc9097997d81afb2cc7346b5e4345a9346bd2a506eb7958598a72f0cf85163ea"}
]"#,
).unwrap();
let total = proofs.total_amount().unwrap();
assert_eq!(total, Amount::from(14));
}
#[test]
fn test_proofs_methods_ys() {
let proofs: Proofs = serde_json::from_str(
r#"[
{"id":"009a1f293253e41e","amount":2,"secret":"secret1","C":"02bc9097997d81afb2cc7346b5e4345a9346bd2a506eb7958598a72f0cf85163ea"},
{"id":"009a1f293253e41e","amount":8,"secret":"secret2","C":"029e8e5050b890a7d6c0968db16bc1d5d5fa040ea1de284f6ec69d61299f671059"}
]"#,
).unwrap();
let ys = proofs.ys().unwrap();
assert_eq!(ys.len(), 2);
assert_ne!(ys[0], ys[1]);
}
#[test]
fn test_proofs_methods_hashset() {
let proofs: Proofs = serde_json::from_str(
r#"[
{"id":"009a1f293253e41e","amount":2,"secret":"secret1","C":"02bc9097997d81afb2cc7346b5e4345a9346bd2a506eb7958598a72f0cf85163ea"},
{"id":"009a1f293253e41e","amount":8,"secret":"secret2","C":"029e8e5050b890a7d6c0968db16bc1d5d5fa040ea1de284f6ec69d61299f671059"},
{"id":"00ad268c4d1f5826","amount":4,"secret":"secret3","C":"02bc9097997d81afb2cc7346b5e4345a9346bd2a506eb7958598a72f0cf85163ea"}
]"#,
).unwrap();
let proof_set: HashSet<Proof> = proofs.into_iter().collect();
let counts = proof_set.count_by_keyset();
assert_eq!(counts.len(), 2);
let sums = proof_set.sum_by_keyset();
assert_eq!(sums.len(), 2);
let total: u64 = sums.values().map(|a| u64::from(*a)).sum();
assert_eq!(total, 14);
}
#[test]
fn test_hashset_total_amount() {
let proofs: Proofs = serde_json::from_str(
r#"[
{"id":"009a1f293253e41e","amount":2,"secret":"secret1","C":"02bc9097997d81afb2cc7346b5e4345a9346bd2a506eb7958598a72f0cf85163ea"},
{"id":"009a1f293253e41e","amount":8,"secret":"secret2","C":"029e8e5050b890a7d6c0968db16bc1d5d5fa040ea1de284f6ec69d61299f671059"},
{"id":"00ad268c4d1f5826","amount":4,"secret":"secret3","C":"02bc9097997d81afb2cc7346b5e4345a9346bd2a506eb7958598a72f0cf85163ea"}
]"#,
)
.unwrap();
let proof_set: HashSet<Proof> = proofs.into_iter().collect();
let total = proof_set.total_amount().unwrap();
assert_eq!(total, Amount::from(14));
}
#[test]
fn test_hashset_ys() {
let proofs: Proofs = serde_json::from_str(
r#"[
{"id":"009a1f293253e41e","amount":2,"secret":"secret1","C":"02bc9097997d81afb2cc7346b5e4345a9346bd2a506eb7958598a72f0cf85163ea"},
{"id":"009a1f293253e41e","amount":8,"secret":"secret2","C":"029e8e5050b890a7d6c0968db16bc1d5d5fa040ea1de284f6ec69d61299f671059"}
]"#,
)
.unwrap();
let proof_set: HashSet<Proof> = proofs.into_iter().collect();
let ys = proof_set.ys().unwrap();
assert_eq!(ys.len(), 2);
assert_ne!(ys[0], ys[1]);
}
#[test]
fn test_hashset_without_dleqs() {
let proofs: Proofs = serde_json::from_str(
r#"[
{"id":"009a1f293253e41e","amount":2,"secret":"secret1","C":"02bc9097997d81afb2cc7346b5e4345a9346bd2a506eb7958598a72f0cf85163ea"},
{"id":"009a1f293253e41e","amount":8,"secret":"secret2","C":"029e8e5050b890a7d6c0968db16bc1d5d5fa040ea1de284f6ec69d61299f671059"}
]"#,
)
.unwrap();
let proof_set: HashSet<Proof> = proofs.into_iter().collect();
let proofs_without_dleqs = proof_set.without_dleqs();
assert_eq!(proofs_without_dleqs.len(), 2);
for proof in &proofs_without_dleqs {
assert!(proof.dleq.is_none());
}
}
#[test]
fn test_proofs_without_p2pk_e_preserves_other_fields() {
let p2pk_e = PublicKey::from_str(
"02bc9097997d81afb2cc7346b5e4345a9346bd2a506eb7958598a72f0cf85163ea",
)
.unwrap();
let mut proofs: Proofs = serde_json::from_str(
r#"[
{"id":"009a1f293253e41e","amount":2,"secret":"secret1","C":"02bc9097997d81afb2cc7346b5e4345a9346bd2a506eb7958598a72f0cf85163ea"},
{"id":"00ad268c4d1f5826","amount":8,"secret":"secret2","C":"029e8e5050b890a7d6c0968db16bc1d5d5fa040ea1de284f6ec69d61299f671059"}
]"#,
)
.unwrap();
proofs[0].p2pk_e = Some(p2pk_e);
proofs[1].p2pk_e = Some(p2pk_e);
let stripped = proofs.without_p2pk_e();
assert_eq!(stripped.len(), proofs.len());
assert!(stripped.iter().all(|proof| proof.p2pk_e.is_none()));
assert_eq!(stripped[0].amount, proofs[0].amount);
assert_eq!(stripped[0].keyset_id, proofs[0].keyset_id);
assert_eq!(stripped[0].secret, proofs[0].secret);
assert_eq!(stripped[0].c, proofs[0].c);
assert_eq!(proofs[0].p2pk_e, Some(p2pk_e));
assert_eq!(proofs[1].p2pk_e, Some(p2pk_e));
}
#[test]
fn test_hashset_without_p2pk_e_preserves_all_proofs() {
let p2pk_e = PublicKey::from_str(
"02bc9097997d81afb2cc7346b5e4345a9346bd2a506eb7958598a72f0cf85163ea",
)
.unwrap();
let mut proofs: Proofs = serde_json::from_str(
r#"[
{"id":"009a1f293253e41e","amount":2,"secret":"secret1","C":"02bc9097997d81afb2cc7346b5e4345a9346bd2a506eb7958598a72f0cf85163ea"},
{"id":"00ad268c4d1f5826","amount":8,"secret":"secret2","C":"029e8e5050b890a7d6c0968db16bc1d5d5fa040ea1de284f6ec69d61299f671059"}
]"#,
)
.unwrap();
proofs[0].p2pk_e = Some(p2pk_e);
proofs[1].p2pk_e = Some(p2pk_e);
let proof_set: HashSet<Proof> = proofs.clone().into_iter().collect();
let stripped = proof_set.without_p2pk_e();
assert_eq!(stripped.len(), proof_set.len());
assert!(stripped.iter().all(|proof| proof.p2pk_e.is_none()));
let stripped_keys: HashSet<_> = stripped.iter().map(|proof| proof.secret.clone()).collect();
let original_keys: HashSet<_> = proofs.iter().map(|proof| proof.secret.clone()).collect();
assert_eq!(stripped_keys, original_keys);
}
#[test]
#[cfg(feature = "wallet")]
fn test_with_p2bk_rejects_mismatched_ephemeral_keys_when_not_sig_all() {
use crate::amount::{FeeAndAmounts, SplitTarget};
use crate::nuts::nut11::SigFlag;
use crate::Conditions;
let keyset_id = Id::from_str("009a1f293253e41e").unwrap();
let receiver_secret_key = crate::nuts::nut01::SecretKey::generate();
let receiver_pubkey = receiver_secret_key.public_key();
let conditions =
Conditions::new(None, None, None, None, Some(SigFlag::SigInputs), None).unwrap();
let ephemeral_keys = vec![crate::nuts::nut01::SecretKey::generate()];
let fee_and_amounts = FeeAndAmounts::from((0, (0..32).map(|x| 2u64.pow(x)).collect()));
let result = PreMintSecrets::with_p2bk(
keyset_id,
Amount::from(3_u64),
&SplitTarget::default(),
receiver_pubkey,
Some(conditions),
&ephemeral_keys,
&fee_and_amounts,
);
assert!(result.is_err());
}
#[test]
#[cfg(feature = "wallet")]
fn test_with_p2bk_allows_single_ephemeral_key_for_sig_all() {
use crate::amount::{FeeAndAmounts, SplitTarget};
use crate::nuts::nut11::SigFlag;
use crate::{Conditions, SpendingConditions};
let keyset_id = Id::from_str("009a1f293253e41e").unwrap();
let receiver_secret_key = crate::nuts::nut01::SecretKey::generate();
let receiver_pubkey = receiver_secret_key.public_key();
let conditions =
Conditions::new(None, None, None, None, Some(SigFlag::SigAll), None).unwrap();
let ephemeral_key = crate::nuts::nut01::SecretKey::generate();
let fee_and_amounts = FeeAndAmounts::from((0, (0..32).map(|x| 2u64.pow(x)).collect()));
let result = PreMintSecrets::with_p2bk(
keyset_id,
Amount::from(3_u64),
&SplitTarget::default(),
receiver_pubkey,
Some(conditions),
&[ephemeral_key],
&fee_and_amounts,
)
.unwrap();
assert_eq!(result.len(), 2);
for premint in result.iter() {
let spending_conditions = SpendingConditions::try_from(&premint.secret).unwrap();
match spending_conditions {
SpendingConditions::P2PKConditions { data, conditions } => {
assert_ne!(data, receiver_pubkey);
assert_eq!(conditions.unwrap().sig_flag, SigFlag::SigAll);
}
SpendingConditions::HTLCConditions { .. } => panic!("expected P2PK conditions"),
}
}
}
#[test]
#[cfg(feature = "wallet")]
fn test_with_p2bk_allows_one_ephemeral_key_per_output_when_not_sig_all() {
use crate::amount::{FeeAndAmounts, SplitTarget};
use crate::nuts::nut11::SigFlag;
use crate::{Conditions, SpendingConditions};
let keyset_id = Id::from_str("009a1f293253e41e").unwrap();
let receiver_secret_key = crate::nuts::nut01::SecretKey::generate();
let receiver_pubkey = receiver_secret_key.public_key();
let conditions =
Conditions::new(None, None, None, None, Some(SigFlag::SigInputs), None).unwrap();
let ephemeral_keys = vec![
crate::nuts::nut01::SecretKey::generate(),
crate::nuts::nut01::SecretKey::generate(),
];
let fee_and_amounts = FeeAndAmounts::from((0, (0..32).map(|x| 2u64.pow(x)).collect()));
let result = PreMintSecrets::with_p2bk(
keyset_id,
Amount::from(3_u64),
&SplitTarget::default(),
receiver_pubkey,
Some(conditions),
&ephemeral_keys,
&fee_and_amounts,
)
.unwrap();
assert_eq!(result.len(), 2);
for premint in result.iter() {
let spending_conditions = SpendingConditions::try_from(&premint.secret).unwrap();
match spending_conditions {
SpendingConditions::P2PKConditions { data, conditions } => {
assert_ne!(data, receiver_pubkey);
assert_eq!(conditions.unwrap().sig_flag, SigFlag::SigInputs);
}
SpendingConditions::HTLCConditions { .. } => panic!("expected P2PK conditions"),
}
}
}
#[test]
#[cfg(feature = "wallet")]
fn test_with_p2bk_uses_canonical_slots_for_pubkeys_and_refund_keys() {
use crate::amount::{FeeAndAmounts, SplitTarget};
use crate::nuts::nut11::SigFlag;
use crate::nuts::nut28::{blind_public_key, ecdh_kdf};
use crate::{Conditions, SpendingConditions};
let keyset_id = Id::from_str("009a1f293253e41e").unwrap();
let receiver_secret_key = crate::nuts::nut01::SecretKey::generate();
let receiver_pubkey = receiver_secret_key.public_key();
let additional_key_1 = crate::nuts::nut01::SecretKey::generate().public_key();
let additional_key_2 = crate::nuts::nut01::SecretKey::generate().public_key();
let refund_key_1 = crate::nuts::nut01::SecretKey::generate().public_key();
let refund_key_2 = crate::nuts::nut01::SecretKey::generate().public_key();
let conditions = Conditions::new(
None,
Some(vec![additional_key_1, additional_key_2]),
Some(vec![refund_key_1, refund_key_2]),
Some(1),
Some(SigFlag::SigAll),
Some(1),
)
.unwrap();
let ephemeral_key = crate::nuts::nut01::SecretKey::generate();
let fee_and_amounts = FeeAndAmounts::from((0, (0..32).map(|x| 2u64.pow(x)).collect()));
let premint_secrets = PreMintSecrets::with_p2bk(
keyset_id,
Amount::from(1_u64),
&SplitTarget::default(),
receiver_pubkey,
Some(conditions.clone()),
std::slice::from_ref(&ephemeral_key),
&fee_and_amounts,
)
.unwrap();
let premint = premint_secrets.iter().next().unwrap();
let spending_conditions = SpendingConditions::try_from(&premint.secret).unwrap();
match spending_conditions {
SpendingConditions::P2PKConditions { data, conditions } => {
let blinded_conditions = conditions.unwrap();
let expected_primary = blind_public_key(
&receiver_pubkey,
&ecdh_kdf(&ephemeral_key, &receiver_pubkey, 0).unwrap(),
)
.unwrap();
let expected_additional = vec![
blind_public_key(
&additional_key_1,
&ecdh_kdf(&ephemeral_key, &additional_key_1, 1).unwrap(),
)
.unwrap(),
blind_public_key(
&additional_key_2,
&ecdh_kdf(&ephemeral_key, &additional_key_2, 2).unwrap(),
)
.unwrap(),
];
let expected_refund = vec![
blind_public_key(
&refund_key_1,
&ecdh_kdf(&ephemeral_key, &refund_key_1, 3).unwrap(),
)
.unwrap(),
blind_public_key(
&refund_key_2,
&ecdh_kdf(&ephemeral_key, &refund_key_2, 4).unwrap(),
)
.unwrap(),
];
assert_eq!(data, expected_primary);
assert_eq!(blinded_conditions.pubkeys.unwrap(), expected_additional);
assert_eq!(blinded_conditions.refund_keys.unwrap(), expected_refund);
assert_eq!(blinded_conditions.sig_flag, SigFlag::SigAll);
}
SpendingConditions::HTLCConditions { .. } => panic!("expected P2PK conditions"),
}
}
#[test]
fn test_blind_signature_partial_cmp() {
let sig1 = BlindSignature {
amount: Amount::from(10),
keyset_id: Id::from_str("009a1f293253e41e").unwrap(),
c: PublicKey::from_str(
"02bc9097997d81afb2cc7346b5e4345a9346bd2a506eb7958598a72f0cf85163ea",
)
.unwrap(),
dleq: None,
};
let sig2 = BlindSignature {
amount: Amount::from(20),
keyset_id: Id::from_str("009a1f293253e41e").unwrap(),
c: PublicKey::from_str(
"02bc9097997d81afb2cc7346b5e4345a9346bd2a506eb7958598a72f0cf85163ea",
)
.unwrap(),
dleq: None,
};
let sig3 = BlindSignature {
amount: Amount::from(10),
keyset_id: Id::from_str("009a1f293253e41e").unwrap(),
c: PublicKey::from_str(
"02bc9097997d81afb2cc7346b5e4345a9346bd2a506eb7958598a72f0cf85163ea",
)
.unwrap(),
dleq: None,
};
assert_eq!(sig1.partial_cmp(&sig2), Some(Ordering::Less));
assert_eq!(sig2.partial_cmp(&sig1), Some(Ordering::Greater));
assert_eq!(sig1.partial_cmp(&sig3), Some(Ordering::Equal));
let mut sigs = [sig2.clone(), sig1.clone(), sig3.clone()];
sigs.sort();
assert_eq!(sigs[0].amount, Amount::from(10));
assert_eq!(sigs[2].amount, Amount::from(20));
}
#[test]
fn test_witness_preimage() {
let htlc_witness = HTLCWitness {
preimage: "test_preimage".to_string(),
signatures: Some(vec!["sig1".to_string()]),
};
let witness = Witness::HTLCWitness(htlc_witness);
assert_eq!(witness.preimage(), Some("test_preimage".to_string()));
let p2pk_witness = P2PKWitness {
signatures: vec!["sig1".to_string()],
};
let witness = Witness::P2PKWitness(p2pk_witness);
assert_eq!(witness.preimage(), None);
}
#[test]
fn test_proof_is_active() {
let proof: Proof = serde_json::from_str(
r#"{"id":"009a1f293253e41e","amount":2,"secret":"secret1","C":"02bc9097997d81afb2cc7346b5e4345a9346bd2a506eb7958598a72f0cf85163ea"}"#,
).unwrap();
let active_keyset_id = Id::from_str("009a1f293253e41e").unwrap();
let inactive_keyset_id = Id::from_str("00ad268c4d1f5826").unwrap();
assert!(proof.is_active(&[active_keyset_id]));
assert!(!proof.is_active(&[inactive_keyset_id]));
assert!(!proof.is_active(&[]));
assert!(proof.is_active(&[inactive_keyset_id, active_keyset_id]));
}
fn compute_hash<T: Hash>(value: &T) -> u64 {
let mut hasher = DefaultHasher::new();
value.hash(&mut hasher);
hasher.finish()
}
#[test]
fn test_proof_hash_uses_secret() {
let proof1: Proof = serde_json::from_str(
r#"{"id":"009a1f293253e41e","amount":2,"secret":"same_secret","C":"02bc9097997d81afb2cc7346b5e4345a9346bd2a506eb7958598a72f0cf85163ea"}"#,
).unwrap();
let proof2: Proof = serde_json::from_str(
r#"{"id":"00ad268c4d1f5826","amount":8,"secret":"same_secret","C":"029e8e5050b890a7d6c0968db16bc1d5d5fa040ea1de284f6ec69d61299f671059"}"#,
).unwrap();
assert_eq!(compute_hash(&proof1), compute_hash(&proof2));
let proof3: Proof = serde_json::from_str(
r#"{"id":"009a1f293253e41e","amount":2,"secret":"different_secret","C":"02bc9097997d81afb2cc7346b5e4345a9346bd2a506eb7958598a72f0cf85163ea"}"#,
).unwrap();
assert_ne!(compute_hash(&proof1), compute_hash(&proof3));
}
#[test]
fn test_proof_v4_hash_uses_secret() {
let proof1: Proof = serde_json::from_str(
r#"{"id":"009a1f293253e41e","amount":2,"secret":"same_secret","C":"02bc9097997d81afb2cc7346b5e4345a9346bd2a506eb7958598a72f0cf85163ea"}"#,
).unwrap();
let proof2: Proof = serde_json::from_str(
r#"{"id":"00ad268c4d1f5826","amount":8,"secret":"same_secret","C":"029e8e5050b890a7d6c0968db16bc1d5d5fa040ea1de284f6ec69d61299f671059"}"#,
).unwrap();
let proof_v4_1: ProofV4 = proof1.into();
let proof_v4_2: ProofV4 = proof2.into();
assert_eq!(compute_hash(&proof_v4_1), compute_hash(&proof_v4_2));
let proof3: Proof = serde_json::from_str(
r#"{"id":"009a1f293253e41e","amount":2,"secret":"different_secret","C":"02bc9097997d81afb2cc7346b5e4345a9346bd2a506eb7958598a72f0cf85163ea"}"#,
).unwrap();
let proof_v4_3: ProofV4 = proof3.into();
assert_ne!(compute_hash(&proof_v4_1), compute_hash(&proof_v4_3));
}
#[test]
fn test_proof_v3_hash_uses_secret() {
let proof1: Proof = serde_json::from_str(
r#"{"id":"009a1f293253e41e","amount":2,"secret":"same_secret","C":"02bc9097997d81afb2cc7346b5e4345a9346bd2a506eb7958598a72f0cf85163ea"}"#,
).unwrap();
let proof2: Proof = serde_json::from_str(
r#"{"id":"00ad268c4d1f5826","amount":8,"secret":"same_secret","C":"029e8e5050b890a7d6c0968db16bc1d5d5fa040ea1de284f6ec69d61299f671059"}"#,
).unwrap();
let proof_v3_1: ProofV3 = proof1.into();
let proof_v3_2: ProofV3 = proof2.into();
assert_eq!(compute_hash(&proof_v3_1), compute_hash(&proof_v3_2));
let proof3: Proof = serde_json::from_str(
r#"{"id":"009a1f293253e41e","amount":2,"secret":"different_secret","C":"02bc9097997d81afb2cc7346b5e4345a9346bd2a506eb7958598a72f0cf85163ea"}"#,
).unwrap();
let proof_v3_3: ProofV3 = proof3.into();
assert_ne!(compute_hash(&proof_v3_1), compute_hash(&proof_v3_3));
}
#[test]
#[cfg(feature = "mint")]
#[allow(deprecated)]
fn test_currency_unit_derivation_index() {
assert_eq!(CurrencyUnit::Sat.derivation_index(), Some(0));
assert_eq!(CurrencyUnit::Msat.derivation_index(), Some(1));
assert_eq!(CurrencyUnit::Usd.derivation_index(), Some(2));
assert_eq!(CurrencyUnit::Eur.derivation_index(), Some(3));
assert_eq!(CurrencyUnit::Auth.derivation_index(), Some(4));
assert_eq!(
CurrencyUnit::Custom("btc".to_string()).derivation_index(),
None
);
}
}