Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NUT13-only Wallet with provided seed #127

Merged
merged 4 commits into from
May 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ jobs:
-p cdk --no-default-features,
-p cdk --no-default-features --features wallet,
-p cdk --no-default-features --features mint,
-p cdk --no-default-features --features all-nuts,
-p cdk-redb
]
steps:
Expand Down Expand Up @@ -65,7 +64,6 @@ jobs:
-p cdk,
-p cdk --no-default-features,
-p cdk --no-default-features --features wallet,
-p cdk --no-default-features --features all-nuts,
-p cdk-js
]
steps:
Expand Down
2 changes: 1 addition & 1 deletion .helix/languages.toml
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
[language-server.rust-analyzer.config]
cargo = { features = [ "wallet", "mint", "all-nuts" ] }
cargo = { features = ["wallet", "mint"] }
1 change: 1 addition & 0 deletions bindings/cdk-js/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use wasm_bindgen::prelude::*;
pub mod error;
pub mod nuts;
pub mod types;
#[cfg(all(feature = "wallet", target_arch = "wasm32"))]
pub mod wallet;

#[wasm_bindgen(start)]
Expand Down
4 changes: 2 additions & 2 deletions bindings/cdk-js/src/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,11 @@ impl From<Wallet> for JsWallet {
#[wasm_bindgen(js_class = Wallet)]
impl JsWallet {
#[wasm_bindgen(constructor)]
pub async fn new() -> Self {
pub async fn new(seed: Vec<u8>) -> Self {
let client = HttpClient::new();
let db = RexieWalletDatabase::new().await.unwrap();

Wallet::new(client, Arc::new(db), None).await.into()
Wallet::new(client, Arc::new(db), &seed).await.into()
}

#[wasm_bindgen(js_name = totalBalance)]
Expand Down
6 changes: 3 additions & 3 deletions crates/cdk-redb/src/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const PROOFS_TABLE: MultimapTableDefinition<&str, &str> = MultimapTableDefinitio
const PENDING_PROOFS_TABLE: MultimapTableDefinition<&str, &str> =
MultimapTableDefinition::new("pending_proofs");
const CONFIG_TABLE: TableDefinition<&str, &str> = TableDefinition::new("config");
const KEYSET_COUNTER: TableDefinition<&str, u64> = TableDefinition::new("keyset_counter");
const KEYSET_COUNTER: TableDefinition<&str, u32> = TableDefinition::new("keyset_counter");

const DATABASE_VERSION: u32 = 0;

Expand Down Expand Up @@ -503,7 +503,7 @@ impl WalletDatabase for RedbWalletDatabase {
}

#[instrument(skip(self))]
async fn increment_keyset_counter(&self, keyset_id: &Id, count: u64) -> Result<(), Self::Err> {
async fn increment_keyset_counter(&self, keyset_id: &Id, count: u32) -> Result<(), Self::Err> {
let db = self.db.lock().await;

let current_counter;
Expand Down Expand Up @@ -535,7 +535,7 @@ impl WalletDatabase for RedbWalletDatabase {
}

#[instrument(skip(self))]
async fn get_keyset_counter(&self, keyset_id: &Id) -> Result<Option<u64>, Self::Err> {
async fn get_keyset_counter(&self, keyset_id: &Id) -> Result<Option<u32>, Self::Err> {
let db = self.db.lock().await;
let read_txn = db.begin_read().map_err(Error::from)?;
let table = read_txn.open_table(KEYSET_COUNTER).map_err(Error::from)?;
Expand Down
8 changes: 4 additions & 4 deletions crates/cdk-rexie/src/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -592,7 +592,7 @@ impl WalletDatabase for RexieWalletDatabase {
Ok(())
}

async fn increment_keyset_counter(&self, keyset_id: &Id, count: u64) -> Result<(), Self::Err> {
async fn increment_keyset_counter(&self, keyset_id: &Id, count: u32) -> Result<(), Self::Err> {
let rexie = self.db.lock().await;

let transaction = rexie
Expand All @@ -604,7 +604,7 @@ impl WalletDatabase for RexieWalletDatabase {
let keyset_id = serde_wasm_bindgen::to_value(keyset_id).map_err(Error::from)?;

let current_count = counter_store.get(&keyset_id).await.map_err(Error::from)?;
let current_count: Option<u64> =
let current_count: Option<u32> =
serde_wasm_bindgen::from_value(current_count).map_err(Error::from)?;

let new_count = current_count.unwrap_or_default() + count;
Expand All @@ -621,7 +621,7 @@ impl WalletDatabase for RexieWalletDatabase {
Ok(())
}

async fn get_keyset_counter(&self, keyset_id: &Id) -> Result<Option<u64>, Self::Err> {
async fn get_keyset_counter(&self, keyset_id: &Id) -> Result<Option<u32>, Self::Err> {
let rexie = self.db.lock().await;

let transaction = rexie
Expand All @@ -633,7 +633,7 @@ impl WalletDatabase for RexieWalletDatabase {
let keyset_id = serde_wasm_bindgen::to_value(keyset_id).map_err(Error::from)?;

let current_count = counter_store.get(&keyset_id).await.map_err(Error::from)?;
let current_count: Option<u64> =
let current_count: Option<u32> =
serde_wasm_bindgen::from_value(current_count).map_err(Error::from)?;

Ok(current_count)
Expand Down
7 changes: 2 additions & 5 deletions crates/cdk/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,9 @@ license.workspace = true


[features]
default = ["mint", "wallet", "all-nuts"]
default = ["mint", "wallet"]
mint = ["dep:bip39"]
wallet = ["nut13", "dep:bip39", "dep:reqwest"]
all-nuts = ["nut13"]
nut13 = ["dep:bip39"]

wallet = ["dep:bip39", "dep:reqwest"]

[dependencies]
async-trait = "0.1"
Expand Down
4 changes: 2 additions & 2 deletions crates/cdk/src/cdk_database/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,8 @@ pub trait WalletDatabase {
proofs: &Proofs,
) -> Result<(), Self::Err>;

async fn increment_keyset_counter(&self, keyset_id: &Id, count: u64) -> Result<(), Self::Err>;
async fn get_keyset_counter(&self, keyset_id: &Id) -> Result<Option<u64>, Self::Err>;
async fn increment_keyset_counter(&self, keyset_id: &Id, count: u32) -> Result<(), Self::Err>;
async fn get_keyset_counter(&self, keyset_id: &Id) -> Result<Option<u32>, Self::Err>;
}

#[cfg(feature = "mint")]
Expand Down
8 changes: 4 additions & 4 deletions crates/cdk/src/cdk_database/wallet_memory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,15 @@ pub struct WalletMemoryDatabase {
mint_keys: Arc<Mutex<HashMap<Id, Keys>>>,
proofs: Arc<Mutex<HashMap<UncheckedUrl, HashSet<Proof>>>>,
pending_proofs: Arc<Mutex<HashMap<UncheckedUrl, HashSet<Proof>>>>,
keyset_counter: Arc<Mutex<HashMap<Id, u64>>>,
keyset_counter: Arc<Mutex<HashMap<Id, u32>>>,
}

impl WalletMemoryDatabase {
pub fn new(
mint_quotes: Vec<MintQuote>,
melt_quotes: Vec<MeltQuote>,
mint_keys: Vec<Keys>,
keyset_counter: HashMap<Id, u64>,
keyset_counter: HashMap<Id, u32>,
) -> Self {
Self {
mints: Arc::new(Mutex::new(HashMap::new())),
Expand Down Expand Up @@ -215,7 +215,7 @@ impl WalletDatabase for WalletMemoryDatabase {
Ok(())
}

async fn increment_keyset_counter(&self, keyset_id: &Id, count: u64) -> Result<(), Error> {
async fn increment_keyset_counter(&self, keyset_id: &Id, count: u32) -> Result<(), Error> {
let keyset_counter = self.keyset_counter.lock().await;
let current_counter = keyset_counter.get(keyset_id).unwrap_or(&0);
self.keyset_counter
Expand All @@ -225,7 +225,7 @@ impl WalletDatabase for WalletMemoryDatabase {
Ok(())
}

async fn get_keyset_counter(&self, id: &Id) -> Result<Option<u64>, Error> {
async fn get_keyset_counter(&self, id: &Id) -> Result<Option<u32>, Error> {
Ok(self.keyset_counter.lock().await.get(id).cloned())
}
}
1 change: 0 additions & 1 deletion crates/cdk/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ pub enum Error {
#[error(transparent)]
Secret(#[from] super::secret::Error),
/// Bip32 error
#[cfg(feature = "nut13")]
#[error(transparent)]
Bip32(#[from] bitcoin::bip32::Error),
/// Parse int error
Expand Down
1 change: 0 additions & 1 deletion crates/cdk/src/nuts/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ pub mod nut09;
pub mod nut10;
pub mod nut11;
pub mod nut12;
#[cfg(feature = "nut13")]
pub mod nut13;
pub mod nut14;

Expand Down
91 changes: 44 additions & 47 deletions crates/cdk/src/nuts/nut13.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,7 @@
//!
//! <https://github.com/cashubtc/nuts/blob/main/13.md>

use core::str::FromStr;

use bip39::Mnemonic;
use bitcoin::bip32::{DerivationPath, ExtendedPrivKey};
use bitcoin::Network;
use bitcoin::bip32::{ChildNumber, DerivationPath, ExtendedPrivKey};

use super::nut00::{BlindedMessage, PreMint, PreMintSecrets};
use super::nut01::SecretKey;
Expand All @@ -18,21 +14,12 @@ use crate::util::hex;
use crate::{Amount, SECP256K1};

impl Secret {
pub fn from_seed(mnemonic: &Mnemonic, keyset_id: Id, counter: u64) -> Result<Self, Error> {
tracing::debug!(
"Deriving secret for {} with count {}",
keyset_id.to_string(),
counter.to_string()
);
let path: DerivationPath = DerivationPath::from_str(&format!(
"m/129372'/0'/{}'/{}'/0",
u64::try_from(keyset_id)?,
counter
))?;

let seed: [u8; 64] = mnemonic.to_seed("");
let bip32_root_key = ExtendedPrivKey::new_master(Network::Bitcoin, &seed)?;
let derived_xpriv = bip32_root_key.derive_priv(&SECP256K1, &path)?;
pub fn from_xpriv(xpriv: ExtendedPrivKey, keyset_id: Id, counter: u32) -> Result<Self, Error> {
tracing::debug!("Deriving secret for {} with count {}", keyset_id, counter);
let path = derive_path_from_keyset_id(keyset_id)?
.child(ChildNumber::from_hardened_idx(counter)?)
.child(ChildNumber::from_normal_idx(0)?);
let derived_xpriv = xpriv.derive_priv(&SECP256K1, &path)?;

Ok(Self::new(hex::encode(
derived_xpriv.private_key.secret_bytes(),
Expand All @@ -41,21 +28,12 @@ impl Secret {
}

impl SecretKey {
pub fn from_seed(mnemonic: &Mnemonic, keyset_id: Id, counter: u64) -> Result<Self, Error> {
tracing::debug!(
"Deriving key for {} with count {}",
keyset_id.to_string(),
counter.to_string()
);
let path = DerivationPath::from_str(&format!(
"m/129372'/0'/{}'/{}'/1",
u64::try_from(keyset_id)?,
counter
))?;

let seed: [u8; 64] = mnemonic.to_seed("");
let bip32_root_key = ExtendedPrivKey::new_master(Network::Bitcoin, &seed)?;
let derived_xpriv = bip32_root_key.derive_priv(&SECP256K1, &path)?;
pub fn from_xpriv(xpriv: ExtendedPrivKey, keyset_id: Id, counter: u32) -> Result<Self, Error> {
tracing::debug!("Deriving key for {} with count {}", keyset_id, counter);
let path = derive_path_from_keyset_id(keyset_id)?
.child(ChildNumber::from_hardened_idx(counter)?)
.child(ChildNumber::from_normal_idx(1)?);
let derived_xpriv = xpriv.derive_priv(&SECP256K1, &path)?;

Ok(Self::from(derived_xpriv.private_key))
}
Expand All @@ -64,10 +42,10 @@ impl SecretKey {
impl PreMintSecrets {
/// Generate blinded messages from predetermined secrets and blindings
/// factor
pub fn from_seed(
pub fn from_xpriv(
keyset_id: Id,
counter: u64,
mnemonic: &Mnemonic,
counter: u32,
xpriv: ExtendedPrivKey,
amount: Amount,
zero_amount: bool,
) -> Result<Self, Error> {
Expand All @@ -76,8 +54,8 @@ impl PreMintSecrets {
let mut counter = counter;

for amount in amount.split() {
let secret = Secret::from_seed(mnemonic, keyset_id, counter)?;
let blinding_factor = SecretKey::from_seed(mnemonic, keyset_id, counter)?;
let secret = Secret::from_xpriv(xpriv, keyset_id, counter)?;
let blinding_factor = SecretKey::from_xpriv(xpriv, keyset_id, counter)?;

let (blinded, r) = blind_message(&secret.to_bytes(), Some(blinding_factor))?;

Expand All @@ -103,15 +81,15 @@ impl PreMintSecrets {
/// factor
pub fn restore_batch(
keyset_id: Id,
mnemonic: &Mnemonic,
start_count: u64,
end_count: u64,
xpriv: ExtendedPrivKey,
start_count: u32,
end_count: u32,
) -> Result<Self, Error> {
let mut pre_mint_secrets = PreMintSecrets::default();

for i in start_count..=end_count {
let secret = Secret::from_seed(mnemonic, keyset_id, i)?;
let blinding_factor = SecretKey::from_seed(mnemonic, keyset_id, i)?;
let secret = Secret::from_xpriv(xpriv, keyset_id, i)?;
let blinding_factor = SecretKey::from_xpriv(xpriv, keyset_id, i)?;

let (blinded, r) = blind_message(&secret.to_bytes(), Some(blinding_factor))?;

Expand All @@ -131,15 +109,32 @@ impl PreMintSecrets {
}
}

fn derive_path_from_keyset_id(id: Id) -> Result<DerivationPath, Error> {
let index = (u64::try_from(id)? % (2u64.pow(31) - 1)) as u32;
let keyset_child_number = ChildNumber::from_hardened_idx(index)?;
Ok(DerivationPath::from(vec![
ChildNumber::from_hardened_idx(129372)?,
ChildNumber::from_hardened_idx(0)?,
keyset_child_number,
]))
}

#[cfg(test)]
mod tests {
use std::str::FromStr;

use bip39::Mnemonic;
use bitcoin::Network;

use super::*;

#[test]
fn test_secret_from_seed() {
let seed =
"half depart obvious quality work element tank gorilla view sugar picture humble";
let mnemonic = Mnemonic::from_str(seed).unwrap();
let seed: [u8; 64] = mnemonic.to_seed("");
let xpriv = ExtendedPrivKey::new_master(Network::Bitcoin, &seed).unwrap();
let keyset_id = Id::from_str("009a1f293253e41e").unwrap();

let test_secrets = [
Expand All @@ -151,7 +146,7 @@ mod tests {
];

for (i, test_secret) in test_secrets.iter().enumerate() {
let secret = Secret::from_seed(&mnemonic, keyset_id, i.try_into().unwrap()).unwrap();
let secret = Secret::from_xpriv(xpriv, keyset_id, i.try_into().unwrap()).unwrap();
assert_eq!(secret, Secret::from_str(test_secret).unwrap())
}
}
Expand All @@ -160,6 +155,8 @@ mod tests {
let seed =
"half depart obvious quality work element tank gorilla view sugar picture humble";
let mnemonic = Mnemonic::from_str(seed).unwrap();
let seed: [u8; 64] = mnemonic.to_seed("");
let xpriv = ExtendedPrivKey::new_master(Network::Bitcoin, &seed).unwrap();
let keyset_id = Id::from_str("009a1f293253e41e").unwrap();

let test_rs = [
Expand All @@ -171,7 +168,7 @@ mod tests {
];

for (i, test_r) in test_rs.iter().enumerate() {
let r = SecretKey::from_seed(&mnemonic, keyset_id, i.try_into().unwrap()).unwrap();
let r = SecretKey::from_xpriv(xpriv, keyset_id, i.try_into().unwrap()).unwrap();
assert_eq!(r, SecretKey::from_hex(test_r).unwrap())
}
}
Expand Down
Loading
Loading