Keysets V2 (#702)

---------
Co-authored-by: thesimplekid <tsk@thesimplekid.com>
This commit is contained in:
lollerfirst
2025-06-19 16:36:16 +02:00
committed by GitHub
parent ad5f29c9a6
commit c61fd3830a
27 changed files with 818 additions and 168 deletions

View File

@@ -12,6 +12,7 @@ use std::string::FromUtf8Error;
use serde::{de, Deserialize, Deserializer, Serialize}; use serde::{de, Deserialize, Deserializer, Serialize};
use thiserror::Error; use thiserror::Error;
use super::nut02::ShortKeysetId;
#[cfg(feature = "wallet")] #[cfg(feature = "wallet")]
use super::nut10; use super::nut10;
#[cfg(feature = "wallet")] #[cfg(feature = "wallet")]
@@ -183,6 +184,9 @@ pub enum Error {
/// NUT11 error /// NUT11 error
#[error(transparent)] #[error(transparent)]
NUT11(#[from] crate::nuts::nut11::Error), NUT11(#[from] crate::nuts::nut11::Error),
/// Short keyset id -> id error
#[error(transparent)]
NUT02(#[from] crate::nuts::nut02::Error),
} }
/// Blinded Message (also called `output`) /// Blinded Message (also called `output`)
@@ -434,6 +438,12 @@ impl ProofV4 {
} }
} }
impl Hash for ProofV4 {
fn hash<H: Hasher>(&self, state: &mut H) {
self.secret.hash(state);
}
}
impl From<Proof> for ProofV4 { impl From<Proof> for ProofV4 {
fn from(proof: Proof) -> ProofV4 { fn from(proof: Proof) -> ProofV4 {
let Proof { let Proof {
@@ -454,6 +464,80 @@ impl From<Proof> for ProofV4 {
} }
} }
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,
}
}
}
/// Proof v3 with short keyset id
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProofV3 {
/// Amount
pub amount: Amount,
/// Short keyset id
#[serde(rename = "id")]
pub keyset_id: ShortKeysetId,
/// Secret message
pub secret: Secret,
/// Unblinded signature
#[serde(rename = "C")]
pub c: PublicKey,
/// Witness
#[serde(skip_serializing_if = "Option::is_none")]
pub witness: Option<Witness>,
/// DLEQ Proof
#[serde(skip_serializing_if = "Option::is_none")]
pub dleq: Option<ProofDleq>,
}
impl ProofV3 {
/// [`ProofV3`] into [`Proof`]
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(),
}
}
}
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> fn serialize_v4_pubkey<S>(key: &PublicKey, serializer: S) -> Result<S::Ok, S::Error>
where where
S: serde::Serializer, S: serde::Serializer,

View File

