refactor: Mint to use bip32 derivation and not store priv keys

This commit is contained in:
David Caseria
2024-04-24 14:22:52 -04:00
committed by thesimplekid
parent d65a0fac73
commit a5a50f281c
6 changed files with 206 additions and 121 deletions

View File

@@ -5,9 +5,8 @@ use std::sync::Arc;
use async_trait::async_trait;
use cdk::cdk_database::{self, MintDatabase};
use cdk::dhke::hash_to_curve;
use cdk::nuts::{
BlindSignature, CurrencyUnit, Id, MintInfo, MintKeySet as KeySet, Proof, PublicKey,
};
use cdk::mint::MintKeySetInfo;
use cdk::nuts::{BlindSignature, CurrencyUnit, Id, MintInfo, Proof, PublicKey};
use cdk::secret::Secret;
use cdk::types::{MeltQuote, MintQuote};
use redb::{Database, ReadableTable, TableDefinition};
@@ -167,7 +166,7 @@ impl MintDatabase for MintRedbDatabase {
Ok(active_keysets)
}
async fn add_keyset(&self, keyset: KeySet) -> Result<(), Self::Err> {
async fn add_keyset_info(&self, keyset: MintKeySetInfo) -> Result<(), Self::Err> {
let db = self.db.lock().await;
let write_txn = db.begin_write().map_err(Error::from)?;
@@ -176,7 +175,7 @@ impl MintDatabase for MintRedbDatabase {
let mut table = write_txn.open_table(KEYSETS_TABLE).map_err(Error::from)?;
table
.insert(
Id::from(keyset.clone()).to_string().as_str(),
keyset.id.to_string().as_str(),
serde_json::to_string(&keyset)
.map_err(Error::from)?
.as_str(),
@@ -188,7 +187,7 @@ impl MintDatabase for MintRedbDatabase {
Ok(())
}
async fn get_keyset(&self, keyset_id: &Id) -> Result<Option<KeySet>, Self::Err> {
async fn get_keyset_info(&self, keyset_id: &Id) -> Result<Option<MintKeySetInfo>, Self::Err> {
let db = self.db.lock().await;
let read_txn = db.begin_read().map_err(Error::from)?;
let table = read_txn.open_table(KEYSETS_TABLE).map_err(Error::from)?;
@@ -202,7 +201,7 @@ impl MintDatabase for MintRedbDatabase {
}
}
async fn get_keysets(&self) -> Result<Vec<KeySet>, Self::Err> {
async fn get_keyset_infos(&self) -> Result<Vec<MintKeySetInfo>, Self::Err> {
let db = self.db.lock().await;
let read_txn = db.begin_read().map_err(Error::from)?;
let table = read_txn.open_table(KEYSETS_TABLE).map_err(Error::from)?;

View File

@@ -6,7 +6,7 @@ use tokio::sync::Mutex;
use super::{Error, MintDatabase};
use crate::dhke::hash_to_curve;
use crate::nuts::nut02::MintKeySet;
use crate::mint::MintKeySetInfo;
use crate::nuts::{BlindSignature, CurrencyUnit, Id, MintInfo, Proof, Proofs, PublicKey};
use crate::secret::Secret;
use crate::types::{MeltQuote, MintQuote};
@@ -15,7 +15,7 @@ use crate::types::{MeltQuote, MintQuote};
pub struct MintMemoryDatabase {
mint_info: Arc<Mutex<MintInfo>>,
active_keysets: Arc<Mutex<HashMap<CurrencyUnit, Id>>>,
keysets: Arc<Mutex<HashMap<Id, MintKeySet>>>,
keysets: Arc<Mutex<HashMap<Id, MintKeySetInfo>>>,
mint_quotes: Arc<Mutex<HashMap<String, MintQuote>>>,
melt_quotes: Arc<Mutex<HashMap<String, MeltQuote>>>,
pending_proofs: Arc<Mutex<HashMap<[u8; 33], Proof>>>,
@@ -28,7 +28,7 @@ impl MintMemoryDatabase {
pub fn new(
mint_info: MintInfo,
active_keysets: HashMap<CurrencyUnit, Id>,
keysets: Vec<MintKeySet>,
keysets: Vec<MintKeySetInfo>,
mint_quotes: Vec<MintQuote>,
melt_quotes: Vec<MeltQuote>,
pending_proofs: Proofs,
@@ -87,16 +87,16 @@ impl MintDatabase for MintMemoryDatabase {
Ok(self.active_keysets.lock().await.clone())
}
async fn add_keyset(&self, keyset: MintKeySet) -> Result<(), Error> {
async fn add_keyset_info(&self, keyset: MintKeySetInfo) -> Result<(), Error> {
self.keysets.lock().await.insert(keyset.id, keyset);
Ok(())
}
async fn get_keyset(&self, keyset_id: &Id) -> Result<Option<MintKeySet>, Error> {
async fn get_keyset_info(&self, keyset_id: &Id) -> Result<Option<MintKeySetInfo>, Error> {
Ok(self.keysets.lock().await.get(keyset_id).cloned())
}
async fn get_keysets(&self) -> Result<Vec<MintKeySet>, Error> {
async fn get_keyset_infos(&self) -> Result<Vec<MintKeySetInfo>, Error> {
Ok(self.keysets.lock().await.values().cloned().collect())
}

View File

@@ -1,16 +1,25 @@
//! CDK Database
#[cfg(any(feature = "wallet", feature = "mint"))]
use std::collections::HashMap;
#[cfg(any(feature = "wallet", feature = "mint"))]
use async_trait::async_trait;
use thiserror::Error;
use crate::nuts::{
BlindSignature, CurrencyUnit, Id, KeySetInfo, Keys, MintInfo, MintKeySet, Proof, Proofs,
PublicKey,
};
#[cfg(feature = "mint")]
use crate::mint::MintKeySetInfo;
#[cfg(feature = "mint")]
use crate::nuts::{BlindSignature, CurrencyUnit, Proof, PublicKey};
#[cfg(any(feature = "wallet", feature = "mint"))]
use crate::nuts::{Id, MintInfo};
#[cfg(feature = "wallet")]
use crate::nuts::{KeySetInfo, Keys, Proofs};
#[cfg(feature = "mint")]
use crate::secret::Secret;
#[cfg(any(feature = "wallet", feature = "mint"))]
use crate::types::{MeltQuote, MintQuote};
#[cfg(feature = "wallet")]
use crate::url::UncheckedUrl;
#[cfg(feature = "mint")]
@@ -26,6 +35,7 @@ pub enum Error {
Cdk(#[from] crate::error::Error),
}
#[cfg(feature = "wallet")]
#[async_trait]
pub trait WalletDatabase {
type Err: Into<Error> + From<Error>;
@@ -81,6 +91,7 @@ pub trait WalletDatabase {
async fn get_keyset_counter(&self, keyset_id: &Id) -> Result<Option<u64>, Self::Err>;
}
#[cfg(feature = "mint")]
#[async_trait]
pub trait MintDatabase {
type Err: Into<Error> + From<Error>;
@@ -102,9 +113,9 @@ pub trait MintDatabase {
async fn get_melt_quotes(&self) -> Result<Vec<MeltQuote>, Self::Err>;
async fn remove_melt_quote(&self, quote_id: &str) -> Result<(), Self::Err>;
async fn add_keyset(&self, keyset: MintKeySet) -> Result<(), Self::Err>;
async fn get_keyset(&self, id: &Id) -> Result<Option<MintKeySet>, Self::Err>;
async fn get_keysets(&self) -> Result<Vec<MintKeySet>, Self::Err>;
async fn add_keyset_info(&self, keyset: MintKeySetInfo) -> Result<(), Self::Err>;
async fn get_keyset_info(&self, id: &Id) -> Result<Option<MintKeySetInfo>, Self::Err>;
async fn get_keyset_infos(&self) -> Result<Vec<MintKeySetInfo>, Self::Err>;
async fn add_spent_proof(&self, proof: Proof) -> Result<(), Self::Err>;
async fn get_spent_proof_by_secret(&self, secret: &Secret) -> Result<Option<Proof>, Self::Err>;

View File

@@ -1,10 +1,12 @@
use std::collections::HashSet;
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use bip39::Mnemonic;
use bitcoin::bip32::{ChildNumber, DerivationPath, ExtendedPrivKey};
use bitcoin::secp256k1::{self, Secp256k1};
use http::StatusCode;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use tokio::sync::RwLock;
use tracing::{debug, error, info};
use crate::cdk_database::{self, MintDatabase};
@@ -12,6 +14,7 @@ use crate::dhke::{hash_to_curve, sign_message, verify_message};
use crate::error::ErrorResponse;
use crate::nuts::*;
use crate::types::{MeltQuote, MintQuote};
use crate::util::unix_time;
use crate::Amount;
#[derive(Debug, Error)]
@@ -83,52 +86,43 @@ impl From<Error> for (StatusCode, ErrorResponse) {
#[derive(Clone)]
pub struct Mint {
// pub pubkey: PublicKey
mnemonic: Mnemonic,
keysets: Arc<RwLock<HashMap<Id, MintKeySet>>>,
secp_ctx: Secp256k1<secp256k1::All>,
xpriv: ExtendedPrivKey,
pub fee_reserve: FeeReserve,
pub localstore: Arc<dyn MintDatabase<Err = cdk_database::Error> + Send + Sync>,
}
impl Mint {
pub async fn new(
seed: &[u8],
localstore: Arc<dyn MintDatabase<Err = cdk_database::Error> + Send + Sync>,
mnemonic: Mnemonic,
keysets_info: HashSet<MintKeySetInfo>,
min_fee_reserve: Amount,
percent_fee_reserve: f32,
) -> Result<Self, Error> {
let mut active_units: HashSet<CurrencyUnit> = HashSet::default();
let secp_ctx = Secp256k1::new();
let xpriv =
ExtendedPrivKey::new_master(bitcoin::Network::Bitcoin, seed).expect("RNG busted");
let mut keysets = HashMap::new();
let keysets_info = localstore.get_keyset_infos().await?;
if keysets_info.is_empty() {
let keyset =
MintKeySet::generate(&mnemonic.to_seed_normalized(""), CurrencyUnit::Sat, "", 64);
localstore
.add_active_keyset(CurrencyUnit::Sat, keyset.id)
.await?;
localstore.add_keyset(keyset).await?;
} else {
// Check that there is only one active keyset per unit
for keyset_info in keysets_info {
if keyset_info.active && !active_units.insert(keyset_info.unit.clone()) {
// TODO: Handle Error
todo!()
}
let keyset = MintKeySet::generate(
&mnemonic.to_seed_normalized(""),
keyset_info.unit.clone(),
&keyset_info.derivation_path.clone(),
keyset_info.max_order,
);
localstore.add_keyset(keyset).await?;
}
let derivation_path = DerivationPath::from(vec![
ChildNumber::from_hardened_idx(0).expect("0 is a valid index")
]);
let (keyset, keyset_info) =
create_new_keyset(&secp_ctx, xpriv, derivation_path, CurrencyUnit::Sat, 64);
let id = keyset_info.id;
localstore.add_active_keyset(CurrencyUnit::Sat, id).await?;
localstore.add_keyset_info(keyset_info).await?;
keysets.insert(id, keyset);
}
Ok(Self {
keysets: Arc::new(RwLock::new(keysets)),
secp_ctx,
xpriv,
localstore,
mnemonic,
fee_reserve: FeeReserve {
min_fee_reserve,
percent_fee_reserve,
@@ -199,13 +193,9 @@ impl Mint {
/// Retrieve the public keys of the active keyset for distribution to
/// wallet clients
pub async fn keyset_pubkeys(&self, keyset_id: &Id) -> Result<KeysResponse, Error> {
let keyset = match self.localstore.get_keyset(keyset_id).await? {
Some(keyset) => keyset.clone(),
None => {
return Err(Error::UnknownKeySet);
}
};
self.ensure_keyset_loaded(keyset_id).await?;
let keysets = self.keysets.read().await;
let keyset = keysets.get(keyset_id).ok_or(Error::UnknownKeySet)?.clone();
Ok(KeysResponse {
keysets: vec![keyset.into()],
})
@@ -214,16 +204,19 @@ impl Mint {
/// Retrieve the public keys of the active keyset for distribution to
/// wallet clients
pub async fn pubkeys(&self) -> Result<KeysResponse, Error> {
let keysets = self.localstore.get_keysets().await?;
let keyset_infos = self.localstore.get_keyset_infos().await?;
for keyset_info in keyset_infos {
self.ensure_keyset_loaded(&keyset_info.id).await?;
}
let keysets = self.keysets.read().await;
Ok(KeysResponse {
keysets: keysets.into_iter().map(|k| k.into()).collect(),
keysets: keysets.values().map(|k| k.clone().into()).collect(),
})
}
/// Return a list of all supported keysets
pub async fn keysets(&self) -> Result<KeysetResponse, Error> {
let keysets = self.localstore.get_keysets().await?;
let keysets = self.localstore.get_keyset_infos().await?;
let active_keysets: HashSet<Id> = self
.localstore
.get_active_keysets()
@@ -245,11 +238,10 @@ impl Mint {
}
pub async fn keyset(&self, id: &Id) -> Result<Option<KeySet>, Error> {
Ok(self
.localstore
.get_keyset(id)
.await?
.map(|ks| ks.clone().into()))
self.ensure_keyset_loaded(id).await?;
let keysets = self.keysets.read().await;
let keyset = keysets.get(id).map(|k| k.clone().into());
Ok(keyset)
}
/// Add current keyset to inactive keysets
@@ -257,21 +249,22 @@ impl Mint {
pub async fn rotate_keyset(
&mut self,
unit: CurrencyUnit,
derivation_path: &str,
derivation_path: DerivationPath,
max_order: u8,
) -> Result<(), Error> {
let new_keyset = MintKeySet::generate(
&self.mnemonic.to_seed_normalized(""),
unit.clone(),
let (keyset, keyset_info) = create_new_keyset(
&self.secp_ctx,
self.xpriv,
derivation_path,
unit.clone(),
max_order,
);
let id = keyset_info.id;
self.localstore.add_keyset_info(keyset_info).await?;
self.localstore.add_active_keyset(unit, id).await?;
self.localstore.add_keyset(new_keyset.clone()).await?;
self.localstore
.add_active_keyset(unit, new_keyset.id)
.await?;
let mut keysets = self.keysets.write().await;
keysets.insert(id, keyset);
Ok(())
}
@@ -327,24 +320,27 @@ impl Mint {
keyset_id,
..
} = blinded_message;
self.ensure_keyset_loaded(keyset_id).await?;
let keyset = self
let keyset_info = self
.localstore
.get_keyset(keyset_id)
.get_keyset_info(keyset_id)
.await?
.ok_or(Error::UnknownKeySet)?;
let active = self
.localstore
.get_active_keyset_id(&keyset.unit)
.get_active_keyset_id(&keyset_info.unit)
.await?
.ok_or(Error::InactiveKeyset)?;
// Check that the keyset is active and should be used to sign
if keyset.id.ne(&active) {
if keyset_info.id.ne(&active) {
return Err(Error::InactiveKeyset);
}
let keysets = self.keysets.read().await;
let keyset = keysets.get(keyset_id).ok_or(Error::UnknownKeySet)?;
let Some(key_pair) = keyset.keys.get(amount) else {
// No key for amount
return Err(Error::AmountKey);
@@ -355,7 +351,7 @@ impl Mint {
let blinded_signature = BlindSignature::new(
*amount,
c,
keyset.id,
keyset_info.id,
&blinded_message.blinded_secret,
key_pair.secret_key.clone(),
)?;
@@ -416,7 +412,7 @@ impl Mint {
for id in input_keyset_ids {
let keyset = self
.localstore
.get_keyset(&id)
.get_keyset_info(&id)
.await?
.ok_or(Error::UnknownKeySet)?;
keyset_units.insert(keyset.unit);
@@ -428,7 +424,7 @@ impl Mint {
for id in &output_keyset_ids {
let keyset = self
.localstore
.get_keyset(id)
.get_keyset_info(id)
.await?
.ok_or(Error::UnknownKeySet)?;
@@ -483,12 +479,9 @@ impl Mint {
return Err(Error::TokenPending);
}
let keyset = self
.localstore
.get_keyset(&proof.keyset_id)
.await?
.ok_or(Error::UnknownKeySet)?;
self.ensure_keyset_loaded(&proof.keyset_id).await?;
let keysets = self.keysets.read().await;
let keyset = keysets.get(&proof.keyset_id).ok_or(Error::UnknownKeySet)?;
let Some(keypair) = keyset.keys.get(&proof.amount) else {
return Err(Error::AmountKey);
};
@@ -552,7 +545,7 @@ impl Mint {
for id in input_keyset_ids {
let keyset = self
.localstore
.get_keyset(&id)
.get_keyset_info(&id)
.await?
.ok_or(Error::UnknownKeySet)?;
keyset_units.insert(keyset.unit);
@@ -563,7 +556,7 @@ impl Mint {
for id in output_keysets_ids {
let keyset = self
.localstore
.get_keyset(&id)
.get_keyset_info(&id)
.await?
.ok_or(Error::UnknownKeySet)?;
@@ -734,6 +727,27 @@ impl Mint {
signatures,
})
}
async fn ensure_keyset_loaded(&self, id: &Id) -> Result<(), Error> {
let keysets = self.keysets.read().await;
if keysets.contains_key(id) {
return Ok(());
}
let mut keysets = self.keysets.write().await;
let keyset_info = self
.localstore
.get_keyset_info(id)
.await?
.ok_or(Error::UnknownKeySet)?;
let id = keyset_info.id;
keysets.insert(id, self.generate_keyset(keyset_info));
Ok(())
}
fn generate_keyset(&self, keyset_info: MintKeySetInfo) -> MintKeySet {
MintKeySet::generate_from_xpriv(&self.secp_ctx, self.xpriv, keyset_info)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
@@ -749,7 +763,7 @@ pub struct MintKeySetInfo {
pub active: bool,
pub valid_from: u64,
pub valid_to: Option<u64>,
pub derivation_path: String,
pub derivation_path: DerivationPath,
pub max_order: u8,
}
@@ -762,3 +776,30 @@ impl From<MintKeySetInfo> for KeySetInfo {
}
}
}
fn create_new_keyset<C: secp256k1::Signing>(
secp: &secp256k1::Secp256k1<C>,
xpriv: ExtendedPrivKey,
derivation_path: DerivationPath,
unit: CurrencyUnit,
max_order: u8,
) -> (MintKeySet, MintKeySetInfo) {
let keyset = MintKeySet::generate(
secp,
xpriv
.derive_priv(secp, &derivation_path)
.expect("RNG busted"),
unit,
max_order,
);
let keyset_info = MintKeySetInfo {
id: keyset.id,
unit: keyset.unit.clone(),
active: true,
valid_from: unix_time(),
valid_to: None,
derivation_path,
max_order,
};
(keyset, keyset_info)
}

View File

@@ -19,7 +19,9 @@ pub use nut00::{
Proofs, Token,
};
pub use nut01::{Keys, KeysResponse, PublicKey, SecretKey};
pub use nut02::{Id, KeySet, KeySetInfo, KeysetResponse, MintKeySet};
#[cfg(feature = "mint")]
pub use nut02::MintKeySet;
pub use nut02::{Id, KeySet, KeySetInfo, KeysetResponse};
#[cfg(feature = "wallet")]
pub use nut03::PreSwap;
pub use nut03::{SwapRequest, SwapResponse};

View File

@@ -5,17 +5,29 @@
use core::fmt;
use core::str::FromStr;
use std::array::TryFromSliceError;
#[cfg(feature = "mint")]
use std::collections::BTreeMap;
#[cfg(feature = "mint")]
use bitcoin::bip32::{ChildNumber, ExtendedPrivKey};
use bitcoin::hashes::sha256::Hash as Sha256;
use bitcoin::hashes::{Hash, HashEngine};
use bitcoin::hashes::Hash;
#[cfg(feature = "mint")]
use bitcoin::key::Secp256k1;
#[cfg(feature = "mint")]
use bitcoin::secp256k1;
use serde::{Deserialize, Deserializer, Serialize};
use serde_with::{serde_as, VecSkipError};
use thiserror::Error;
use super::nut01::{Keys, MintKeyPair, MintKeys, SecretKey};
use super::nut01::Keys;
#[cfg(feature = "mint")]
use super::nut01::{MintKeyPair, MintKeys};
#[cfg(feature = "mint")]
use crate::mint::MintKeySetInfo;
use crate::nuts::nut00::CurrencyUnit;
use crate::util::hex;
#[cfg(feature = "mint")]
use crate::Amount;
#[derive(Debug, Error)]
@@ -220,6 +232,7 @@ pub struct KeySet {
pub keys: Keys,
}
#[cfg(feature = "mint")]
impl From<MintKeySet> for KeySet {
fn from(keyset: MintKeySet) -> Self {
Self {
@@ -247,6 +260,7 @@ impl From<KeySet> for KeySetInfo {
}
}
#[cfg(feature = "mint")]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MintKeySet {
pub id: Id,
@@ -254,49 +268,66 @@ pub struct MintKeySet {
pub keys: MintKeys,
}
#[cfg(feature = "mint")]
impl MintKeySet {
pub fn generate(
secret: &[u8],
pub fn generate<C: secp256k1::Signing>(
secp: &Secp256k1<C>,
xpriv: ExtendedPrivKey,
unit: CurrencyUnit,
derivation_path: &str,
max_order: u8,
) -> Self {
// Elliptic curve math context
/* NUT-02 § 2.1
for i in range(MAX_ORDER):
k_i = HASH_SHA256(s + D + i)[:32]
*/
let mut map = BTreeMap::new();
// SHA-256 midstate, for quicker hashing
let mut engine = Sha256::engine();
engine.input(secret);
engine.input(derivation_path.as_bytes());
for i in 0..max_order {
let amount = Amount::from(2_u64.pow(i as u32));
// Reuse midstate
let mut e = engine.clone();
e.input(i.to_string().as_bytes());
let hash = Sha256::from_engine(e);
let secret_key = SecretKey::from_slice(&hash.to_byte_array()).unwrap(); // TODO: remove unwrap
let keypair = MintKeyPair::from_secret_key(secret_key);
map.insert(amount, keypair);
let secret_key = xpriv
.derive_priv(
secp,
&[ChildNumber::from_hardened_idx(i as u32).expect("order is valid index")],
)
.expect("RNG busted")
.private_key;
let public_key = secret_key.public_key(secp);
map.insert(
amount,
MintKeyPair {
secret_key: secret_key.into(),
public_key: public_key.into(),
},
);
}
let keys = MintKeys::new(map);
Self {
id: (&keys).into(),
unit,
keys,
}
}
pub fn generate_from_seed<C: secp256k1::Signing>(
secp: &Secp256k1<C>,
seed: &[u8],
info: MintKeySetInfo,
) -> Self {
let xpriv =
ExtendedPrivKey::new_master(bitcoin::Network::Bitcoin, seed).expect("RNG busted");
let max_order = info.max_order;
let unit = info.unit;
Self::generate(secp, xpriv, unit, max_order)
}
pub fn generate_from_xpriv<C: secp256k1::Signing>(
secp: &Secp256k1<C>,
xpriv: ExtendedPrivKey,
info: MintKeySetInfo,
) -> Self {
let max_order = info.max_order;
let unit = info.unit;
Self::generate(secp, xpriv, unit, max_order)
}
}
#[cfg(feature = "mint")]
impl From<MintKeySet> for Id {
fn from(keyset: MintKeySet) -> Id {
let keys: super::KeySet = keyset.into();
@@ -305,6 +336,7 @@ impl From<MintKeySet> for Id {
}
}
#[cfg(feature = "mint")]
impl From<&MintKeys> for Id {
fn from(map: &MintKeys) -> Self {
let keys: super::Keys = map.clone().into();