mirror of
https://github.com/aljazceru/cdk.git
synced 2025-12-19 05:35:18 +01:00
Keysets V2 (#702)
--------- Co-authored-by: thesimplekid <tsk@thesimplekid.com>
This commit is contained in:
@@ -12,6 +12,7 @@ use std::string::FromUtf8Error;
|
||||
use serde::{de, Deserialize, Deserializer, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
use super::nut02::ShortKeysetId;
|
||||
#[cfg(feature = "wallet")]
|
||||
use super::nut10;
|
||||
#[cfg(feature = "wallet")]
|
||||
@@ -183,6 +184,9 @@ pub enum Error {
|
||||
/// NUT11 error
|
||||
#[error(transparent)]
|
||||
NUT11(#[from] crate::nuts::nut11::Error),
|
||||
/// Short keyset id -> id error
|
||||
#[error(transparent)]
|
||||
NUT02(#[from] crate::nuts::nut02::Error),
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
fn from(proof: Proof) -> ProofV4 {
|
||||
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>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
|
||||
@@ -10,11 +10,11 @@ use bitcoin::base64::engine::{general_purpose, GeneralPurpose};
|
||||
use bitcoin::base64::{alphabet, Engine as _};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::{Error, Proof, ProofV4, Proofs};
|
||||
use super::{Error, Proof, ProofV3, ProofV4, Proofs};
|
||||
use crate::mint_url::MintUrl;
|
||||
use crate::nuts::nut00::ProofsMethods;
|
||||
use crate::nut02::ShortKeysetId;
|
||||
use crate::nuts::{CurrencyUnit, Id};
|
||||
use crate::{ensure_cdk, Amount};
|
||||
use crate::{ensure_cdk, Amount, KeySetInfo};
|
||||
|
||||
/// Token Enum
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@@ -66,10 +66,10 @@ impl Token {
|
||||
}
|
||||
|
||||
/// Proofs in [`Token`]
|
||||
pub fn proofs(&self) -> Proofs {
|
||||
pub fn proofs(&self, mint_keysets: &[KeySetInfo]) -> Result<Proofs, Error> {
|
||||
match self {
|
||||
Self::TokenV3(token) => token.proofs(),
|
||||
Self::TokenV4(token) => token.proofs(),
|
||||
Self::TokenV3(token) => token.proofs(mint_keysets),
|
||||
Self::TokenV4(token) => token.proofs(mint_keysets),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,8 +181,8 @@ impl TryFrom<&Vec<u8>> for Token {
|
||||
pub struct TokenV3Token {
|
||||
/// Url of mint
|
||||
pub mint: MintUrl,
|
||||
/// [`Proofs`]
|
||||
pub proofs: Proofs,
|
||||
/// [`Vec<ProofV3>`]
|
||||
pub proofs: Vec<ProofV3>,
|
||||
}
|
||||
|
||||
impl TokenV3Token {
|
||||
@@ -190,7 +190,7 @@ impl TokenV3Token {
|
||||
pub fn new(mint_url: MintUrl, proofs: Proofs) -> Self {
|
||||
Self {
|
||||
mint: mint_url,
|
||||
proofs,
|
||||
proofs: proofs.into_iter().map(ProofV3::from).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -226,17 +226,21 @@ impl TokenV3 {
|
||||
}
|
||||
|
||||
/// Proofs
|
||||
pub fn proofs(&self) -> Proofs {
|
||||
self.token
|
||||
.iter()
|
||||
.flat_map(|token| token.proofs.clone())
|
||||
.collect()
|
||||
pub fn proofs(&self, mint_keysets: &[KeySetInfo]) -> Result<Proofs, Error> {
|
||||
let mut proofs: Proofs = vec![];
|
||||
for t in self.token.iter() {
|
||||
for p in t.proofs.iter() {
|
||||
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
|
||||
#[inline]
|
||||
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
|
||||
.iter()
|
||||
.collect::<std::collections::HashSet<_>>()
|
||||
@@ -247,7 +251,12 @@ impl TokenV3 {
|
||||
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
|
||||
@@ -306,10 +315,27 @@ impl fmt::Display for TokenV3 {
|
||||
|
||||
impl From<TokenV4> for TokenV3 {
|
||||
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 {
|
||||
token: vec![TokenV3Token::new(token.mint_url, proofs)],
|
||||
token: vec![token_v3_token],
|
||||
memo: token.memo,
|
||||
unit: Some(token.unit),
|
||||
}
|
||||
@@ -335,17 +361,19 @@ pub struct TokenV4 {
|
||||
|
||||
impl TokenV4 {
|
||||
/// Proofs from token
|
||||
pub fn proofs(&self) -> Proofs {
|
||||
self.token
|
||||
.iter()
|
||||
.flat_map(|token| token.proofs.iter().map(|p| p.into_proof(&token.keyset_id)))
|
||||
.collect()
|
||||
pub fn proofs(&self, mint_keysets: &[KeySetInfo]) -> Result<Proofs, Error> {
|
||||
let mut proofs: Proofs = vec![];
|
||||
for t in self.token.iter() {
|
||||
let long_id = Id::from_short_keyset_id(&t.keyset_id, mint_keysets)?;
|
||||
proofs.extend(t.proofs.iter().map(|p| p.into_proof(&long_id)));
|
||||
}
|
||||
Ok(proofs)
|
||||
}
|
||||
|
||||
/// Value - errors if duplicate proofs are found
|
||||
#[inline]
|
||||
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
|
||||
.iter()
|
||||
.collect::<std::collections::HashSet<_>>()
|
||||
@@ -356,7 +384,12 @@ impl TokenV4 {
|
||||
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
|
||||
@@ -421,23 +454,29 @@ impl TryFrom<&Vec<u8>> for TokenV4 {
|
||||
impl TryFrom<TokenV3> for TokenV4 {
|
||||
type Error = Error;
|
||||
fn try_from(token: TokenV3) -> Result<Self, Self::Error> {
|
||||
let proofs = token.proofs();
|
||||
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);
|
||||
|
||||
let mint_url = mint_urls.first().ok_or(Error::UnsupportedToken)?;
|
||||
|
||||
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()
|
||||
.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();
|
||||
|
||||
Ok(TokenV4 {
|
||||
@@ -458,32 +497,34 @@ pub struct TokenV4Token {
|
||||
serialize_with = "serialize_v4_keyset_id",
|
||||
deserialize_with = "deserialize_v4_keyset_id"
|
||||
)]
|
||||
pub keyset_id: Id,
|
||||
pub keyset_id: ShortKeysetId,
|
||||
/// Proofs
|
||||
#[serde(rename = "p")]
|
||||
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
|
||||
S: serde::Serializer,
|
||||
{
|
||||
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
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
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 {
|
||||
/// Create new [`TokenV4Token`]
|
||||
pub fn new(keyset_id: Id, proofs: Proofs) -> Self {
|
||||
// Create a short keyset id from id
|
||||
let short_id = ShortKeysetId::from(keyset_id);
|
||||
Self {
|
||||
keyset_id,
|
||||
keyset_id: short_id,
|
||||
proofs: proofs.into_iter().map(|p| p.into()).collect(),
|
||||
}
|
||||
}
|
||||
@@ -493,7 +534,10 @@ impl TokenV4Token {
|
||||
mod tests {
|
||||
use std::str::FromStr;
|
||||
|
||||
use bip39::rand::{self, RngCore};
|
||||
|
||||
use super::*;
|
||||
use crate::dhke::hash_to_curve;
|
||||
use crate::mint_url::MintUrl;
|
||||
use crate::secret::Secret;
|
||||
use crate::util::hex;
|
||||
@@ -522,7 +566,7 @@ mod tests {
|
||||
);
|
||||
assert_eq!(
|
||||
token.token[0].keyset_id,
|
||||
Id::from_str("00ad268c4d1f5826").unwrap()
|
||||
ShortKeysetId::from_str("00ad268c4d1f5826").unwrap()
|
||||
);
|
||||
|
||||
let encoded = &token.to_string();
|
||||
@@ -546,12 +590,13 @@ mod tests {
|
||||
|
||||
match 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!(tokens.contains(&Id::from_str("00ffd48b8f5ecf80").unwrap()));
|
||||
assert!(tokens.contains(&Id::from_str("00ad268c4d1f5826").unwrap()));
|
||||
assert!(tokens.contains(&ShortKeysetId::from_str("00ffd48b8f5ecf80").unwrap()));
|
||||
assert!(tokens.contains(&ShortKeysetId::from_str("00ad268c4d1f5826").unwrap()));
|
||||
|
||||
let mint_url = token.mint_url;
|
||||
|
||||
@@ -584,7 +629,7 @@ mod tests {
|
||||
);
|
||||
assert_eq!(
|
||||
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);
|
||||
|
||||
@@ -684,4 +729,101 @@ mod tests {
|
||||
assert!(result.is_ok());
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,12 @@ pub enum Error {
|
||||
/// Keyset id does not match
|
||||
#[error("Keyset id incorrect")]
|
||||
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
|
||||
#[error(transparent)]
|
||||
Slice(#[from] TryFromSliceError),
|
||||
@@ -51,8 +57,10 @@ pub enum Error {
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
|
||||
pub enum KeySetVersion {
|
||||
/// Current Version 00
|
||||
/// Version 00
|
||||
Version00,
|
||||
/// Version 01
|
||||
Version01,
|
||||
}
|
||||
|
||||
impl KeySetVersion {
|
||||
@@ -60,6 +68,7 @@ impl KeySetVersion {
|
||||
pub fn to_byte(&self) -> u8 {
|
||||
match self {
|
||||
Self::Version00 => 0,
|
||||
Self::Version01 => 1,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +76,7 @@ impl KeySetVersion {
|
||||
pub fn from_byte(byte: &u8) -> Result<Self, Error> {
|
||||
match byte {
|
||||
0 => Ok(Self::Version00),
|
||||
1 => Ok(Self::Version01),
|
||||
_ => Err(Error::UnknownVersion),
|
||||
}
|
||||
}
|
||||
@@ -76,6 +86,27 @@ impl fmt::Display for KeySetVersion {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
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))]
|
||||
pub struct Id {
|
||||
version: KeySetVersion,
|
||||
id: [u8; Self::BYTELEN],
|
||||
id: IdBytes,
|
||||
}
|
||||
|
||||
impl Id {
|
||||
const STRLEN: usize = 14;
|
||||
const BYTELEN: usize = 7;
|
||||
const STRLEN_V1: usize = 14;
|
||||
const BYTELEN_V1: usize = 7;
|
||||
const STRLEN_V2: usize = 64;
|
||||
const BYTELEN_V2: usize = 32;
|
||||
|
||||
/// [`Id`] to bytes
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
@@ -103,18 +136,122 @@ impl Id {
|
||||
|
||||
/// [`Id`] from bytes
|
||||
pub fn from_bytes(bytes: &[u8]) -> Result<Self, Error> {
|
||||
Ok(Self {
|
||||
version: KeySetVersion::from_byte(&bytes[0])?,
|
||||
id: bytes[1..].try_into()?,
|
||||
})
|
||||
let version = KeySetVersion::from_byte(&bytes[0])?;
|
||||
let id = match version {
|
||||
KeySetVersion::Version00 => IdBytes::V1(bytes[1..].try_into()?),
|
||||
KeySetVersion::Version01 => IdBytes::V2(bytes[1..].try_into()?),
|
||||
};
|
||||
Ok(Self { version, id })
|
||||
}
|
||||
|
||||
/// [`Id`] as bytes
|
||||
pub fn as_bytes(&self) -> [u8; Self::BYTELEN + 1] {
|
||||
let mut bytes = [0u8; Self::BYTELEN + 1];
|
||||
bytes[0] = self.version.to_byte();
|
||||
bytes[1..].copy_from_slice(&self.id);
|
||||
bytes
|
||||
/// Get the version of the keyset
|
||||
pub fn get_version(&self) -> KeySetVersion {
|
||||
self.version
|
||||
}
|
||||
|
||||
/// *** 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
|
||||
impl From<Id> for u32 {
|
||||
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);
|
||||
|
||||
@@ -132,13 +271,21 @@ impl From<Id> for u32 {
|
||||
|
||||
impl fmt::Display for Id {
|
||||
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 {
|
||||
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;
|
||||
|
||||
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 {
|
||||
version: KeySetVersion::from_byte(&hex::decode(&s[..2])?[0])?,
|
||||
id: hex::decode(&s[2..])?
|
||||
let version: KeySetVersion = KeySetVersion::from_byte(&hex::decode(&s[..2])?[0])?;
|
||||
let id = match version {
|
||||
KeySetVersion::Version00 => IdBytes::V1(
|
||||
hex::decode(&s[2..])?
|
||||
.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 {
|
||||
/// 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
|
||||
fn from(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: hex::decode(&hex_of_hash[0..Self::STRLEN])
|
||||
.expect("Keys hash could not be hex decoded")
|
||||
.try_into()
|
||||
.expect("Invalid length of hex id"),
|
||||
/// Improper prefix of the keyset ID. In case of v1, this is the whole ID.
|
||||
/// In case of v2, this is the 8-byte prefix
|
||||
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||
#[serde(into = "String", try_from = "String")]
|
||||
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
|
||||
pub struct ShortKeysetId {
|
||||
/// The version of the short keyset
|
||||
version: KeySetVersion,
|
||||
/// The improper prefix of the keyset ID bytes
|
||||
prefix: Vec<u8>,
|
||||
}
|
||||
|
||||
impl ShortKeysetId {
|
||||
/// [`ShortKeysetId`] to bytes
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
[vec![self.version.to_byte()], self.prefix.clone()].concat()
|
||||
}
|
||||
|
||||
/// [`ShortKeysetId`] from bytes
|
||||
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 })
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Id> for ShortKeysetId {
|
||||
fn from(id: Id) -> Self {
|
||||
let version = id.version;
|
||||
let prefix: Vec<u8> = match id.version {
|
||||
KeySetVersion::Version00 => match id.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,
|
||||
/// Keyset [`Keys`]
|
||||
pub keys: Keys,
|
||||
/// Expiry
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub final_expiry: Option<u64>,
|
||||
}
|
||||
|
||||
impl KeySet {
|
||||
/// Verify the keyset is matches keys
|
||||
/// Verify the keyset id matches keys
|
||||
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);
|
||||
}
|
||||
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(())
|
||||
}
|
||||
@@ -243,6 +469,7 @@ impl From<MintKeySet> for KeySet {
|
||||
id: keyset.id,
|
||||
unit: keyset.unit,
|
||||
keys: Keys::from(keyset.keys),
|
||||
final_expiry: keyset.final_expiry,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -265,6 +492,9 @@ pub struct KeySetInfo {
|
||||
default = "default_input_fee_ppk"
|
||||
)]
|
||||
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>
|
||||
@@ -290,6 +520,9 @@ pub struct MintKeySet {
|
||||
pub unit: CurrencyUnit,
|
||||
/// Keyset [`MintKeys`]
|
||||
pub keys: MintKeys,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
/// Expiry [`Option<u64>`]
|
||||
pub final_expiry: Option<u64>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "mint")]
|
||||
@@ -300,6 +533,8 @@ impl MintKeySet {
|
||||
xpriv: Xpriv,
|
||||
unit: CurrencyUnit,
|
||||
max_order: u8,
|
||||
final_expiry: Option<u64>,
|
||||
version: KeySetVersion,
|
||||
) -> Self {
|
||||
let mut map = BTreeMap::new();
|
||||
for i in 0..max_order {
|
||||
@@ -322,10 +557,15 @@ impl MintKeySet {
|
||||
}
|
||||
|
||||
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 {
|
||||
id: (&keys).into(),
|
||||
id,
|
||||
unit,
|
||||
keys,
|
||||
final_expiry,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -336,6 +576,8 @@ impl MintKeySet {
|
||||
max_order: u8,
|
||||
currency_unit: CurrencyUnit,
|
||||
derivation_path: DerivationPath,
|
||||
final_expiry: Option<u64>,
|
||||
version: KeySetVersion,
|
||||
) -> Self {
|
||||
let xpriv = Xpriv::new_master(bitcoin::Network::Bitcoin, seed).expect("RNG busted");
|
||||
Self::generate(
|
||||
@@ -345,6 +587,8 @@ impl MintKeySet {
|
||||
.expect("RNG busted"),
|
||||
currency_unit,
|
||||
max_order,
|
||||
final_expiry,
|
||||
version,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -355,6 +599,8 @@ impl MintKeySet {
|
||||
max_order: u8,
|
||||
currency_unit: CurrencyUnit,
|
||||
derivation_path: DerivationPath,
|
||||
final_expiry: Option<u64>,
|
||||
version: KeySetVersion,
|
||||
) -> Self {
|
||||
Self::generate(
|
||||
secp,
|
||||
@@ -363,6 +609,8 @@ impl MintKeySet {
|
||||
.expect("RNG busted"),
|
||||
currency_unit,
|
||||
max_order,
|
||||
final_expiry,
|
||||
version,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -371,8 +619,10 @@ impl MintKeySet {
|
||||
impl From<MintKeySet> for Id {
|
||||
fn from(keyset: MintKeySet) -> Id {
|
||||
let keys: super::KeySet = keyset.into();
|
||||
|
||||
Id::from(&keys.keys)
|
||||
match keys.id.version {
|
||||
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 {
|
||||
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 super::{KeySetInfo, Keys, KeysetResponse};
|
||||
use super::{KeySetInfo, KeySetVersion, Keys, KeysetResponse, ShortKeysetId};
|
||||
use crate::nuts::nut02::{Error, Id};
|
||||
use crate::nuts::KeysResponse;
|
||||
use crate::util::hex;
|
||||
use crate::CurrencyUnit;
|
||||
|
||||
const SHORT_KEYSET_ID: &str = "00456a94ab4e1c46";
|
||||
const SHORT_KEYSET: &str = r#"
|
||||
@@ -482,17 +733,43 @@ mod test {
|
||||
|
||||
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());
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
#[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]
|
||||
fn test_deserialization_keyset_info() {
|
||||
let h = r#"{"id":"009a1f293253e41e","unit":"sat","active":true}"#;
|
||||
@@ -519,6 +796,15 @@ mod test {
|
||||
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]
|
||||
fn test_id_from_invalid_byte_length() {
|
||||
let three_bytes = [0x01, 0x02, 0x03];
|
||||
@@ -548,16 +834,28 @@ mod test {
|
||||
assert_eq!(keys_response.keysets.len(), 2);
|
||||
}
|
||||
|
||||
fn generate_random_id() -> Id {
|
||||
fn generate_random_id(version: KeySetVersion) -> Id {
|
||||
match version {
|
||||
KeySetVersion::Version00 => {
|
||||
let mut rand_bytes = vec![0u8; 8];
|
||||
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)))
|
||||
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]
|
||||
fn test_id_serialization() {
|
||||
let id = generate_random_id();
|
||||
let id = generate_random_id(KeySetVersion::Version00);
|
||||
let id_str = id.to_string();
|
||||
|
||||
assert!(id_str.chars().all(|c| c.is_ascii_hexdigit()));
|
||||
@@ -565,6 +863,16 @@ mod test {
|
||||
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]
|
||||
fn test_id_deserialization() {
|
||||
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());
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,7 +91,19 @@ pub async fn pay_request(
|
||||
},
|
||||
)
|
||||
.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 {
|
||||
let payload = PaymentRequestPayload {
|
||||
|
||||
@@ -19,7 +19,7 @@ async fn setup_keyset<E: Debug, DB: Database<E> + KeysDatabase<Err = E>>(db: &DB
|
||||
unit: CurrencyUnit::Sat,
|
||||
active: true,
|
||||
valid_from: 0,
|
||||
valid_to: None,
|
||||
final_expiry: None,
|
||||
derivation_path: bitcoin::bip32::DerivationPath::from_str("m/0'/0'/0'").unwrap(),
|
||||
derivation_path_index: Some(0),
|
||||
max_order: 32,
|
||||
|
||||
@@ -4,6 +4,7 @@ use std::collections::HashMap;
|
||||
use std::fmt::Debug;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use cashu::KeySet;
|
||||
|
||||
use super::Error;
|
||||
use crate::common::ProofInfo;
|
||||
@@ -72,7 +73,7 @@ pub trait Database: Debug {
|
||||
async fn remove_melt_quote(&self, quote_id: &str) -> Result<(), Self::Err>;
|
||||
|
||||
/// 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
|
||||
async fn get_keys(&self, id: &Id) -> Result<Option<Keys>, Self::Err>;
|
||||
/// Remove [`Keys`] from storage
|
||||
|
||||
@@ -139,9 +139,6 @@ pub struct MintKeySetInfo {
|
||||
pub active: bool,
|
||||
/// Starting unix time Keyset is valid from
|
||||
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
|
||||
pub derivation_path: DerivationPath,
|
||||
/// DerivationPath index of Keyset
|
||||
@@ -151,6 +148,8 @@ pub struct MintKeySetInfo {
|
||||
/// Input Fee ppk
|
||||
#[serde(default = "default_fee")]
|
||||
pub input_fee_ppk: u64,
|
||||
/// Final expiry
|
||||
pub final_expiry: Option<u64>,
|
||||
}
|
||||
|
||||
/// Default fee
|
||||
@@ -165,6 +164,7 @@ impl From<MintKeySetInfo> for KeySetInfo {
|
||||
unit: keyset_info.unit,
|
||||
active: keyset_info.active,
|
||||
input_fee_ppk: keyset_info.input_fee_ppk,
|
||||
final_expiry: keyset_info.final_expiry,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,10 +77,11 @@ async fn test_swap_to_send() {
|
||||
)
|
||||
.await
|
||||
.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!(
|
||||
Amount::from(40),
|
||||
token
|
||||
.proofs()
|
||||
token_proofs
|
||||
.total_amount()
|
||||
.expect("Failed to get total amount")
|
||||
);
|
||||
@@ -92,7 +93,7 @@ async fn test_swap_to_send() {
|
||||
.expect("Failed to get balance")
|
||||
);
|
||||
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(
|
||||
wallet_alice
|
||||
.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
|
||||
.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(0), transaction.fee);
|
||||
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
|
||||
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");
|
||||
let received_amount = wallet_carol
|
||||
.receive_proofs(
|
||||
token.proofs(),
|
||||
token_proofs.clone(),
|
||||
ReceiveOptions::default(),
|
||||
token.memo().clone(),
|
||||
)
|
||||
@@ -149,7 +151,7 @@ async fn test_swap_to_send() {
|
||||
assert_eq!(Amount::from(40), transaction.amount);
|
||||
assert_eq!(Amount::from(0), transaction.fee);
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -237,8 +239,8 @@ async fn test_mint_double_spend() {
|
||||
.await
|
||||
.expect("Could not get proofs");
|
||||
|
||||
let keys = mint_bob.pubkeys().keysets.first().unwrap().clone().keys;
|
||||
let keyset_id = Id::from(&keys);
|
||||
let keys = mint_bob.pubkeys().keysets.first().unwrap().clone();
|
||||
let keyset_id = keys.id;
|
||||
|
||||
let preswap = PreMintSecrets::random(
|
||||
keyset_id,
|
||||
@@ -294,8 +296,8 @@ async fn test_attempt_to_swap_by_overflowing() {
|
||||
|
||||
let amount = 2_u64.pow(63);
|
||||
|
||||
let keys = mint_bob.pubkeys().keysets.first().unwrap().clone().keys;
|
||||
let keyset_id = Id::from(&keys);
|
||||
let keys = mint_bob.pubkeys().keysets.first().unwrap().clone();
|
||||
let keyset_id = keys.id;
|
||||
|
||||
let pre_mint_amount =
|
||||
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");
|
||||
|
||||
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();
|
||||
|
||||
@@ -597,8 +599,8 @@ async fn test_mint_enforce_fee() {
|
||||
.await
|
||||
.expect("Could not get proofs");
|
||||
|
||||
let keys = mint_bob.pubkeys().keysets.first().unwrap().clone().keys;
|
||||
let keyset_id = Id::from(&keys);
|
||||
let keys = mint_bob.pubkeys().keysets.first().unwrap().clone();
|
||||
let keyset_id = keys.id;
|
||||
|
||||
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 {
|
||||
let keys = mint.pubkeys().keysets.first().unwrap().clone().keys;
|
||||
Id::from(&keys)
|
||||
let keys = mint.pubkeys().keysets.first().unwrap().clone();
|
||||
keys.verify_id()
|
||||
.expect("Keyset ID generation is successful");
|
||||
keys.id
|
||||
}
|
||||
|
||||
@@ -13,7 +13,8 @@ use cdk_common::mint_url::MintUrl;
|
||||
use cdk_common::util::unix_time;
|
||||
use cdk_common::wallet::{self, MintQuote, Transaction, TransactionDirection, TransactionId};
|
||||
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 tracing::instrument;
|
||||
@@ -493,15 +494,19 @@ impl WalletDatabase for WalletRedbDatabase {
|
||||
}
|
||||
|
||||
#[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)?;
|
||||
|
||||
keyset.verify_id()?;
|
||||
|
||||
{
|
||||
let mut table = write_txn.open_table(MINT_KEYS_TABLE).map_err(Error::from)?;
|
||||
table
|
||||
.insert(
|
||||
Id::from(&keys).to_string().as_str(),
|
||||
serde_json::to_string(&keys).map_err(Error::from)?.as_str(),
|
||||
keyset.id.to_string().as_str(),
|
||||
serde_json::to_string(&keyset.keys)
|
||||
.map_err(Error::from)?
|
||||
.as_str(),
|
||||
)
|
||||
.map_err(Error::from)?;
|
||||
}
|
||||
|
||||
@@ -547,7 +547,10 @@ impl WalletDatabase for WalletRexieDatabase {
|
||||
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 transaction = rexie
|
||||
@@ -556,7 +559,7 @@ impl WalletDatabase for WalletRexieDatabase {
|
||||
|
||||
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)?;
|
||||
|
||||
keys_store
|
||||
|
||||
@@ -68,6 +68,8 @@ pub async fn init_keysets(
|
||||
highest_index_keyset.max_order,
|
||||
highest_index_keyset.unit.clone(),
|
||||
highest_index_keyset.derivation_path.clone(),
|
||||
highest_index_keyset.final_expiry,
|
||||
cdk_common::nut02::KeySetVersion::Version00,
|
||||
);
|
||||
active_keysets.insert(id, keyset);
|
||||
let mut keyset_info = highest_index_keyset;
|
||||
@@ -97,6 +99,8 @@ pub async fn init_keysets(
|
||||
unit.clone(),
|
||||
*max_order,
|
||||
*input_fee_ppk,
|
||||
// TODO: add Mint settings for a final expiry of newly generated keysets
|
||||
None,
|
||||
);
|
||||
|
||||
let id = keyset_info.id;
|
||||
@@ -114,6 +118,7 @@ pub async fn init_keysets(
|
||||
|
||||
/// Generate new [`MintKeySetInfo`] from path
|
||||
#[tracing::instrument(skip_all)]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn create_new_keyset<C: secp256k1::Signing>(
|
||||
secp: &secp256k1::Secp256k1<C>,
|
||||
xpriv: Xpriv,
|
||||
@@ -122,6 +127,7 @@ pub fn create_new_keyset<C: secp256k1::Signing>(
|
||||
unit: CurrencyUnit,
|
||||
max_order: u8,
|
||||
input_fee_ppk: u64,
|
||||
final_expiry: Option<u64>,
|
||||
) -> (MintKeySet, MintKeySetInfo) {
|
||||
let keyset = MintKeySet::generate(
|
||||
secp,
|
||||
@@ -130,13 +136,16 @@ pub fn create_new_keyset<C: secp256k1::Signing>(
|
||||
.expect("RNG busted"),
|
||||
unit,
|
||||
max_order,
|
||||
final_expiry,
|
||||
// TODO: change this to Version01 to generate keysets v2
|
||||
cdk_common::nut02::KeySetVersion::Version00,
|
||||
);
|
||||
let keyset_info = MintKeySetInfo {
|
||||
id: keyset.id,
|
||||
unit: keyset.unit.clone(),
|
||||
active: true,
|
||||
valid_from: unix_time(),
|
||||
valid_to: None,
|
||||
final_expiry: keyset.final_expiry,
|
||||
derivation_path,
|
||||
derivation_path_index,
|
||||
max_order,
|
||||
|
||||
@@ -72,6 +72,8 @@ impl DbSignatory {
|
||||
unit.clone(),
|
||||
max_order,
|
||||
fee,
|
||||
// TODO: add and connect settings for this
|
||||
None,
|
||||
);
|
||||
|
||||
let id = keyset_info.id;
|
||||
@@ -130,6 +132,8 @@ impl DbSignatory {
|
||||
keyset_info.max_order,
|
||||
keyset_info.unit.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.max_order,
|
||||
args.input_fee_ppk,
|
||||
// TODO: add and connect settings for this
|
||||
None,
|
||||
);
|
||||
let id = info.id;
|
||||
self.localstore.add_keyset_info(info.clone()).await?;
|
||||
@@ -266,6 +272,8 @@ mod test {
|
||||
2,
|
||||
CurrencyUnit::Sat,
|
||||
derivation_path_from_unit(CurrencyUnit::Sat, 0).unwrap(),
|
||||
None,
|
||||
cdk_common::nut02::KeySetVersion::Version00,
|
||||
);
|
||||
|
||||
assert_eq!(keyset.unit, CurrencyUnit::Sat);
|
||||
@@ -310,6 +318,8 @@ mod test {
|
||||
2,
|
||||
CurrencyUnit::Sat,
|
||||
derivation_path_from_unit(CurrencyUnit::Sat, 0).unwrap(),
|
||||
None,
|
||||
cdk_common::nut02::KeySetVersion::Version00,
|
||||
);
|
||||
|
||||
assert_eq!(keyset.unit, CurrencyUnit::Sat);
|
||||
|
||||
@@ -60,6 +60,7 @@ impl TryInto<crate::signatory::SignatoryKeySet> for KeySet {
|
||||
.map(|(amount, pk)| PublicKey::from_slice(&pk).map(|pk| (amount.into(), pk)))
|
||||
.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()))
|
||||
.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)))
|
||||
.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,
|
||||
input_fee_ppk: value.input_fee_ppk,
|
||||
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()))?,
|
||||
active: self.active,
|
||||
input_fee_ppk: self.input_fee_ppk,
|
||||
final_expiry: self.final_expiry,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ message KeySet {
|
||||
bool active = 3;
|
||||
uint64 input_fee_ppk = 4;
|
||||
Keys keys = 5;
|
||||
optional uint64 final_expiry = 6;
|
||||
}
|
||||
|
||||
message Keys {
|
||||
|
||||
@@ -73,6 +73,8 @@ pub struct SignatoryKeySet {
|
||||
pub keys: Keys,
|
||||
/// Information about the fee per public key
|
||||
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 {
|
||||
@@ -87,6 +89,7 @@ impl From<SignatoryKeySet> for KeySet {
|
||||
id: val.id,
|
||||
unit: val.unit,
|
||||
keys: val.keys,
|
||||
final_expiry: val.final_expiry,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -107,7 +110,7 @@ impl From<SignatoryKeySet> for MintKeySetInfo {
|
||||
derivation_path: Default::default(),
|
||||
derivation_path_index: Default::default(),
|
||||
max_order: 0,
|
||||
valid_to: None,
|
||||
final_expiry: val.final_expiry,
|
||||
valid_from: 0,
|
||||
}
|
||||
}
|
||||
@@ -121,6 +124,7 @@ impl From<&(MintKeySetInfo, MintKeySet)> for SignatoryKeySet {
|
||||
active: info.active,
|
||||
input_fee_ppk: info.input_fee_ppk,
|
||||
keys: key.keys.clone().into(),
|
||||
final_expiry: key.final_expiry,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ impl MintAuthDatabase for MintSqliteAuthDatabase {
|
||||
.bind(":unit", keyset.unit.to_string())
|
||||
.bind(":active", keyset.active)
|
||||
.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(":max_order", keyset.max_order)
|
||||
.bind(":derivation_path_index", keyset.derivation_path_index)
|
||||
|
||||
@@ -214,7 +214,7 @@ impl MintKeysDatabase for MintSqliteDatabase {
|
||||
.bind(":unit", keyset.unit.to_string())
|
||||
.bind(":active", keyset.active)
|
||||
.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(":max_order", keyset.max_order)
|
||||
.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),
|
||||
active: matches!(active, Column::Integer(1)),
|
||||
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_index: column_as_nullable_number!(derivation_path_index),
|
||||
max_order: column_as_number!(max_order),
|
||||
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,
|
||||
active: true,
|
||||
valid_from: 0,
|
||||
valid_to: None,
|
||||
derivation_path: bitcoin::bip32::DerivationPath::from_str("m/0'/0'/0'").unwrap(),
|
||||
derivation_path_index: Some(0),
|
||||
max_order: 32,
|
||||
input_fee_ppk: 0,
|
||||
final_expiry: None,
|
||||
};
|
||||
db.add_keyset_info(keyset_info).await.unwrap();
|
||||
|
||||
@@ -1387,11 +1387,11 @@ mod tests {
|
||||
unit: CurrencyUnit::Sat,
|
||||
active: true,
|
||||
valid_from: 0,
|
||||
valid_to: None,
|
||||
derivation_path: bitcoin::bip32::DerivationPath::from_str("m/0'/0'/0'").unwrap(),
|
||||
derivation_path_index: Some(0),
|
||||
max_order: 32,
|
||||
input_fee_ppk: 0,
|
||||
final_expiry: None,
|
||||
};
|
||||
db.add_keyset_info(keyset_info).await.unwrap();
|
||||
|
||||
|
||||
@@ -16,4 +16,5 @@ pub static MIGRATIONS: &[(&str, &str)] = &[
|
||||
("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"#)),
|
||||
("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"#)),
|
||||
];
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE keyset ADD COLUMN final_expiry INTEGER DEFAULT NULL;
|
||||
@@ -14,8 +14,8 @@ use cdk_common::nuts::{MeltQuoteState, MintQuoteState};
|
||||
use cdk_common::secret::Secret;
|
||||
use cdk_common::wallet::{self, MintQuote, Transaction, TransactionDirection, TransactionId};
|
||||
use cdk_common::{
|
||||
database, Amount, CurrencyUnit, Id, KeySetInfo, Keys, MintInfo, Proof, ProofDleq, PublicKey,
|
||||
SecretKey, SpendingConditions, State,
|
||||
database, Amount, CurrencyUnit, Id, KeySet, KeySetInfo, Keys, MintInfo, Proof, ProofDleq,
|
||||
PublicKey, SecretKey, SpendingConditions, State,
|
||||
};
|
||||
use error::Error;
|
||||
use tracing::instrument;
|
||||
@@ -294,14 +294,15 @@ ON CONFLICT(mint_url) DO UPDATE SET
|
||||
Statement::new(
|
||||
r#"
|
||||
INSERT INTO keyset
|
||||
(mint_url, id, unit, active, input_fee_ppk)
|
||||
(mint_url, id, unit, active, input_fee_ppk, final_expiry)
|
||||
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
|
||||
mint_url = excluded.mint_url,
|
||||
unit = excluded.unit,
|
||||
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())
|
||||
@@ -309,6 +310,7 @@ ON CONFLICT(mint_url) DO UPDATE SET
|
||||
.bind(":unit", keyset.unit.to_string())
|
||||
.bind(":active", keyset.active)
|
||||
.bind(":input_fee_ppk", keyset.input_fee_ppk as i64)
|
||||
.bind(":final_expiry", keyset.final_expiry.map(|v| v as i64))
|
||||
.execute(&conn)
|
||||
.map_err(Error::Sqlite)?;
|
||||
}
|
||||
@@ -327,7 +329,8 @@ ON CONFLICT(mint_url) DO UPDATE SET
|
||||
id,
|
||||
unit,
|
||||
active,
|
||||
input_fee_ppk
|
||||
input_fee_ppk,
|
||||
final_expiry
|
||||
FROM
|
||||
keyset
|
||||
WHERE mint_url = :mint_url
|
||||
@@ -354,7 +357,8 @@ ON CONFLICT(mint_url) DO UPDATE SET
|
||||
id,
|
||||
unit,
|
||||
active,
|
||||
input_fee_ppk
|
||||
input_fee_ppk,
|
||||
final_expiry
|
||||
FROM
|
||||
keyset
|
||||
WHERE id = :id
|
||||
@@ -528,7 +532,10 @@ ON CONFLICT(id) DO UPDATE SET
|
||||
}
|
||||
|
||||
#[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(
|
||||
r#"
|
||||
INSERT INTO key
|
||||
@@ -539,8 +546,11 @@ ON CONFLICT(id) DO UPDATE SET
|
||||
keys = excluded.keys
|
||||
"#,
|
||||
)
|
||||
.bind(":id", Id::from(&keys).to_string())
|
||||
.bind(":keys", serde_json::to_string(&keys).map_err(Error::from)?)
|
||||
.bind(":id", keyset.id.to_string())
|
||||
.bind(
|
||||
":keys",
|
||||
serde_json::to_string(&keyset.keys).map_err(Error::from)?,
|
||||
)
|
||||
.execute(&self.pool.get().map_err(Error::Pool)?)
|
||||
.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> {
|
||||
unpack_into!(
|
||||
let (
|
||||
id,
|
||||
unit,
|
||||
active,
|
||||
input_fee_ppk
|
||||
input_fee_ppk,
|
||||
final_expiry
|
||||
) = row
|
||||
);
|
||||
|
||||
@@ -924,6 +936,7 @@ fn sqlite_row_to_keyset(row: Vec<Column>) -> Result<KeySetInfo, Error> {
|
||||
unit: column_as_string!(unit, CurrencyUnit::from_str),
|
||||
active: matches!(active, Column::Integer(1)),
|
||||
input_fee_ppk: column_as_nullable_number!(input_fee_ppk).unwrap_or_default(),
|
||||
final_expiry: column_as_nullable_number!(final_expiry),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ impl Mint {
|
||||
unit: key.unit.clone(),
|
||||
active: key.active,
|
||||
input_fee_ppk: key.input_fee_ppk,
|
||||
final_expiry: key.final_expiry,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
|
||||
@@ -53,6 +53,7 @@ impl Mint {
|
||||
unit: k.unit.clone(),
|
||||
active: k.active,
|
||||
input_fee_ppk: k.input_fee_ppk,
|
||||
final_expiry: k.final_expiry,
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
|
||||
@@ -180,7 +180,7 @@ impl AuthWallet {
|
||||
|
||||
keys.verify_id()?;
|
||||
|
||||
self.localstore.add_keys(keys.keys.clone()).await?;
|
||||
self.localstore.add_keys(keys.clone()).await?;
|
||||
|
||||
keys.keys
|
||||
};
|
||||
|
||||
@@ -19,7 +19,7 @@ impl Wallet {
|
||||
|
||||
keys.verify_id()?;
|
||||
|
||||
self.localstore.add_keys(keys.keys.clone()).await?;
|
||||
self.localstore.add_keys(keys.clone()).await?;
|
||||
|
||||
keys.keys
|
||||
};
|
||||
@@ -27,7 +27,23 @@ impl Wallet {
|
||||
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
|
||||
#[instrument(skip(self))]
|
||||
|
||||
@@ -474,7 +474,7 @@ impl Wallet {
|
||||
/// 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
|
||||
#[instrument(skip(self, token))]
|
||||
pub fn verify_token_p2pk(
|
||||
pub async fn verify_token_p2pk(
|
||||
&self,
|
||||
token: &Token,
|
||||
spending_conditions: SpendingConditions,
|
||||
@@ -526,8 +526,10 @@ impl Wallet {
|
||||
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 {
|
||||
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 {
|
||||
let mint_pubkey = match keys_cache.get(&proof.keyset_id) {
|
||||
Some(keys) => keys.amount_key(proof.amount),
|
||||
|
||||
@@ -271,12 +271,6 @@ impl MultiMintWallet {
|
||||
let token_data = Token::from_str(encoded_token)?;
|
||||
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()?;
|
||||
|
||||
// Check that all mints in tokes have wallets
|
||||
@@ -291,6 +285,22 @@ impl MultiMintWallet {
|
||||
.get(&wallet_key)
|
||||
.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
|
||||
.receive_proofs(proofs, opts, token_data.memo().clone())
|
||||
.await
|
||||
@@ -356,7 +366,7 @@ impl MultiMintWallet {
|
||||
.get(wallet_key)
|
||||
.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
|
||||
|
||||
@@ -219,7 +219,8 @@ impl Wallet {
|
||||
|
||||
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 {
|
||||
ensure_cdk!(!token.is_multi_mint(), Error::MultiMintTokenNotSupported);
|
||||
|
||||
Reference in New Issue
Block a user