@@ -10,11 +10,11 @@ use bitcoin::base64::engine::{general_purpose, GeneralPurpose};
use bitcoin::base64::{alphabet, Engine as _}; use bitcoin::base64::{alphabet, Engine as _};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use super::{Error, Proof, ProofV4, Proofs}; use super::{Error, Proof, ProofV3, ProofV4, Proofs};
use crate::mint_url::MintUrl; use crate::mint_url::MintUrl;
use crate::nuts::nut00::ProofsMethods; use crate::nut02::ShortKeysetId;
use crate::nuts::{CurrencyUnit, Id}; use crate::nuts::{CurrencyUnit, Id};
use crate::{ensure_cdk, Amount}; use crate::{ensure_cdk, Amount, KeySetInfo};
/// Token Enum /// Token Enum
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
@@ -66,10 +66,10 @@ impl Token {
} }
/// Proofs in [`Token`] /// Proofs in [`Token`]
pub fn proofs(&self) -> Proofs { pub fn proofs(&self, mint_keysets: &[KeySetInfo]) -> Result<Proofs, Error> {
match self { match self {
Self::TokenV3(token) => token.proofs(), Self::TokenV3(token) => token.proofs(mint_keysets),
Self::TokenV4(token) => token.proofs(), Self::TokenV4(token) => token.proofs(mint_keysets),
} }
} }
@@ -181,8 +181,8 @@ impl TryFrom<&Vec<u8>> for Token {
pub struct TokenV3Token { pub struct TokenV3Token {
/// Url of mint /// Url of mint
pub mint: MintUrl, pub mint: MintUrl,
/// [`Proofs`] /// [`Vec<ProofV3>`]
pub proofs: Proofs, pub proofs: Vec<ProofV3>,
} }
impl TokenV3Token { impl TokenV3Token {
@@ -190,7 +190,7 @@ impl TokenV3Token {
pub fn new(mint_url: MintUrl, proofs: Proofs) -> Self { pub fn new(mint_url: MintUrl, proofs: Proofs) -> Self {
Self { Self {
mint: mint_url, mint: mint_url,
proofs, proofs: proofs.into_iter().map(ProofV3::from).collect(),
} }
} }
} }
@@ -226,17 +226,21 @@ impl TokenV3 {
} }
/// Proofs /// Proofs
pub fn proofs(&self) -> Proofs { pub fn proofs(&self, mint_keysets: &[KeySetInfo]) -> Result<Proofs, Error> {
self.token let mut proofs: Proofs = vec![];
.iter() for t in self.token.iter() {
.flat_map(|token| token.proofs.clone()) for p in t.proofs.iter() {
.collect() let long_id = Id::from_short_keyset_id(&p.keyset_id, mint_keysets)?;
proofs.push(p.into_proof(&long_id));
}
}
Ok(proofs)
} }
/// Value - errors if duplicate proofs are found /// Value - errors if duplicate proofs are found
#[inline] #[inline]
pub fn value(&self) -> Result<Amount, Error> { pub fn value(&self) -> Result<Amount, Error> {
let proofs = self.proofs(); let proofs: Vec<ProofV3> = self.token.iter().flat_map(|t| t.proofs.clone()).collect();
let unique_count = proofs let unique_count = proofs
.iter() .iter()
.collect::<std::collections::HashSet<_>>() .collect::<std::collections::HashSet<_>>()
@@ -247,7 +251,12 @@ impl TokenV3 {
return Err(Error::DuplicateProofs); return Err(Error::DuplicateProofs);
} }
proofs.total_amount() Ok(Amount::try_sum(
self.token
.iter()
.map(|t| Amount::try_sum(t.proofs.iter().map(|p| p.amount)))
.collect::<Result<Vec<Amount>, _>>()?,
)?)
} }
/// Memo /// Memo
@@ -306,10 +315,27 @@ impl fmt::Display for TokenV3 {
impl From<TokenV4> for TokenV3 { impl From<TokenV4> for TokenV3 {
fn from(token: TokenV4) -> Self { fn from(token: TokenV4) -> Self {
let proofs = token.proofs(); let proofs: Vec<ProofV3> = token
.token
.into_iter()
.flat_map(|token| {
token.proofs.into_iter().map(move |p| ProofV3 {
amount: p.amount,
keyset_id: token.keyset_id.clone(),
secret: p.secret,
c: p.c,
witness: p.witness,
dleq: p.dleq,
})
})
.collect();
let token_v3_token = TokenV3Token {
mint: token.mint_url,
proofs,
};
TokenV3 { TokenV3 {
token: vec![TokenV3Token::new(token.mint_url, proofs)], token: vec![token_v3_token],
memo: token.memo, memo: token.memo,
unit: Some(token.unit), unit: Some(token.unit),
} }
@@ -335,17 +361,19 @@ pub struct TokenV4 {
impl TokenV4 { impl TokenV4 {
/// Proofs from token /// Proofs from token
pub fn proofs(&self) -> Proofs { pub fn proofs(&self, mint_keysets: &[KeySetInfo]) -> Result<Proofs, Error> {
self.token let mut proofs: Proofs = vec![];
.iter() for t in self.token.iter() {
.flat_map(|token| token.proofs.iter().map(|p| p.into_proof(&token.keyset_id))) let long_id = Id::from_short_keyset_id(&t.keyset_id, mint_keysets)?;
.collect() proofs.extend(t.proofs.iter().map(|p| p.into_proof(&long_id)));
}
Ok(proofs)
} }
/// Value - errors if duplicate proofs are found /// Value - errors if duplicate proofs are found
#[inline] #[inline]
pub fn value(&self) -> Result<Amount, Error> { pub fn value(&self) -> Result<Amount, Error> {
let proofs = self.proofs(); let proofs: Vec<ProofV4> = self.token.iter().flat_map(|t| t.proofs.clone()).collect();
let unique_count = proofs let unique_count = proofs
.iter() .iter()
.collect::<std::collections::HashSet<_>>() .collect::<std::collections::HashSet<_>>()
@@ -356,7 +384,12 @@ impl TokenV4 {
return Err(Error::DuplicateProofs); return Err(Error::DuplicateProofs);
} }
proofs.total_amount() Ok(Amount::try_sum(
self.token
.iter()
.map(|t| Amount::try_sum(t.proofs.iter().map(|p| p.amount)))
.collect::<Result<Vec<Amount>, _>>()?,
)?)
} }
/// Memo /// Memo
@@ -421,23 +454,29 @@ impl TryFrom<&Vec<u8>> for TokenV4 {
impl TryFrom<TokenV3> for TokenV4 { impl TryFrom<TokenV3> for TokenV4 {
type Error = Error; type Error = Error;
fn try_from(token: TokenV3) -> Result<Self, Self::Error> { fn try_from(token: TokenV3) -> Result<Self, Self::Error> {
let proofs = token.proofs();
let mint_urls = token.mint_urls(); let mint_urls = token.mint_urls();
let proofs: Vec<ProofV3> = token.token.into_iter().flat_map(|t| t.proofs).collect();
ensure_cdk!(mint_urls.len() == 1, Error::UnsupportedToken); ensure_cdk!(mint_urls.len() == 1, Error::UnsupportedToken);
let mint_url = mint_urls.first().ok_or(Error::UnsupportedToken)?; let mint_url = mint_urls.first().ok_or(Error::UnsupportedToken)?;
let proofs = proofs let proofs = proofs
.iter()
.fold(HashMap::new(), |mut acc, val| {
acc.entry(val.keyset_id)
.and_modify(|p: &mut Vec<Proof>| p.push(val.clone()))
.or_insert(vec![val.clone()]);
acc
})
.into_iter() .into_iter()
.map(|(id, proofs)| TokenV4Token::new(id, proofs)) .fold(
HashMap::<ShortKeysetId, Vec<ProofV4>>::new(),
|mut acc, val| {
acc.entry(val.keyset_id.clone())
.and_modify(|p: &mut Vec<ProofV4>| p.push(val.clone().into()))
.or_insert(vec![val.clone().into()]);
acc
},
)
.into_iter()
.map(|(id, proofs)| TokenV4Token {
keyset_id: id,
proofs,
})
.collect(); .collect();
Ok(TokenV4 { Ok(TokenV4 {
@@ -458,32 +497,34 @@ pub struct TokenV4Token {
serialize_with = "serialize_v4_keyset_id", serialize_with = "serialize_v4_keyset_id",
deserialize_with = "deserialize_v4_keyset_id" deserialize_with = "deserialize_v4_keyset_id"
)] )]
pub keyset_id: Id, pub keyset_id: ShortKeysetId,
/// Proofs /// Proofs
#[serde(rename = "p")] #[serde(rename = "p")]
pub proofs: Vec<ProofV4>, pub proofs: Vec<ProofV4>,
} }
fn serialize_v4_keyset_id<S>(keyset_id: &Id, serializer: S) -> Result<S::Ok, S::Error> fn serialize_v4_keyset_id<S>(keyset_id: &ShortKeysetId, serializer: S) -> Result<S::Ok, S::Error>
where where
S: serde::Serializer, S: serde::Serializer,
{ {
serializer.serialize_bytes(&keyset_id.to_bytes()) serializer.serialize_bytes(&keyset_id.to_bytes())
} }
fn deserialize_v4_keyset_id<'de, D>(deserializer: D) -> Result<Id, D::Error> fn deserialize_v4_keyset_id<'de, D>(deserializer: D) -> Result<ShortKeysetId, D::Error>
where where
D: serde::Deserializer<'de>, D: serde::Deserializer<'de>,
{ {
let bytes = Vec::<u8>::deserialize(deserializer)?; let bytes = Vec::<u8>::deserialize(deserializer)?;
Id::from_bytes(&bytes).map_err(serde::de::Error::custom) ShortKeysetId::from_bytes(&bytes).map_err(serde::de::Error::custom)
} }
impl TokenV4Token { impl TokenV4Token {
/// Create new [`TokenV4Token`] /// Create new [`TokenV4Token`]
pub fn new(keyset_id: Id, proofs: Proofs) -> Self { pub fn new(keyset_id: Id, proofs: Proofs) -> Self {
// Create a short keyset id from id
let short_id = ShortKeysetId::from(keyset_id);
Self { Self {
keyset_id, keyset_id: short_id,
proofs: proofs.into_iter().map(|p| p.into()).collect(), proofs: proofs.into_iter().map(|p| p.into()).collect(),
} }
} }
@@ -493,7 +534,10 @@ impl TokenV4Token {
mod tests { mod tests {
use std::str::FromStr; use std::str::FromStr;
use bip39::rand::{self, RngCore};
use super::*; use super::*;
use crate::dhke::hash_to_curve;
use crate::mint_url::MintUrl; use crate::mint_url::MintUrl;
use crate::secret::Secret; use crate::secret::Secret;
use crate::util::hex; use crate::util::hex;
@@ -522,7 +566,7 @@ mod tests {
); );
assert_eq!( assert_eq!(
token.token[0].keyset_id, token.token[0].keyset_id,
Id::from_str("00ad268c4d1f5826").unwrap() ShortKeysetId::from_str("00ad268c4d1f5826").unwrap()
); );
let encoded = &token.to_string(); let encoded = &token.to_string();
@@ -546,12 +590,13 @@ mod tests {
match token { match token {
Token::TokenV4(token) => { Token::TokenV4(token) => {
let tokens: Vec<Id> = token.token.iter().map(|t| t.keyset_id).collect(); let tokens: Vec<ShortKeysetId> =
token.token.iter().map(|t| t.keyset_id.clone()).collect();
assert_eq!(tokens.len(), 2); assert_eq!(tokens.len(), 2);
assert!(tokens.contains(&Id::from_str("00ffd48b8f5ecf80").unwrap())); assert!(tokens.contains(&ShortKeysetId::from_str("00ffd48b8f5ecf80").unwrap()));
assert!(tokens.contains(&Id::from_str("00ad268c4d1f5826").unwrap())); assert!(tokens.contains(&ShortKeysetId::from_str("00ad268c4d1f5826").unwrap()));
let mint_url = token.mint_url; let mint_url = token.mint_url;
@@ -584,7 +629,7 @@ mod tests {
); );
assert_eq!( assert_eq!(
token.token[0].proofs[0].clone().keyset_id, token.token[0].proofs[0].clone().keyset_id,
Id::from_str("009a1f293253e41e").unwrap() ShortKeysetId::from_str("009a1f293253e41e").unwrap()
); );
assert_eq!(token.unit.clone().unwrap(), CurrencyUnit::Sat); assert_eq!(token.unit.clone().unwrap(), CurrencyUnit::Sat);
@@ -684,4 +729,101 @@ mod tests {
assert!(result.is_ok()); assert!(result.is_ok());
assert_eq!(result.unwrap(), Amount::from(20)); assert_eq!(result.unwrap(), Amount::from(20));
} }
#[test]
fn test_token_from_proofs_with_idv2_round_trip() {
let mint_url = MintUrl::from_str("https://example.com").unwrap();
let keysets_info: Vec<KeySetInfo> = (0..10)
.map(|_| {
let mut bytes: [u8; 33] = [0u8; 33];
bytes[0] = 1u8;
rand::thread_rng().fill_bytes(&mut bytes[1..]);
let id = Id::from_bytes(&bytes).unwrap();
KeySetInfo {
id,
unit: CurrencyUnit::Sat,
active: true,
input_fee_ppk: 0,
final_expiry: None,
}
})
.collect();
let chosen_keyset_id = keysets_info[0].id;
// Make up a bunch of fake proofs
let proofs = (0..5)
.map(|_| {
let mut c_preimage: [u8; 33] = [0u8; 33];
c_preimage[0] = 1u8;
rand::thread_rng().fill_bytes(&mut c_preimage[1..]);
Proof::new(
Amount::from(1),
chosen_keyset_id,
Secret::generate(),
hash_to_curve(&c_preimage).unwrap(),
)
})
.collect();
let token = Token::new(mint_url.clone(), proofs, None, CurrencyUnit::Sat);
let token_str = token.to_string();
let token1 = Token::from_str(&token_str);
assert!(token1.is_ok());
let proofs1 = token1.unwrap().proofs(&keysets_info);
assert!(proofs1.is_ok());
//println!("{:?}", proofs1);
}
#[test]
fn test_token_proofs_with_unknown_short_keyset_id() {
let mint_url = MintUrl::from_str("https://example.com").unwrap();
let keysets_info: Vec<KeySetInfo> = (0..10)
.map(|_| {
let mut bytes: [u8; 33] = [0u8; 33];
bytes[0] = 1u8;
rand::thread_rng().fill_bytes(&mut bytes[1..]);
let id = Id::from_bytes(&bytes).unwrap();
KeySetInfo {
id,
unit: CurrencyUnit::Sat,
active: true,
input_fee_ppk: 0,
final_expiry: None,
}
})
.collect();
let chosen_keyset_id =
Id::from_str("01c352c0b47d42edb764bddf8c53d77b85f057157d92084d9d05e876251ecd8422")
.unwrap();
// Make up a bunch of fake proofs
let proofs = (0..5)
.map(|_| {
let mut c_preimage: [u8; 33] = [0u8; 33];
c_preimage[0] = 1u8;
rand::thread_rng().fill_bytes(&mut c_preimage[1..]);
Proof::new(
Amount::from(1),
chosen_keyset_id,
Secret::generate(),
hash_to_curve(&c_preimage).unwrap(),
)
})
.collect();
let token = Token::new(mint_url.clone(), proofs, None, CurrencyUnit::Sat);
let token_str = token.to_string();
let token1 = Token::from_str(&token_str);
assert!(token1.is_ok());
let proofs1 = token1.unwrap().proofs(&keysets_info);
assert!(proofs1.is_err());
}
} }

View File

@@ -42,6 +42,12 @@ pub enum Error {
/// Keyset id does not match /// Keyset id does not match
#[error("Keyset id incorrect")] #[error("Keyset id incorrect")]
IncorrectKeysetId, IncorrectKeysetId,
/// Short keyset id does not match any of the provided IDv2s
#[error("Short keyset id does not match any of the provided IDv2s")]
UnknownShortKeysetId,
/// Short keyset id is ill-formed
#[error("Short keyset id is ill-formed")]
MalformedShortKeysetId,
/// Slice Error /// Slice Error
#[error(transparent)] #[error(transparent)]
Slice(#[from] TryFromSliceError), Slice(#[from] TryFromSliceError),
@@ -51,8 +57,10 @@ pub enum Error {
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))] #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
pub enum KeySetVersion { pub enum KeySetVersion {
/// Current Version 00 /// Version 00
Version00, Version00,
/// Version 01
Version01,
} }
impl KeySetVersion { impl KeySetVersion {
@@ -60,6 +68,7 @@ impl KeySetVersion {
pub fn to_byte(&self) -> u8 { pub fn to_byte(&self) -> u8 {
match self { match self {
Self::Version00 => 0, Self::Version00 => 0,
Self::Version01 => 1,
} }
} }
@@ -67,6 +76,7 @@ impl KeySetVersion {
pub fn from_byte(byte: &u8) -> Result<Self, Error> { pub fn from_byte(byte: &u8) -> Result<Self, Error> {
match byte { match byte {
0 => Ok(Self::Version00), 0 => Ok(Self::Version00),
1 => Ok(Self::Version01),
_ => Err(Error::UnknownVersion), _ => Err(Error::UnknownVersion),
} }
} }
@@ -76,6 +86,27 @@ impl fmt::Display for KeySetVersion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
KeySetVersion::Version00 => f.write_str("00"), KeySetVersion::Version00 => f.write_str("00"),
KeySetVersion::Version01 => f.write_str("01"),
}
}
}
/// Keyset ID bytes
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
pub enum IdBytes {
/// Bytes for v1
V1([u8; 7]),
/// Bytes for v2
V2([u8; 32]),
}
impl IdBytes {
/// Convert [`IdBytes`] to [`Vec<u8>`]
pub fn to_vec(&self) -> Vec<u8> {
match self {
IdBytes::V1(bytes) => bytes.to_vec(),
IdBytes::V2(bytes) => bytes.to_vec(),
} }
} }
} }
@@ -89,12 +120,14 @@ impl fmt::Display for KeySetVersion {
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))] #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
pub struct Id { pub struct Id {
version: KeySetVersion, version: KeySetVersion,
id: [u8; Self::BYTELEN], id: IdBytes,
} }
impl Id { impl Id {
const STRLEN: usize = 14; const STRLEN_V1: usize = 14;
const BYTELEN: usize = 7; const BYTELEN_V1: usize = 7;
const STRLEN_V2: usize = 64;
const BYTELEN_V2: usize = 32;
/// [`Id`] to bytes /// [`Id`] to bytes
pub fn to_bytes(&self) -> Vec<u8> { pub fn to_bytes(&self) -> Vec<u8> {
@@ -103,18 +136,122 @@ impl Id {
/// [`Id`] from bytes /// [`Id`] from bytes
pub fn from_bytes(bytes: &[u8]) -> Result<Self, Error> { pub fn from_bytes(bytes: &[u8]) -> Result<Self, Error> {
Ok(Self { let version = KeySetVersion::from_byte(&bytes[0])?;
version: KeySetVersion::from_byte(&bytes[0])?, let id = match version {
id: bytes[1..].try_into()?, KeySetVersion::Version00 => IdBytes::V1(bytes[1..].try_into()?),
}) KeySetVersion::Version01 => IdBytes::V2(bytes[1..].try_into()?),
};
Ok(Self { version, id })
} }
/// [`Id`] as bytes /// Get the version of the keyset
pub fn as_bytes(&self) -> [u8; Self::BYTELEN + 1] { pub fn get_version(&self) -> KeySetVersion {
let mut bytes = [0u8; Self::BYTELEN + 1]; self.version
bytes[0] = self.version.to_byte(); }
bytes[1..].copy_from_slice(&self.id);
bytes /// *** V2 KEYSET ***
/// create [`Id`] v2 from keys, unit and (optionally) expiry
/// 1 - sort public keys by their amount in ascending order
/// 2 - concatenate all public keys to one byte array
/// 3 - concatenate the lowercase unit string to the byte array (e.g. "unit:sat")
/// 4 - If a final expiration is specified, convert it into a radix-10 string and concatenate it (e.g "final_expiry:1896187313")
/// 5 - HASH_SHA256 the concatenated byte array and take the first 31 bytes
/// 6 - prefix it with a keyset ID version byte
pub fn v2_from_data(map: &Keys, unit: &CurrencyUnit, expiry: Option<u64>) -> Self {
let mut keys: Vec<(&Amount, &super::PublicKey)> = map.iter().collect();
keys.sort_by_key(|(amt, _v)| *amt);
let mut pubkeys_concat: Vec<u8> = keys
.iter()
.map(|(_, pubkey)| pubkey.to_bytes())
.collect::<Vec<[u8; 33]>>()
.concat();
// Add the unit
pubkeys_concat.extend(b"unit:");
pubkeys_concat.extend(unit.to_string().to_lowercase().as_bytes());
// Add the expiration
if let Some(expiry) = expiry {
pubkeys_concat.extend(b"final_expiry:");
pubkeys_concat.extend(expiry.to_string().as_bytes());
}
let hash = Sha256::hash(&pubkeys_concat);
let hex_of_hash = hex::encode(hash.to_byte_array());
Self {
version: KeySetVersion::Version01,
id: IdBytes::V2(
hex::decode(&hex_of_hash[0..Self::STRLEN_V2])
.expect("Keys hash could not be hex decoded")
.try_into()
.expect("Invalid length of hex id"),
),
}
}
/// *** V1 VERSION ***
/// As per NUT-02:
/// 1. sort public keys by their amount in ascending order
/// 2. concatenate all public keys to one string
/// 3. HASH_SHA256 the concatenated public keys
/// 4. take the first 14 characters of the hex-encoded hash
/// 5. prefix it with a keyset ID version byte
pub fn v1_from_keys(map: &Keys) -> Self {
let mut keys: Vec<(&Amount, &super::PublicKey)> = map.iter().collect();
keys.sort_by_key(|(amt, _v)| *amt);
let pubkeys_concat: Vec<u8> = keys
.iter()
.map(|(_, pubkey)| pubkey.to_bytes())
.collect::<Vec<[u8; 33]>>()
.concat();
let hash = Sha256::hash(&pubkeys_concat);
let hex_of_hash = hex::encode(hash.to_byte_array());
Self {
version: KeySetVersion::Version00,
id: IdBytes::V1(
hex::decode(&hex_of_hash[0..Self::STRLEN_V1])
.expect("Keys hash could not be hex decoded")
.try_into()
.expect("Invalid length of hex id"),
),
}
}
/// Selects the correct IDv2 from a list of keysets and the given short-id
/// or returns the short-id in the case of v1.
pub fn from_short_keyset_id(
short_id: &ShortKeysetId,
keysets_info: &[KeySetInfo],
) -> Result<Self, Error> {
// Check prefix length
if short_id.prefix.len() < Self::BYTELEN_V1 || short_id.prefix.len() > Self::BYTELEN_V2 {
return Err(Error::MalformedShortKeysetId);
}
match short_id.version {
KeySetVersion::Version00 => {
let mut idbytes: [u8; Self::BYTELEN_V1] = [0u8; Self::BYTELEN_V1];
idbytes.copy_from_slice(&short_id.prefix[..Self::BYTELEN_V1]);
Ok(Self {
version: short_id.version,
id: IdBytes::V1(idbytes),
})
}
KeySetVersion::Version01 => {
// We return the first match or error
for keyset_info in keysets_info.iter() {
if keyset_info.id.id.to_vec()[..short_id.prefix.len()] == short_id.prefix {
return Ok(keyset_info.id);
}
}
Err(Error::UnknownShortKeysetId)
}
}
} }
} }
@@ -122,7 +259,9 @@ impl Id {
// This is a one-way function // This is a one-way function
impl From<Id> for u32 { impl From<Id> for u32 {
fn from(value: Id) -> Self { fn from(value: Id) -> Self {
let hex_bytes: [u8; 8] = value.as_bytes(); let id_bytes = value.to_bytes();
let mut hex_bytes: [u8; 8] = [0; 8];
hex_bytes.copy_from_slice(&id_bytes[..8]);
let int = u64::from_be_bytes(hex_bytes); let int = u64::from_be_bytes(hex_bytes);
@@ -132,13 +271,21 @@ impl From<Id> for u32 {
impl fmt::Display for Id { impl fmt::Display for Id {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&format!("{}{}", self.version, hex::encode(self.id))) let hex_id = match self.id {
IdBytes::V1(id) => hex::encode(id),
IdBytes::V2(id) => hex::encode(id),
};
f.write_str(&format!("{}{}", self.version, hex_id))
} }
} }
impl fmt::Debug for Id { impl fmt::Debug for Id {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str(&format!("{}{}", self.version, hex::encode(self.id))) let hex_id = match self.id {
IdBytes::V1(id) => hex::encode(id),
IdBytes::V2(id) => hex::encode(id),
};
f.write_str(&format!("{}{}", self.version, hex_id))
} }
} }
@@ -146,14 +293,26 @@ impl TryFrom<String> for Id {
type Error = Error; type Error = Error;
fn try_from(s: String) -> Result<Self, Self::Error> { fn try_from(s: String) -> Result<Self, Self::Error> {
ensure_cdk!(s.len() == 16, Error::Length); ensure_cdk!(
s.len() == Self::STRLEN_V1 + 2 || s.len() == Self::STRLEN_V2 + 2,
Error::Length
);
Ok(Self { let version: KeySetVersion = KeySetVersion::from_byte(&hex::decode(&s[..2])?[0])?;
version: KeySetVersion::from_byte(&hex::decode(&s[..2])?[0])?, let id = match version {
id: hex::decode(&s[2..])? KeySetVersion::Version00 => IdBytes::V1(
.try_into() hex::decode(&s[2..])?
.map_err(|_| Error::Length)?, .try_into()
}) .map_err(|_| Error::Length)?,
),
KeySetVersion::Version01 => IdBytes::V2(
hex::decode(&s[2..])?
.try_into()
.map_err(|_| Error::Length)?,
),
};
Ok(Self { version, id })
} }
} }
@@ -171,33 +330,88 @@ impl From<Id> for String {
} }
} }
impl From<&Keys> for Id { /// Improper prefix of the keyset ID. In case of v1, this is the whole ID.
/// As per NUT-02: /// In case of v2, this is the 8-byte prefix
/// 1. sort public keys by their amount in ascending order #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
/// 2. concatenate all public keys to one string #[serde(into = "String", try_from = "String")]
/// 3. HASH_SHA256 the concatenated public keys #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
/// 4. take the first 14 characters of the hex-encoded hash pub struct ShortKeysetId {
/// 5. prefix it with a keyset ID version byte /// The version of the short keyset
fn from(map: &Keys) -> Self { version: KeySetVersion,
let mut keys: Vec<(&Amount, &super::PublicKey)> = map.iter().collect(); /// The improper prefix of the keyset ID bytes
keys.sort_by_key(|(amt, _v)| *amt); prefix: Vec<u8>,
}
let pubkeys_concat: Vec<u8> = keys impl ShortKeysetId {
.iter() /// [`ShortKeysetId`] to bytes
.map(|(_, pubkey)| pubkey.to_bytes()) pub fn to_bytes(&self) -> Vec<u8> {
.collect::<Vec<[u8; 33]>>() [vec![self.version.to_byte()], self.prefix.clone()].concat()
.concat(); }
let hash = Sha256::hash(&pubkeys_concat); /// [`ShortKeysetId`] from bytes
let hex_of_hash = hex::encode(hash.to_byte_array()); pub fn from_bytes(bytes: &[u8]) -> Result<Self, Error> {
let version = KeySetVersion::from_byte(&bytes[0])?;
let prefix = bytes[1..].to_vec();
Ok(Self { version, prefix })
}
}
Self { impl From<Id> for ShortKeysetId {
version: KeySetVersion::Version00, fn from(id: Id) -> Self {
id: hex::decode(&hex_of_hash[0..Self::STRLEN]) let version = id.version;
.expect("Keys hash could not be hex decoded") let prefix: Vec<u8> = match id.version {
.try_into() KeySetVersion::Version00 => match id.id {
.expect("Invalid length of hex id"), IdBytes::V1(idbytes) => Vec::from(&idbytes),
} _ => panic!("Unexpected IdBytes length"),
},
KeySetVersion::Version01 => match id.id {
IdBytes::V2(idbytes) => Vec::from(&idbytes[..7]),
_ => panic!("Unexpected IdBytes length"),
},
};
Self { version, prefix }
}
}
impl fmt::Display for ShortKeysetId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let hex_id = hex::encode(&self.prefix);
f.write_str(&format!("{}{}", self.version, hex_id))
}
}
impl fmt::Debug for ShortKeysetId {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let hex_id = hex::encode(&self.prefix);
f.write_str(&format!("{}{}", self.version, hex_id))
}
}
impl TryFrom<String> for ShortKeysetId {
type Error = Error;
fn try_from(s: String) -> Result<Self, Self::Error> {
ensure_cdk!(s.len() == 16, Error::Length);
let version: KeySetVersion = KeySetVersion::from_byte(&hex::decode(&s[..2])?[0])?;
let prefix = hex::decode(&s[2..])?;
Ok(Self { version, prefix })
}
}
impl FromStr for ShortKeysetId {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::try_from(s.to_string())
}
}
impl From<ShortKeysetId> for String {
fn from(value: ShortKeysetId) -> Self {
value.to_string()
} }
} }
@@ -223,14 +437,26 @@ pub struct KeySet {
pub unit: CurrencyUnit, pub unit: CurrencyUnit,
/// Keyset [`Keys`] /// Keyset [`Keys`]
pub keys: Keys, pub keys: Keys,
/// Expiry
#[serde(skip_serializing_if = "Option::is_none")]
pub final_expiry: Option<u64>,
} }
impl KeySet { impl KeySet {
/// Verify the keyset is matches keys /// Verify the keyset id matches keys
pub fn verify_id(&self) -> Result<(), Error> { pub fn verify_id(&self) -> Result<(), Error> {
let keys_id: Id = (&self.keys).into(); match self.id.version {
KeySetVersion::Version00 => {
let keys_id: Id = Id::v1_from_keys(&self.keys);
ensure_cdk!(keys_id == self.id, Error::IncorrectKeysetId); ensure_cdk!(keys_id == self.id, Error::IncorrectKeysetId);
}
KeySetVersion::Version01 => {
let keys_id: Id = Id::v2_from_data(&self.keys, &self.unit, self.final_expiry);
ensure_cdk!(keys_id == self.id, Error::IncorrectKeysetId);
}
}
Ok(()) Ok(())
} }
@@ -243,6 +469,7 @@ impl From<MintKeySet> for KeySet {
id: keyset.id, id: keyset.id,
unit: keyset.unit, unit: keyset.unit,
keys: Keys::from(keyset.keys), keys: Keys::from(keyset.keys),
final_expiry: keyset.final_expiry,
} }
} }
} }
@@ -265,6 +492,9 @@ pub struct KeySetInfo {
default = "default_input_fee_ppk" default = "default_input_fee_ppk"
)] )]
pub input_fee_ppk: u64, pub input_fee_ppk: u64,
/// Expiry of the keyset
#[serde(skip_serializing_if = "Option::is_none")]
pub final_expiry: Option<u64>,
} }
fn deserialize_input_fee_ppk<'de, D>(deserializer: D) -> Result<u64, D::Error> fn deserialize_input_fee_ppk<'de, D>(deserializer: D) -> Result<u64, D::Error>
@@ -290,6 +520,9 @@ pub struct MintKeySet {
pub unit: CurrencyUnit, pub unit: CurrencyUnit,
/// Keyset [`MintKeys`] /// Keyset [`MintKeys`]
pub keys: MintKeys, pub keys: MintKeys,
#[serde(skip_serializing_if = "Option::is_none")]
/// Expiry [`Option<u64>`]
pub final_expiry: Option<u64>,
} }
#[cfg(feature = "mint")] #[cfg(feature = "mint")]
@@ -300,6 +533,8 @@ impl MintKeySet {
xpriv: Xpriv, xpriv: Xpriv,
unit: CurrencyUnit, unit: CurrencyUnit,
max_order: u8, max_order: u8,
final_expiry: Option<u64>,
version: KeySetVersion,
) -> Self { ) -> Self {
let mut map = BTreeMap::new(); let mut map = BTreeMap::new();
for i in 0..max_order { for i in 0..max_order {
@@ -322,10 +557,15 @@ impl MintKeySet {
} }
let keys = MintKeys::new(map); let keys = MintKeys::new(map);
let id = match version {
KeySetVersion::Version00 => Id::v1_from_keys(&keys.clone().into()),
KeySetVersion::Version01 => Id::v2_from_data(&keys.clone().into(), &unit, final_expiry),
};
Self { Self {
id: (&keys).into(), id,
unit, unit,
keys, keys,
final_expiry,
} }
} }
@@ -336,6 +576,8 @@ impl MintKeySet {
max_order: u8, max_order: u8,
currency_unit: CurrencyUnit, currency_unit: CurrencyUnit,
derivation_path: DerivationPath, derivation_path: DerivationPath,
final_expiry: Option<u64>,
version: KeySetVersion,
) -> Self { ) -> Self {
let xpriv = Xpriv::new_master(bitcoin::Network::Bitcoin, seed).expect("RNG busted"); let xpriv = Xpriv::new_master(bitcoin::Network::Bitcoin, seed).expect("RNG busted");
Self::generate( Self::generate(
@@ -345,6 +587,8 @@ impl MintKeySet {
.expect("RNG busted"), .expect("RNG busted"),
currency_unit, currency_unit,
max_order, max_order,
final_expiry,
version,
) )
} }
@@ -355,6 +599,8 @@ impl MintKeySet {
max_order: u8, max_order: u8,
currency_unit: CurrencyUnit, currency_unit: CurrencyUnit,
derivation_path: DerivationPath, derivation_path: DerivationPath,
final_expiry: Option<u64>,
version: KeySetVersion,
) -> Self { ) -> Self {
Self::generate( Self::generate(
secp, secp,
@@ -363,6 +609,8 @@ impl MintKeySet {
.expect("RNG busted"), .expect("RNG busted"),
currency_unit, currency_unit,
max_order, max_order,
final_expiry,
version,
) )
} }
} }
@@ -371,8 +619,10 @@ impl MintKeySet {
impl From<MintKeySet> for Id { impl From<MintKeySet> for Id {
fn from(keyset: MintKeySet) -> Id { fn from(keyset: MintKeySet) -> Id {
let keys: super::KeySet = keyset.into(); let keys: super::KeySet = keyset.into();
match keys.id.version {
Id::from(&keys.keys) KeySetVersion::Version00 => Id::v1_from_keys(&keys.keys),
KeySetVersion::Version01 => Id::v2_from_data(&keys.keys, &keys.unit, keys.final_expiry),
}
} }
} }
@@ -381,7 +631,7 @@ impl From<&MintKeys> for Id {
fn from(map: &MintKeys) -> Self { fn from(map: &MintKeys) -> Self {
let keys: super::Keys = map.clone().into(); let keys: super::Keys = map.clone().into();
Id::from(&keys) Id::v1_from_keys(&keys)
} }
} }
@@ -391,10 +641,11 @@ mod test {
use bitcoin::secp256k1::rand::{self, RngCore}; use bitcoin::secp256k1::rand::{self, RngCore};
use super::{KeySetInfo, Keys, KeysetResponse}; use super::{KeySetInfo, KeySetVersion, Keys, KeysetResponse, ShortKeysetId};
use crate::nuts::nut02::{Error, Id}; use crate::nuts::nut02::{Error, Id};
use crate::nuts::KeysResponse; use crate::nuts::KeysResponse;
use crate::util::hex; use crate::util::hex;
use crate::CurrencyUnit;
const SHORT_KEYSET_ID: &str = "00456a94ab4e1c46"; const SHORT_KEYSET_ID: &str = "00456a94ab4e1c46";
const SHORT_KEYSET: &str = r#" const SHORT_KEYSET: &str = r#"
@@ -482,17 +733,43 @@ mod test {
let keys: Keys = serde_json::from_str(SHORT_KEYSET).unwrap(); let keys: Keys = serde_json::from_str(SHORT_KEYSET).unwrap();
let id: Id = (&keys).into(); let id: Id = Id::v1_from_keys(&keys);
assert_eq!(id, Id::from_str(SHORT_KEYSET_ID).unwrap()); assert_eq!(id, Id::from_str(SHORT_KEYSET_ID).unwrap());
let keys: Keys = serde_json::from_str(KEYSET).unwrap(); let keys: Keys = serde_json::from_str(KEYSET).unwrap();
let id: Id = (&keys).into(); let id: Id = Id::v1_from_keys(&keys);
assert_eq!(id, Id::from_str(KEYSET_ID).unwrap()); assert_eq!(id, Id::from_str(KEYSET_ID).unwrap());
} }
#[test]
fn test_v2_deserialization_and_id_generation() {
let unit: CurrencyUnit = CurrencyUnit::from_str("sat").unwrap();
let expiry: u64 = 2059210353; // +10 years from now
let keys: Keys = serde_json::from_str(SHORT_KEYSET).unwrap();
let id_from_str =
Id::from_str("01adc013fa9d85171586660abab27579888611659d357bc86bc09cb26eee8bc035")
.unwrap();
let id = Id::v2_from_data(&keys, &unit, Some(expiry));
assert_eq!(id, id_from_str);
let keys: Keys = serde_json::from_str(KEYSET).unwrap();
let id_from_str =
Id::from_str("0125bc634e270ad7e937af5b957f8396bb627d73f6e1fd2ffe4294c26b57daf9e0")
.unwrap();
let id = Id::v2_from_data(&keys, &unit, Some(expiry));
assert_eq!(id, id_from_str);
let id = Id::v2_from_data(&keys, &unit, None);
let id_from_str =
Id::from_str("016d72f27c8d22808ad66d1959b3dab83af17e2510db7ffd57d2365d9eec3ced75")
.unwrap();
assert_eq!(id, id_from_str);
}
#[test] #[test]
fn test_deserialization_keyset_info() { fn test_deserialization_keyset_info() {
let h = r#"{"id":"009a1f293253e41e","unit":"sat","active":true}"#; let h = r#"{"id":"009a1f293253e41e","unit":"sat","active":true}"#;
@@ -519,6 +796,15 @@ mod test {
assert_eq!(864559728, id_int) assert_eq!(864559728, id_int)
} }
#[test]
fn test_v2_to_int() {
let id = Id::from_str("01adc013fa9d85171586660abab27579888611659d357bc86bc09cb26eee8bc035")
.unwrap();
let id_int = u32::from(id);
assert_eq!(2113471806, id_int);
}
#[test] #[test]
fn test_id_from_invalid_byte_length() { fn test_id_from_invalid_byte_length() {
let three_bytes = [0x01, 0x02, 0x03]; let three_bytes = [0x01, 0x02, 0x03];
@@ -548,16 +834,28 @@ mod test {
assert_eq!(keys_response.keysets.len(), 2); assert_eq!(keys_response.keysets.len(), 2);
} }
fn generate_random_id() -> Id { fn generate_random_id(version: KeySetVersion) -> Id {
let mut rand_bytes = vec![0u8; 8]; match version {
rand::thread_rng().fill_bytes(&mut rand_bytes[1..]); KeySetVersion::Version00 => {
Id::from_bytes(&rand_bytes) let mut rand_bytes = vec![0u8; 8];
.unwrap_or_else(|e| panic!("Failed to create Id from {}: {e}", hex::encode(rand_bytes))) rand::thread_rng().fill_bytes(&mut rand_bytes[1..]);
Id::from_bytes(&rand_bytes).unwrap_or_else(|e| {
panic!("Failed to create Id from {}: {e}", hex::encode(rand_bytes))
})
}
KeySetVersion::Version01 => {
let mut rand_bytes = vec![1u8; 33];
rand::thread_rng().fill_bytes(&mut rand_bytes[1..]);
Id::from_bytes(&rand_bytes).unwrap_or_else(|e| {
panic!("Failed to create Id from {}: {e}", hex::encode(rand_bytes))
})
}
}
} }
#[test] #[test]
fn test_id_serialization() { fn test_id_serialization() {
let id = generate_random_id(); let id = generate_random_id(KeySetVersion::Version00);
let id_str = id.to_string(); let id_str = id.to_string();
assert!(id_str.chars().all(|c| c.is_ascii_hexdigit())); assert!(id_str.chars().all(|c| c.is_ascii_hexdigit()));
@@ -565,6 +863,16 @@ mod test {
assert_eq!(id_str.to_lowercase(), id_str); assert_eq!(id_str.to_lowercase(), id_str);
} }
#[test]
fn test_id_v2_serialization() {
let id = generate_random_id(KeySetVersion::Version01);
let id_str = id.to_string();
assert!(id_str.chars().all(|c| c.is_ascii_hexdigit()));
assert_eq!(66, id_str.len());
assert_eq!(id_str.to_lowercase(), id_str);
}
#[test] #[test]
fn test_id_deserialization() { fn test_id_deserialization() {
let id_from_short_str = Id::from_str("00123"); let id_from_short_str = Id::from_str("00123");
@@ -579,4 +887,18 @@ mod test {
let id_from_uppercase = Id::from_str(&SHORT_KEYSET_ID.to_uppercase()); let id_from_uppercase = Id::from_str(&SHORT_KEYSET_ID.to_uppercase());
assert!(id_from_uppercase.is_ok()); assert!(id_from_uppercase.is_ok());
} }
#[test]
fn test_short_keyset_id_from_id() {
let idv1 = Id::from_str("009a1f293253e41e").unwrap();
let idv2 =
Id::from_str("01adc013fa9d85171586660abab27579888611659d357bc86bc09cb26eee8bc035")
.unwrap();
let short_id_1: ShortKeysetId = idv1.into();
let short_id_2: ShortKeysetId = idv2.into();
assert!(short_id_1.to_string() == "009a1f293253e41e");
assert!(short_id_2.to_string() == "01adc013fa9d8517");
}
} }

View File

@@ -91,7 +91,19 @@ pub async fn pay_request(
}, },
) )
.await?; .await?;
let proofs = matching_wallet.send(prepared_send, None).await?.proofs();
let token = matching_wallet.send(prepared_send, None).await?;
// We need the keysets information to properly convert from token proof to proof
let keysets_info = match matching_wallet
.localstore
.get_mint_keysets(token.mint_url()?)
.await?
{
Some(keysets_info) => keysets_info,
None => matching_wallet.get_mint_keysets().await?, // Hit the keysets endpoint if we don't have the keysets for this Mint
};
let proofs = token.proofs(&keysets_info)?;
if let Some(transport) = transport { if let Some(transport) = transport {
let payload = PaymentRequestPayload { let payload = PaymentRequestPayload {

View File

@@ -19,7 +19,7 @@ async fn setup_keyset<E: Debug, DB: Database<E> + KeysDatabase<Err = E>>(db: &DB
unit: CurrencyUnit::Sat, unit: CurrencyUnit::Sat,
active: true, active: true,
valid_from: 0, valid_from: 0,
valid_to: None, final_expiry: None,
derivation_path: bitcoin::bip32::DerivationPath::from_str("m/0'/0'/0'").unwrap(), derivation_path: bitcoin::bip32::DerivationPath::from_str("m/0'/0'/0'").unwrap(),
derivation_path_index: Some(0), derivation_path_index: Some(0),
max_order: 32, max_order: 32,

View File

@@ -4,6 +4,7 @@ use std::collections::HashMap;
use std::fmt::Debug; use std::fmt::Debug;
use async_trait::async_trait; use async_trait::async_trait;
use cashu::KeySet;
use super::Error; use super::Error;
use crate::common::ProofInfo; use crate::common::ProofInfo;
@@ -72,7 +73,7 @@ pub trait Database: Debug {
async fn remove_melt_quote(&self, quote_id: &str) -> Result<(), Self::Err>; async fn remove_melt_quote(&self, quote_id: &str) -> Result<(), Self::Err>;
/// Add [`Keys`] to storage /// Add [`Keys`] to storage
async fn add_keys(&self, keys: Keys) -> Result<(), Self::Err>; async fn add_keys(&self, keyset: KeySet) -> Result<(), Self::Err>;
/// Get [`Keys`] from storage /// Get [`Keys`] from storage
async fn get_keys(&self, id: &Id) -> Result<Option<Keys>, Self::Err>; async fn get_keys(&self, id: &Id) -> Result<Option<Keys>, Self::Err>;
/// Remove [`Keys`] from storage /// Remove [`Keys`] from storage

View File

@@ -139,9 +139,6 @@ pub struct MintKeySetInfo {
pub active: bool, pub active: bool,
/// Starting unix time Keyset is valid from /// Starting unix time Keyset is valid from
pub valid_from: u64, pub valid_from: u64,
/// When the Keyset is valid to
/// This is not shown to the wallet and can only be used internally
pub valid_to: Option<u64>,
/// [`DerivationPath`] keyset /// [`DerivationPath`] keyset
pub derivation_path: DerivationPath, pub derivation_path: DerivationPath,
/// DerivationPath index of Keyset /// DerivationPath index of Keyset
@@ -151,6 +148,8 @@ pub struct MintKeySetInfo {
/// Input Fee ppk /// Input Fee ppk
#[serde(default = "default_fee")] #[serde(default = "default_fee")]
pub input_fee_ppk: u64, pub input_fee_ppk: u64,
/// Final expiry
pub final_expiry: Option<u64>,
} }
/// Default fee /// Default fee
@@ -165,6 +164,7 @@ impl From<MintKeySetInfo> for KeySetInfo {
unit: keyset_info.unit, unit: keyset_info.unit,
active: keyset_info.active, active: keyset_info.active,
input_fee_ppk: keyset_info.input_fee_ppk, input_fee_ppk: keyset_info.input_fee_ppk,
final_expiry: keyset_info.final_expiry,
} }
} }
} }

View File

@@ -77,10 +77,11 @@ async fn test_swap_to_send() {
) )
.await .await
.expect("Failed to send token"); .expect("Failed to send token");
let keysets_info = wallet_alice.get_mint_keysets().await.unwrap();
let token_proofs = token.proofs(&keysets_info).unwrap();
assert_eq!( assert_eq!(
Amount::from(40), Amount::from(40),
token token_proofs
.proofs()
.total_amount() .total_amount()
.expect("Failed to get total amount") .expect("Failed to get total amount")
); );
@@ -92,7 +93,7 @@ async fn test_swap_to_send() {
.expect("Failed to get balance") .expect("Failed to get balance")
); );
assert_eq!( assert_eq!(
HashSet::<_, RandomState>::from_iter(token.proofs().ys().expect("Failed to get ys")), HashSet::<_, RandomState>::from_iter(token_proofs.ys().expect("Failed to get ys")),
HashSet::from_iter( HashSet::from_iter(
wallet_alice wallet_alice
.get_pending_spent_proofs() .get_pending_spent_proofs()
@@ -103,7 +104,8 @@ async fn test_swap_to_send() {
) )
); );
let transaction_id = TransactionId::from_proofs(token.proofs()).expect("Failed to get tx id"); let transaction_id =
TransactionId::from_proofs(token_proofs.clone()).expect("Failed to get tx id");
let transaction = wallet_alice let transaction = wallet_alice
.get_transaction(transaction_id) .get_transaction(transaction_id)
@@ -115,7 +117,7 @@ async fn test_swap_to_send() {
assert_eq!(Amount::from(40), transaction.amount); assert_eq!(Amount::from(40), transaction.amount);
assert_eq!(Amount::from(0), transaction.fee); assert_eq!(Amount::from(0), transaction.fee);
assert_eq!(CurrencyUnit::Sat, transaction.unit); assert_eq!(CurrencyUnit::Sat, transaction.unit);
assert_eq!(token.proofs().ys().unwrap(), transaction.ys); assert_eq!(token_proofs.ys().unwrap(), transaction.ys);
// Alice sends cashu, Carol receives // Alice sends cashu, Carol receives
let wallet_carol = create_test_wallet_for_mint(mint_bob.clone()) let wallet_carol = create_test_wallet_for_mint(mint_bob.clone())
@@ -123,7 +125,7 @@ async fn test_swap_to_send() {
.expect("Failed to create Carol's wallet"); .expect("Failed to create Carol's wallet");
let received_amount = wallet_carol let received_amount = wallet_carol
.receive_proofs( .receive_proofs(
token.proofs(), token_proofs.clone(),
ReceiveOptions::default(), ReceiveOptions::default(),
token.memo().clone(), token.memo().clone(),
) )
@@ -149,7 +151,7 @@ async fn test_swap_to_send() {
assert_eq!(Amount::from(40), transaction.amount); assert_eq!(Amount::from(40), transaction.amount);
assert_eq!(Amount::from(0), transaction.fee); assert_eq!(Amount::from(0), transaction.fee);
assert_eq!(CurrencyUnit::Sat, transaction.unit); assert_eq!(CurrencyUnit::Sat, transaction.unit);
assert_eq!(token.proofs().ys().unwrap(), transaction.ys); assert_eq!(token_proofs.ys().unwrap(), transaction.ys);
assert_eq!(token.memo().clone(), transaction.memo); assert_eq!(token.memo().clone(), transaction.memo);
} }
@@ -237,8 +239,8 @@ async fn test_mint_double_spend() {
.await .await
.expect("Could not get proofs"); .expect("Could not get proofs");
let keys = mint_bob.pubkeys().keysets.first().unwrap().clone().keys; let keys = mint_bob.pubkeys().keysets.first().unwrap().clone();
let keyset_id = Id::from(&keys); let keyset_id = keys.id;
let preswap = PreMintSecrets::random( let preswap = PreMintSecrets::random(
keyset_id, keyset_id,
@@ -294,8 +296,8 @@ async fn test_attempt_to_swap_by_overflowing() {
let amount = 2_u64.pow(63); let amount = 2_u64.pow(63);
let keys = mint_bob.pubkeys().keysets.first().unwrap().clone().keys; let keys = mint_bob.pubkeys().keysets.first().unwrap().clone();
let keyset_id = Id::from(&keys); let keyset_id = keys.id;
let pre_mint_amount = let pre_mint_amount =
PreMintSecrets::random(keyset_id, amount.into(), &SplitTarget::default()).unwrap(); PreMintSecrets::random(keyset_id, amount.into(), &SplitTarget::default()).unwrap();
@@ -532,7 +534,7 @@ async fn test_swap_overpay_underpay_fee() {
.expect("Could not get proofs"); .expect("Could not get proofs");
let keys = mint_bob.pubkeys().keysets.first().unwrap().clone().keys; let keys = mint_bob.pubkeys().keysets.first().unwrap().clone().keys;
let keyset_id = Id::from(&keys); let keyset_id = Id::v1_from_keys(&keys);
let preswap = PreMintSecrets::random(keyset_id, 9998.into(), &SplitTarget::default()).unwrap(); let preswap = PreMintSecrets::random(keyset_id, 9998.into(), &SplitTarget::default()).unwrap();
@@ -597,8 +599,8 @@ async fn test_mint_enforce_fee() {
.await .await
.expect("Could not get proofs"); .expect("Could not get proofs");
let keys = mint_bob.pubkeys().keysets.first().unwrap().clone().keys; let keys = mint_bob.pubkeys().keysets.first().unwrap().clone();
let keyset_id = Id::from(&keys); let keyset_id = keys.id;
let five_proofs: Vec<_> = proofs.drain(..5).collect(); let five_proofs: Vec<_> = proofs.drain(..5).collect();
@@ -884,6 +886,8 @@ async fn test_concurrent_double_spend_melt() {
} }
async fn get_keyset_id(mint: &Mint) -> Id { async fn get_keyset_id(mint: &Mint) -> Id {
let keys = mint.pubkeys().keysets.first().unwrap().clone().keys; let keys = mint.pubkeys().keysets.first().unwrap().clone();
Id::from(&keys) keys.verify_id()
.expect("Keyset ID generation is successful");
keys.id
} }

View File

@@ -13,7 +13,8 @@ use cdk_common::mint_url::MintUrl;
use cdk_common::util::unix_time; use cdk_common::util::unix_time;
use cdk_common::wallet::{self, MintQuote, Transaction, TransactionDirection, TransactionId}; use cdk_common::wallet::{self, MintQuote, Transaction, TransactionDirection, TransactionId};
use cdk_common::{ use cdk_common::{
database, CurrencyUnit, Id, KeySetInfo, Keys, MintInfo, PublicKey, SpendingConditions, State, database, CurrencyUnit, Id, KeySet, KeySetInfo, Keys, MintInfo, PublicKey, SpendingConditions,
State,
}; };
use redb::{Database, MultimapTableDefinition, ReadableTable, TableDefinition}; use redb::{Database, MultimapTableDefinition, ReadableTable, TableDefinition};
use tracing::instrument; use tracing::instrument;
@@ -493,15 +494,19 @@ impl WalletDatabase for WalletRedbDatabase {
} }
#[instrument(skip_all)] #[instrument(skip_all)]
async fn add_keys(&self, keys: Keys) -> Result<(), Self::Err> { async fn add_keys(&self, keyset: KeySet) -> Result<(), Self::Err> {
let write_txn = self.db.begin_write().map_err(Error::from)?; let write_txn = self.db.begin_write().map_err(Error::from)?;
keyset.verify_id()?;
{ {
let mut table = write_txn.open_table(MINT_KEYS_TABLE).map_err(Error::from)?; let mut table = write_txn.open_table(MINT_KEYS_TABLE).map_err(Error::from)?;
table table
.insert( .insert(
Id::from(&keys).to_string().as_str(), keyset.id.to_string().as_str(),
serde_json::to_string(&keys).map_err(Error::from)?.as_str(), serde_json::to_string(&keyset.keys)
.map_err(Error::from)?
.as_str(),
) )
.map_err(Error::from)?; .map_err(Error::from)?;
} }

View File

@@ -547,7 +547,10 @@ impl WalletDatabase for WalletRexieDatabase {
Ok(()) Ok(())
} }
async fn add_keys(&self, keys: Keys) -> Result<(), Self::Err> { async fn add_keys(&self, keyset: KeySet) -> Result<(), Self::Err> {
// Verify ID by recomputing id
keyset.verify_id()?;
let rexie = self.db.lock().await; let rexie = self.db.lock().await;
let transaction = rexie let transaction = rexie
@@ -556,7 +559,7 @@ impl WalletDatabase for WalletRexieDatabase {
let keys_store = transaction.store(MINT_KEYS).map_err(Error::from)?; let keys_store = transaction.store(MINT_KEYS).map_err(Error::from)?;
let keyset_id = serde_wasm_bindgen::to_value(&Id::from(&keys)).map_err(Error::from)?; let keyset_id = serde_wasm_bindgen::to_value(&keyset.id).map_err(Error::from)?;
let keys = serde_wasm_bindgen::to_value(&keys).map_err(Error::from)?; let keys = serde_wasm_bindgen::to_value(&keys).map_err(Error::from)?;
keys_store keys_store

View File

@@ -68,6 +68,8 @@ pub async fn init_keysets(
highest_index_keyset.max_order, highest_index_keyset.max_order,
highest_index_keyset.unit.clone(), highest_index_keyset.unit.clone(),
highest_index_keyset.derivation_path.clone(), highest_index_keyset.derivation_path.clone(),
highest_index_keyset.final_expiry,
cdk_common::nut02::KeySetVersion::Version00,
); );
active_keysets.insert(id, keyset); active_keysets.insert(id, keyset);
let mut keyset_info = highest_index_keyset; let mut keyset_info = highest_index_keyset;
@@ -97,6 +99,8 @@ pub async fn init_keysets(
unit.clone(), unit.clone(),
*max_order, *max_order,
*input_fee_ppk, *input_fee_ppk,
// TODO: add Mint settings for a final expiry of newly generated keysets
None,
); );
let id = keyset_info.id; let id = keyset_info.id;
@@ -114,6 +118,7 @@ pub async fn init_keysets(
/// Generate new [`MintKeySetInfo`] from path /// Generate new [`MintKeySetInfo`] from path
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
#[allow(clippy::too_many_arguments)]
pub fn create_new_keyset<C: secp256k1::Signing>( pub fn create_new_keyset<C: secp256k1::Signing>(
secp: &secp256k1::Secp256k1<C>, secp: &secp256k1::Secp256k1<C>,
xpriv: Xpriv, xpriv: Xpriv,
@@ -122,6 +127,7 @@ pub fn create_new_keyset<C: secp256k1::Signing>(
unit: CurrencyUnit, unit: CurrencyUnit,
max_order: u8, max_order: u8,
input_fee_ppk: u64, input_fee_ppk: u64,
final_expiry: Option<u64>,
) -> (MintKeySet, MintKeySetInfo) { ) -> (MintKeySet, MintKeySetInfo) {
let keyset = MintKeySet::generate( let keyset = MintKeySet::generate(
secp, secp,
@@ -130,13 +136,16 @@ pub fn create_new_keyset<C: secp256k1::Signing>(
.expect("RNG busted"), .expect("RNG busted"),
unit, unit,
max_order, max_order,
final_expiry,
// TODO: change this to Version01 to generate keysets v2
cdk_common::nut02::KeySetVersion::Version00,
); );
let keyset_info = MintKeySetInfo { let keyset_info = MintKeySetInfo {
id: keyset.id, id: keyset.id,
unit: keyset.unit.clone(), unit: keyset.unit.clone(),
active: true, active: true,
valid_from: unix_time(), valid_from: unix_time(),
valid_to: None, final_expiry: keyset.final_expiry,
derivation_path, derivation_path,
derivation_path_index, derivation_path_index,
max_order, max_order,

View File

@@ -72,6 +72,8 @@ impl DbSignatory {
unit.clone(), unit.clone(),
max_order, max_order,
fee, fee,
// TODO: add and connect settings for this
None,
); );
let id = keyset_info.id; let id = keyset_info.id;
@@ -130,6 +132,8 @@ impl DbSignatory {
keyset_info.max_order, keyset_info.max_order,
keyset_info.unit.clone(), keyset_info.unit.clone(),
keyset_info.derivation_path.clone(), keyset_info.derivation_path.clone(),
keyset_info.final_expiry,
keyset_info.id.get_version(),
) )
} }
} }
@@ -236,6 +240,8 @@ impl Signatory for DbSignatory {
args.unit.clone(), args.unit.clone(),
args.max_order, args.max_order,
args.input_fee_ppk, args.input_fee_ppk,
// TODO: add and connect settings for this
None,
); );
let id = info.id; let id = info.id;
self.localstore.add_keyset_info(info.clone()).await?; self.localstore.add_keyset_info(info.clone()).await?;
@@ -266,6 +272,8 @@ mod test {
2, 2,
CurrencyUnit::Sat, CurrencyUnit::Sat,
derivation_path_from_unit(CurrencyUnit::Sat, 0).unwrap(), derivation_path_from_unit(CurrencyUnit::Sat, 0).unwrap(),
None,
cdk_common::nut02::KeySetVersion::Version00,
); );
assert_eq!(keyset.unit, CurrencyUnit::Sat); assert_eq!(keyset.unit, CurrencyUnit::Sat);
@@ -310,6 +318,8 @@ mod test {
2, 2,
CurrencyUnit::Sat, CurrencyUnit::Sat,
derivation_path_from_unit(CurrencyUnit::Sat, 0).unwrap(), derivation_path_from_unit(CurrencyUnit::Sat, 0).unwrap(),
None,
cdk_common::nut02::KeySetVersion::Version00,
); );
assert_eq!(keyset.unit, CurrencyUnit::Sat); assert_eq!(keyset.unit, CurrencyUnit::Sat);

View File

@@ -60,6 +60,7 @@ impl TryInto<crate::signatory::SignatoryKeySet> for KeySet {
.map(|(amount, pk)| PublicKey::from_slice(&pk).map(|pk| (amount.into(), pk))) .map(|(amount, pk)| PublicKey::from_slice(&pk).map(|pk| (amount.into(), pk)))
.collect::<Result<BTreeMap<Amount, _>, _>>()?, .collect::<Result<BTreeMap<Amount, _>, _>>()?,
), ),
final_expiry: self.final_expiry,
}) })
} }
} }
@@ -78,6 +79,7 @@ impl From<crate::signatory::SignatoryKeySet> for KeySet {
.map(|(key, value)| ((*key).into(), value.to_bytes().to_vec())) .map(|(key, value)| ((*key).into(), value.to_bytes().to_vec()))
.collect(), .collect(),
}), }),
final_expiry: keyset.final_expiry,
} }
} }
} }
@@ -393,6 +395,7 @@ impl TryInto<cdk_common::KeySet> for KeySet {
.map(|(k, v)| cdk_common::PublicKey::from_slice(&v).map(|pk| (k.into(), pk))) .map(|(k, v)| cdk_common::PublicKey::from_slice(&v).map(|pk| (k.into(), pk)))
.collect::<Result<BTreeMap<cdk_common::Amount, cdk_common::PublicKey>, _>>()?, .collect::<Result<BTreeMap<cdk_common::Amount, cdk_common::PublicKey>, _>>()?,
), ),
final_expiry: self.final_expiry,
}) })
} }
} }
@@ -433,6 +436,7 @@ impl From<cdk_common::KeySetInfo> for KeySet {
active: value.active, active: value.active,
input_fee_ppk: value.input_fee_ppk, input_fee_ppk: value.input_fee_ppk,
keys: Default::default(), keys: Default::default(),
final_expiry: value.final_expiry,
} }
} }
} }
@@ -450,6 +454,7 @@ impl TryInto<cdk_common::KeySetInfo> for KeySet {
.map_err(|_| cdk_common::Error::Custom("Invalid unit encoding".to_owned()))?, .map_err(|_| cdk_common::Error::Custom("Invalid unit encoding".to_owned()))?,
active: self.active, active: self.active,
input_fee_ppk: self.input_fee_ppk, input_fee_ppk: self.input_fee_ppk,
final_expiry: self.final_expiry,
}) })
} }
} }

View File

@@ -62,6 +62,7 @@ message KeySet {
bool active = 3; bool active = 3;
uint64 input_fee_ppk = 4; uint64 input_fee_ppk = 4;
Keys keys = 5; Keys keys = 5;
optional uint64 final_expiry = 6;
} }
message Keys { message Keys {

View File

@@ -73,6 +73,8 @@ pub struct SignatoryKeySet {
pub keys: Keys, pub keys: Keys,
/// Information about the fee per public key /// Information about the fee per public key
pub input_fee_ppk: u64, pub input_fee_ppk: u64,
/// Final expiry of the keyset (unix timestamp in the future)
pub final_expiry: Option<u64>,
} }
impl From<&SignatoryKeySet> for KeySet { impl From<&SignatoryKeySet> for KeySet {
@@ -87,6 +89,7 @@ impl From<SignatoryKeySet> for KeySet {
id: val.id, id: val.id,
unit: val.unit, unit: val.unit,
keys: val.keys, keys: val.keys,
final_expiry: val.final_expiry,
} }
} }
} }
@@ -107,7 +110,7 @@ impl From<SignatoryKeySet> for MintKeySetInfo {
derivation_path: Default::default(), derivation_path: Default::default(),
derivation_path_index: Default::default(), derivation_path_index: Default::default(),
max_order: 0, max_order: 0,
valid_to: None, final_expiry: val.final_expiry,
valid_from: 0, valid_from: 0,
} }
} }
@@ -121,6 +124,7 @@ impl From<&(MintKeySetInfo, MintKeySet)> for SignatoryKeySet {
active: info.active, active: info.active,
input_fee_ppk: info.input_fee_ppk, input_fee_ppk: info.input_fee_ppk,
keys: key.keys.clone().into(), keys: key.keys.clone().into(),
final_expiry: key.final_expiry,
} }
} }
} }

View File

@@ -121,7 +121,7 @@ impl MintAuthDatabase for MintSqliteAuthDatabase {
.bind(":unit", keyset.unit.to_string()) .bind(":unit", keyset.unit.to_string())
.bind(":active", keyset.active) .bind(":active", keyset.active)
.bind(":valid_from", keyset.valid_from as i64) .bind(":valid_from", keyset.valid_from as i64)
.bind(":valid_to", keyset.valid_to.map(|v| v as i64)) .bind(":valid_to", keyset.final_expiry.map(|v| v as i64))
.bind(":derivation_path", keyset.derivation_path.to_string()) .bind(":derivation_path", keyset.derivation_path.to_string())
.bind(":max_order", keyset.max_order) .bind(":max_order", keyset.max_order)
.bind(":derivation_path_index", keyset.derivation_path_index) .bind(":derivation_path_index", keyset.derivation_path_index)

View File

@@ -214,7 +214,7 @@ impl MintKeysDatabase for MintSqliteDatabase {
.bind(":unit", keyset.unit.to_string()) .bind(":unit", keyset.unit.to_string())
.bind(":active", keyset.active) .bind(":active", keyset.active)
.bind(":valid_from", keyset.valid_from as i64) .bind(":valid_from", keyset.valid_from as i64)
.bind(":valid_to", keyset.valid_to.map(|v| v as i64)) .bind(":valid_to", keyset.final_expiry.map(|v| v as i64))
.bind(":derivation_path", keyset.derivation_path.to_string()) .bind(":derivation_path", keyset.derivation_path.to_string())
.bind(":max_order", keyset.max_order) .bind(":max_order", keyset.max_order)
.bind(":input_fee_ppk", keyset.input_fee_ppk as i64) .bind(":input_fee_ppk", keyset.input_fee_ppk as i64)
@@ -1134,11 +1134,11 @@ fn sqlite_row_to_keyset_info(row: Vec<Column>) -> Result<MintKeySetInfo, Error>
unit: column_as_string!(unit, CurrencyUnit::from_str), unit: column_as_string!(unit, CurrencyUnit::from_str),
active: matches!(active, Column::Integer(1)), active: matches!(active, Column::Integer(1)),
valid_from: column_as_number!(valid_from), valid_from: column_as_number!(valid_from),
valid_to: column_as_nullable_number!(valid_to),
derivation_path: column_as_string!(derivation_path, DerivationPath::from_str), derivation_path: column_as_string!(derivation_path, DerivationPath::from_str),
derivation_path_index: column_as_nullable_number!(derivation_path_index), derivation_path_index: column_as_nullable_number!(derivation_path_index),
max_order: column_as_number!(max_order), max_order: column_as_number!(max_order),
input_fee_ppk: column_as_number!(row_keyset_ppk), input_fee_ppk: column_as_number!(row_keyset_ppk),
final_expiry: column_as_nullable_number!(valid_to),
}) })
} }
@@ -1319,11 +1319,11 @@ mod tests {
unit: CurrencyUnit::Sat, unit: CurrencyUnit::Sat,
active: true, active: true,
valid_from: 0, valid_from: 0,
valid_to: None,
derivation_path: bitcoin::bip32::DerivationPath::from_str("m/0'/0'/0'").unwrap(), derivation_path: bitcoin::bip32::DerivationPath::from_str("m/0'/0'/0'").unwrap(),
derivation_path_index: Some(0), derivation_path_index: Some(0),
max_order: 32, max_order: 32,
input_fee_ppk: 0, input_fee_ppk: 0,
final_expiry: None,
}; };
db.add_keyset_info(keyset_info).await.unwrap(); db.add_keyset_info(keyset_info).await.unwrap();
@@ -1387,11 +1387,11 @@ mod tests {
unit: CurrencyUnit::Sat, unit: CurrencyUnit::Sat,
active: true, active: true,
valid_from: 0, valid_from: 0,
valid_to: None,
derivation_path: bitcoin::bip32::DerivationPath::from_str("m/0'/0'/0'").unwrap(), derivation_path: bitcoin::bip32::DerivationPath::from_str("m/0'/0'/0'").unwrap(),
derivation_path_index: Some(0), derivation_path_index: Some(0),
max_order: 32, max_order: 32,
input_fee_ppk: 0, input_fee_ppk: 0,
final_expiry: None,
}; };
db.add_keyset_info(keyset_info).await.unwrap(); db.add_keyset_info(keyset_info).await.unwrap();

View File

@@ -16,4 +16,5 @@ pub static MIGRATIONS: &[(&str, &str)] = &[
("20250314082116_allow_pending_spent.sql", include_str!(r#"./migrations/20250314082116_allow_pending_spent.sql"#)), ("20250314082116_allow_pending_spent.sql", include_str!(r#"./migrations/20250314082116_allow_pending_spent.sql"#)),
("20250323152040_wallet_dleq_proofs.sql", include_str!(r#"./migrations/20250323152040_wallet_dleq_proofs.sql"#)), ("20250323152040_wallet_dleq_proofs.sql", include_str!(r#"./migrations/20250323152040_wallet_dleq_proofs.sql"#)),
("20250401120000_add_transactions_table.sql", include_str!(r#"./migrations/20250401120000_add_transactions_table.sql"#)), ("20250401120000_add_transactions_table.sql", include_str!(r#"./migrations/20250401120000_add_transactions_table.sql"#)),
("20250616144830_add_keyset_expiry.sql", include_str!(r#"./migrations/20250616144830_add_keyset_expiry.sql"#)),
]; ];

View File

@@ -0,0 +1 @@
ALTER TABLE keyset ADD COLUMN final_expiry INTEGER DEFAULT NULL;

View File

@@ -14,8 +14,8 @@ use cdk_common::nuts::{MeltQuoteState, MintQuoteState};
use cdk_common::secret::Secret; use cdk_common::secret::Secret;
use cdk_common::wallet::{self, MintQuote, Transaction, TransactionDirection, TransactionId}; use cdk_common::wallet::{self, MintQuote, Transaction, TransactionDirection, TransactionId};
use cdk_common::{ use cdk_common::{
database, Amount, CurrencyUnit, Id, KeySetInfo, Keys, MintInfo, Proof, ProofDleq, PublicKey, database, Amount, CurrencyUnit, Id, KeySet, KeySetInfo, Keys, MintInfo, Proof, ProofDleq,
SecretKey, SpendingConditions, State, PublicKey, SecretKey, SpendingConditions, State,
}; };
use error::Error; use error::Error;
use tracing::instrument; use tracing::instrument;
@@ -294,14 +294,15 @@ ON CONFLICT(mint_url) DO UPDATE SET
Statement::new( Statement::new(
r#" r#"
INSERT INTO keyset INSERT INTO keyset
(mint_url, id, unit, active, input_fee_ppk) (mint_url, id, unit, active, input_fee_ppk, final_expiry)
VALUES VALUES
(:mint_url, :id, :unit, :active, :input_fee_ppk) (:mint_url, :id, :unit, :active, :input_fee_ppk, :final_expiry)
ON CONFLICT(id) DO UPDATE SET ON CONFLICT(id) DO UPDATE SET
mint_url = excluded.mint_url, mint_url = excluded.mint_url,
unit = excluded.unit, unit = excluded.unit,
active = excluded.active, active = excluded.active,
input_fee_ppk = excluded.input_fee_ppk; input_fee_ppk = excluded.input_fee_ppk,
final_expiry = excluded.final_expiry;
"#, "#,
) )
.bind(":mint_url", mint_url.to_string()) .bind(":mint_url", mint_url.to_string())
@@ -309,6 +310,7 @@ ON CONFLICT(mint_url) DO UPDATE SET
.bind(":unit", keyset.unit.to_string()) .bind(":unit", keyset.unit.to_string())
.bind(":active", keyset.active) .bind(":active", keyset.active)
.bind(":input_fee_ppk", keyset.input_fee_ppk as i64) .bind(":input_fee_ppk", keyset.input_fee_ppk as i64)
.bind(":final_expiry", keyset.final_expiry.map(|v| v as i64))
.execute(&conn) .execute(&conn)
.map_err(Error::Sqlite)?; .map_err(Error::Sqlite)?;
} }
@@ -327,7 +329,8 @@ ON CONFLICT(mint_url) DO UPDATE SET
id, id,
unit, unit,
active, active,
input_fee_ppk input_fee_ppk,
final_expiry
FROM FROM
keyset keyset
WHERE mint_url = :mint_url WHERE mint_url = :mint_url
@@ -354,7 +357,8 @@ ON CONFLICT(mint_url) DO UPDATE SET
id, id,
unit, unit,
active, active,
input_fee_ppk input_fee_ppk,
final_expiry
FROM FROM
keyset keyset
WHERE id = :id WHERE id = :id
@@ -528,7 +532,10 @@ ON CONFLICT(id) DO UPDATE SET
} }
#[instrument(skip_all)] #[instrument(skip_all)]
async fn add_keys(&self, keys: Keys) -> Result<(), Self::Err> { async fn add_keys(&self, keyset: KeySet) -> Result<(), Self::Err> {
// Recompute ID for verification
keyset.verify_id()?;
Statement::new( Statement::new(
r#" r#"
INSERT INTO key INSERT INTO key
@@ -539,8 +546,11 @@ ON CONFLICT(id) DO UPDATE SET
keys = excluded.keys keys = excluded.keys
"#, "#,
) )
.bind(":id", Id::from(&keys).to_string()) .bind(":id", keyset.id.to_string())
.bind(":keys", serde_json::to_string(&keys).map_err(Error::from)?) .bind(
":keys",
serde_json::to_string(&keyset.keys).map_err(Error::from)?,
)
.execute(&self.pool.get().map_err(Error::Pool)?) .execute(&self.pool.get().map_err(Error::Pool)?)
.map_err(Error::Sqlite)?; .map_err(Error::Sqlite)?;
@@ -909,13 +919,15 @@ fn sqlite_row_to_mint_info(row: Vec<Column>) -> Result<MintInfo, Error> {
}) })
} }
#[instrument(skip_all)]
fn sqlite_row_to_keyset(row: Vec<Column>) -> Result<KeySetInfo, Error> { fn sqlite_row_to_keyset(row: Vec<Column>) -> Result<KeySetInfo, Error> {
unpack_into!( unpack_into!(
let ( let (
id, id,
unit, unit,
active, active,
input_fee_ppk input_fee_ppk,
final_expiry
) = row ) = row
); );
@@ -924,6 +936,7 @@ fn sqlite_row_to_keyset(row: Vec<Column>) -> Result<KeySetInfo, Error> {
unit: column_as_string!(unit, CurrencyUnit::from_str), unit: column_as_string!(unit, CurrencyUnit::from_str),
active: matches!(active, Column::Integer(1)), active: matches!(active, Column::Integer(1)),
input_fee_ppk: column_as_nullable_number!(input_fee_ppk).unwrap_or_default(), input_fee_ppk: column_as_nullable_number!(input_fee_ppk).unwrap_or_default(),
final_expiry: column_as_nullable_number!(final_expiry),
}) })
} }

View File

@@ -39,6 +39,7 @@ impl Mint {
unit: key.unit.clone(), unit: key.unit.clone(),
active: key.active, active: key.active,
input_fee_ppk: key.input_fee_ppk, input_fee_ppk: key.input_fee_ppk,
final_expiry: key.final_expiry,
}) })
} else { } else {
None None

View File

@@ -53,6 +53,7 @@ impl Mint {
unit: k.unit.clone(), unit: k.unit.clone(),
active: k.active, active: k.active,
input_fee_ppk: k.input_fee_ppk, input_fee_ppk: k.input_fee_ppk,
final_expiry: k.final_expiry,
}) })
.collect(), .collect(),
} }

View File

@@ -180,7 +180,7 @@ impl AuthWallet {
keys.verify_id()?; keys.verify_id()?;
self.localstore.add_keys(keys.keys.clone()).await?; self.localstore.add_keys(keys.clone()).await?;
keys.keys keys.keys
}; };

View File

@@ -19,7 +19,7 @@ impl Wallet {
keys.verify_id()?; keys.verify_id()?;
self.localstore.add_keys(keys.keys.clone()).await?; self.localstore.add_keys(keys.clone()).await?;
keys.keys keys.keys
}; };
@@ -27,7 +27,23 @@ impl Wallet {
Ok(keys) Ok(keys)
} }
/// Get keysets for mint /// Get keysets from DB or fetch them
///
/// Checks the database for keysets and queries the Mint if
/// it can't find any.
#[instrument(skip(self))]
pub async fn load_mint_keysets(&self) -> Result<Vec<KeySetInfo>, Error> {
match self
.localstore
.get_mint_keysets(self.mint_url.clone())
.await?
{
Some(keysets_info) => Ok(keysets_info),
None => self.get_mint_keysets().await, // Hit the keysets endpoint if we don't have the keysets for this Mint
}
}
/// Get keysets for wallet's mint
/// ///
/// Queries mint for all keysets /// Queries mint for all keysets
#[instrument(skip(self))] #[instrument(skip(self))]

View File

@@ -474,7 +474,7 @@ impl Wallet {
/// Can be used to allow a wallet to accept payments offline while reducing /// Can be used to allow a wallet to accept payments offline while reducing
/// the risk of claiming back to the limits let by the spending_conditions /// the risk of claiming back to the limits let by the spending_conditions
#[instrument(skip(self, token))] #[instrument(skip(self, token))]
pub fn verify_token_p2pk( pub async fn verify_token_p2pk(
&self, &self,
token: &Token, token: &Token,
spending_conditions: SpendingConditions, spending_conditions: SpendingConditions,
@@ -526,8 +526,10 @@ impl Wallet {
token.mint_url()? token.mint_url()?
))); )));
} }
// We need the keysets information to properly convert from token proof to proof
let keysets_info = self.load_mint_keysets().await?;
let proofs = token.proofs(&keysets_info)?;
let proofs = token.proofs();
for proof in proofs { for proof in proofs {
let secret: nut10::Secret = (&proof.secret).try_into()?; let secret: nut10::Secret = (&proof.secret).try_into()?;
@@ -620,7 +622,9 @@ impl Wallet {
// ))); // )));
// } // }
let proofs = token.proofs(); // We need the keysets information to properly convert from token proof to proof
let keysets_info = self.load_mint_keysets().await?;
let proofs = token.proofs(&keysets_info)?;
for proof in proofs { for proof in proofs {
let mint_pubkey = match keys_cache.get(&proof.keyset_id) { let mint_pubkey = match keys_cache.get(&proof.keyset_id) {
Some(keys) => keys.amount_key(proof.amount), Some(keys) => keys.amount_key(proof.amount),

View File

@@ -271,12 +271,6 @@ impl MultiMintWallet {
let token_data = Token::from_str(encoded_token)?; let token_data = Token::from_str(encoded_token)?;
let unit = token_data.unit().unwrap_or_default(); let unit = token_data.unit().unwrap_or_default();
let proofs = token_data.proofs();
let mut amount_received = Amount::ZERO;
let mut mint_errors = None;
let mint_url = token_data.mint_url()?; let mint_url = token_data.mint_url()?;
// Check that all mints in tokes have wallets // Check that all mints in tokes have wallets
@@ -291,6 +285,22 @@ impl MultiMintWallet {
.get(&wallet_key) .get(&wallet_key)
.ok_or(Error::UnknownWallet(wallet_key.clone()))?; .ok_or(Error::UnknownWallet(wallet_key.clone()))?;
// We need the keysets information to properly convert from token proof to proof
let keysets_info = match self
.localstore
.get_mint_keysets(token_data.mint_url()?)
.await?
{
Some(keysets_info) => keysets_info,
// Hit the keysets endpoint if we don't have the keysets for this Mint
None => wallet.get_mint_keysets().await?,
};
let proofs = token_data.proofs(&keysets_info)?;
let mut amount_received = Amount::ZERO;
let mut mint_errors = None;
match wallet match wallet
.receive_proofs(proofs, opts, token_data.memo().clone()) .receive_proofs(proofs, opts, token_data.memo().clone())
.await .await
@@ -356,7 +366,7 @@ impl MultiMintWallet {
.get(wallet_key) .get(wallet_key)
.ok_or(Error::UnknownWallet(wallet_key.clone()))?; .ok_or(Error::UnknownWallet(wallet_key.clone()))?;
wallet.verify_token_p2pk(token, conditions) wallet.verify_token_p2pk(token, conditions).await
} }
/// Verifys all proofs in token have valid dleq proof /// Verifys all proofs in token have valid dleq proof

View File

@@ -219,7 +219,8 @@ impl Wallet {
ensure_cdk!(unit == self.unit, Error::UnsupportedUnit); ensure_cdk!(unit == self.unit, Error::UnsupportedUnit);
let proofs = token.proofs(); let keysets_info = self.load_mint_keysets().await?;
let proofs = token.proofs(&keysets_info)?;
if let Token::TokenV3(token) = &token { if let Token::TokenV3(token) = &token {
ensure_cdk!(!token.is_multi_mint(), Error::MultiMintTokenNotSupported); ensure_cdk!(!token.is_multi_mint(), Error::MultiMintTokenNotSupported);