From 6d840d5d27038cabc7304b05efb40cce96ab5735 Mon Sep 17 00:00:00 2001 From: lescuer97 Date: Sat, 27 Dec 2025 10:52:56 +0100 Subject: [PATCH 1/9] p2pk deterministic generation --- crates/cashu/src/nuts/nut10/error.rs | 3 + crates/cdk-cli/src/main.rs | 20 +++ .../src/sub_commands/generate_public_key.rs | 44 +++++ .../src/sub_commands/get_public_keys.rs | 70 ++++++++ crates/cdk-cli/src/sub_commands/mod.rs | 2 + crates/cdk-common/src/database/wallet/mod.rs | 21 +++ .../src/database/wallet/test/mod.rs | 118 +++++++++++++ crates/cdk-common/src/wallet/mod.rs | 14 ++ crates/cdk-ffi/src/database.rs | 160 ++++++++++++++++++ crates/cdk-ffi/src/postgres.rs | 4 +- crates/cdk-ffi/src/sqlite.rs | 4 +- crates/cdk-ffi/src/types/wallet.rs | 45 ++++- crates/cdk-redb/src/wallet/migrations.rs | 21 ++- crates/cdk-redb/src/wallet/mod.rs | 110 +++++++++++- .../20251222000000_add_p2pk_table.sql | 6 + .../sqlite/20251222000000_add_p2pk_table.sql | 6 + crates/cdk-sql-common/src/wallet/mod.rs | 124 +++++++++++++- crates/cdk/src/wallet/mod.rs | 46 +++++ crates/cdk/src/wallet/p2pk.rs | 140 +++++++++++++++ crates/cdk/src/wallet/receive/saga/mod.rs | 37 ++-- crates/cdk/src/wallet/receive/saga/state.rs | 7 +- 21 files changed, 974 insertions(+), 28 deletions(-) create mode 100644 crates/cdk-cli/src/sub_commands/generate_public_key.rs create mode 100644 crates/cdk-cli/src/sub_commands/get_public_keys.rs create mode 100644 crates/cdk-sql-common/src/wallet/migrations/postgres/20251222000000_add_p2pk_table.sql create mode 100644 crates/cdk-sql-common/src/wallet/migrations/sqlite/20251222000000_add_p2pk_table.sql create mode 100644 crates/cdk/src/wallet/p2pk.rs diff --git a/crates/cashu/src/nuts/nut10/error.rs b/crates/cashu/src/nuts/nut10/error.rs index 24d3f41cf..9c8adcd93 100644 --- a/crates/cashu/src/nuts/nut10/error.rs +++ b/crates/cashu/src/nuts/nut10/error.rs @@ -20,6 +20,9 @@ pub enum Error { /// Spend conditions not met #[error("Spend conditions are not met")] SpendConditionsNotMet, + /// Proof does not contain enough signature for lock + #[error("proof does not contain enough proofs to be spendable")] + NotEnoughSignatures, /// From hex error #[error(transparent)] diff --git a/crates/cdk-cli/src/main.rs b/crates/cdk-cli/src/main.rs index a4146199d..913650ad0 100644 --- a/crates/cdk-cli/src/main.rs +++ b/crates/cdk-cli/src/main.rs @@ -129,6 +129,10 @@ enum Commands { #[command(subcommand)] command: sub_commands::npubcash::NpubCashSubCommand, }, + /// Generate a public key + GeneratePublicKey(sub_commands::generate_public_key::GeneratePublicKeySubCommand), + /// Get public keys + GetPublicKeys(sub_commands::get_public_keys::GetPublicKeysSubCommand), } #[tokio::main] @@ -358,5 +362,21 @@ async fn main() -> Result<()> { ) .await } + Commands::GeneratePublicKey(sub_command_args) => { + sub_commands::generate_public_key::generate_public_key( + &wallet_repository, + sub_command_args, + ¤cy_unit, + ) + .await + } + Commands::GetPublicKeys(sub_command_args) => { + sub_commands::get_public_keys::get_public_keys( + &wallet_repository, + sub_command_args, + ¤cy_unit, + ) + .await + } } } diff --git a/crates/cdk-cli/src/sub_commands/generate_public_key.rs b/crates/cdk-cli/src/sub_commands/generate_public_key.rs new file mode 100644 index 000000000..0bcc1867e --- /dev/null +++ b/crates/cdk-cli/src/sub_commands/generate_public_key.rs @@ -0,0 +1,44 @@ +use std::str::FromStr; + +use anyhow::Result; +use cdk::mint_url::MintUrl; +use cdk::nuts::CurrencyUnit; +use cdk::wallet::WalletRepository; +use clap::Args; + +use crate::utils::get_or_create_wallet; + +#[derive(Args)] +pub struct GeneratePublicKeySubCommand { + /// Mint URL to select wallet context + #[arg(long)] + mint_url: Option, +} + +pub async fn generate_public_key( + wallet_repository: &WalletRepository, + sub_command_args: &GeneratePublicKeySubCommand, + unit: &CurrencyUnit, +) -> Result<()> { + let mint_url = match &sub_command_args.mint_url { + Some(url) => MintUrl::from_str(url)?, + None => { + let wallets = wallet_repository.get_wallets().await; + wallets + .iter() + .find(|wallet| &wallet.unit == unit) + .map(|wallet| wallet.mint_url.clone()) + .ok_or_else(|| { + anyhow::anyhow!("No wallet found for unit {}. Use --mint-url.", unit) + })? + } + }; + + let wallet = get_or_create_wallet(wallet_repository, &mint_url, unit).await?; + let public_key = wallet.generate_public_key().await?; + + println!("\npublic key generated!\n"); + println!("public key: {}", public_key.to_hex()); + + Ok(()) +} diff --git a/crates/cdk-cli/src/sub_commands/get_public_keys.rs b/crates/cdk-cli/src/sub_commands/get_public_keys.rs new file mode 100644 index 000000000..56a544d98 --- /dev/null +++ b/crates/cdk-cli/src/sub_commands/get_public_keys.rs @@ -0,0 +1,70 @@ +use std::str::FromStr; + +use anyhow::Result; +use cdk::mint_url::MintUrl; +use cdk::nuts::CurrencyUnit; +use cdk::wallet::WalletRepository; +use clap::Args; + +use crate::utils::get_or_create_wallet; + +#[derive(Args)] +pub struct GetPublicKeysSubCommand { + /// Show the latest public key + #[arg(long)] + pub latest: bool, + /// Mint URL to select wallet context + #[arg(long)] + mint_url: Option, +} + +pub async fn get_public_keys( + wallet_repository: &WalletRepository, + sub_command_args: &GetPublicKeysSubCommand, + unit: &CurrencyUnit, +) -> Result<()> { + let mint_url = match &sub_command_args.mint_url { + Some(url) => MintUrl::from_str(url)?, + None => { + let wallets = wallet_repository.get_wallets().await; + wallets + .iter() + .find(|wallet| &wallet.unit == unit) + .map(|wallet| wallet.mint_url.clone()) + .ok_or_else(|| { + anyhow::anyhow!("No wallet found for unit {}. Use --mint-url.", unit) + })? + } + }; + + let wallet = get_or_create_wallet(wallet_repository, &mint_url, unit).await?; + + if sub_command_args.latest { + let latest_public_key = wallet.get_latest_public_key().await?; + + match latest_public_key { + Some(key) => { + println!("\npublic key found!\n"); + + println!("public key: {}", key.pubkey.to_hex()); + println!("derivation path: {}", key.derivation_path); + } + None => { + println!("\npublic key not found!\n"); + } + } + + return Ok(()); + } + + let list_public_keys = wallet.get_public_keys().await?; + if list_public_keys.is_empty() { + println!("\npublic not found!\n"); + } + println!("\npublic keys found:\n"); + for public_key in list_public_keys { + println!("public key: {}", public_key.pubkey.to_hex()); + println!("derivation path: {}", public_key.derivation_path); + } + Ok(()) +} diff --git a/crates/cdk-cli/src/sub_commands/mod.rs b/crates/cdk-cli/src/sub_commands/mod.rs index 4e614d83f..f0d312f23 100644 --- a/crates/cdk-cli/src/sub_commands/mod.rs +++ b/crates/cdk-cli/src/sub_commands/mod.rs @@ -7,6 +7,8 @@ pub mod check_requests; pub mod create_request; pub mod decode_request; pub mod decode_token; +pub mod generate_public_key; +pub mod get_public_keys; pub mod list_mint_proofs; pub mod melt; pub mod mint; diff --git a/crates/cdk-common/src/database/wallet/mod.rs b/crates/cdk-common/src/database/wallet/mod.rs index 0c21faf38..cb073201c 100644 --- a/crates/cdk-common/src/database/wallet/mod.rs +++ b/crates/cdk-common/src/database/wallet/mod.rs @@ -4,6 +4,7 @@ use std::collections::HashMap; use std::fmt::Debug; use async_trait::async_trait; +use bitcoin::bip32::DerivationPath; use cashu::KeySet; use super::Error; @@ -233,4 +234,24 @@ where secondary_namespace: &str, key: &str, ) -> Result<(), Err>; + + // P2PK signing key methods + + /// Store a P2PK signing key for the wallet + async fn add_p2pk_key( + &self, + pubkey: &PublicKey, + derivation_path: DerivationPath, + derivation_index: u32, + ) -> Result<(), Err>; + + /// Get a stored P2PK signing key by pubkey. + async fn get_p2pk_key(&self, pubkey: &PublicKey) + -> Result, Err>; + + /// List all stored P2PK signing keys. + async fn list_p2pk_keys(&self) -> Result, Err>; + + /// Tries to get the latest p2pk key generated + async fn latest_p2pk(&self) -> Result, Err>; } diff --git a/crates/cdk-common/src/database/wallet/test/mod.rs b/crates/cdk-common/src/database/wallet/test/mod.rs index 25e7571ba..d39f14ff3 100644 --- a/crates/cdk-common/src/database/wallet/test/mod.rs +++ b/crates/cdk-common/src/database/wallet/test/mod.rs @@ -9,6 +9,7 @@ use std::collections::{BTreeMap, HashMap}; use std::str::FromStr; use std::sync::atomic::{AtomicU64, Ordering}; +use bitcoin::bip32::DerivationPath; use cashu::nut00::KnownMethod; use cashu::secret::Secret; use cashu::{Amount, CurrencyUnit, MeltQuoteState, MintQuoteState, SecretKey}; @@ -1124,6 +1125,123 @@ where let value3 = db.kv_read("ns1", "sub2", "key").await.unwrap(); assert_eq!(value3, Some(b"value_sub2".to_vec())); } +/// Test adding and retrieving a P2PK signing key +pub async fn add_and_get_p2pk_key(db: DB) +where + DB: Database, +{ + let pubkey = SecretKey::generate().public_key(); + let derivation_path = DerivationPath::from_str("m/0'/0'/0'").unwrap(); + let derivation_index = 0u32; + + // Add P2PK key + db.add_p2pk_key(&pubkey, derivation_path.clone(), derivation_index) + .await + .unwrap(); + + // Retrieve the key + let retrieved = db.get_p2pk_key(&pubkey).await.unwrap(); + assert!(retrieved.is_some()); + let retrieved_key = retrieved.unwrap(); + assert_eq!(retrieved_key.pubkey, pubkey); + assert_eq!(retrieved_key.derivation_path, derivation_path); + assert_eq!(retrieved_key.derivation_index, derivation_index); + + // Test getting a non-existent key + let non_existent_pubkey = SecretKey::generate().public_key(); + let result = db.get_p2pk_key(&non_existent_pubkey).await.unwrap(); + assert!(result.is_none()); +} + +/// Test that list_p2pk_keys returns empty vector on fresh database +pub async fn list_p2pk_keys_empty(db: DB) +where + DB: Database, +{ + let keys = db.list_p2pk_keys().await.unwrap(); + assert!(keys.is_empty()); +} + +/// Test listing multiple P2PK signing keys +pub async fn list_p2pk_keys_multiple(db: DB) +where + DB: Database, +{ + // Add multiple keys with different derivation indices + let pubkey1 = SecretKey::generate().public_key(); + let pubkey2 = SecretKey::generate().public_key(); + let pubkey3 = SecretKey::generate().public_key(); + + db.add_p2pk_key(&pubkey1, DerivationPath::from_str("m/0'/0'/0'").unwrap(), 0) + .await + .unwrap(); + + db.add_p2pk_key(&pubkey2, DerivationPath::from_str("m/0'/0'/1'").unwrap(), 1) + .await + .unwrap(); + + db.add_p2pk_key(&pubkey3, DerivationPath::from_str("m/0'/0'/2'").unwrap(), 2) + .await + .unwrap(); + + // List all keys + let keys = db.list_p2pk_keys().await.unwrap(); + assert_eq!(keys.len(), 3); + + // Verify all keys are present + let pubkeys: Vec<_> = keys.iter().map(|k| k.pubkey).collect(); + assert!(pubkeys.contains(&pubkey1)); + assert!(pubkeys.contains(&pubkey2)); + assert!(pubkeys.contains(&pubkey3)); + + // Verify derivation indices are correct + let derivation_indices: Vec<_> = keys.iter().map(|k| k.derivation_index).collect(); + assert!(derivation_indices.contains(&0)); + assert!(derivation_indices.contains(&1)); + assert!(derivation_indices.contains(&2)); +} + +/// Test that latest_p2pk returns None on fresh database +pub async fn latest_p2pk_empty(db: DB) +where + DB: Database, +{ + let latest = db.latest_p2pk().await.unwrap(); + assert!(latest.is_none()); +} + +/// Test getting the latest P2PK signing key +pub async fn latest_p2pk_with_keys(db: DB) +where + DB: Database, +{ + // Add multiple keys with delays to ensure different timestamps + let pubkey1 = SecretKey::generate().public_key(); + let pubkey2 = SecretKey::generate().public_key(); + let pubkey3 = SecretKey::generate().public_key(); + + db.add_p2pk_key(&pubkey1, DerivationPath::from_str("m/0'/0'/0'").unwrap(), 0) + .await + .unwrap(); + + db.add_p2pk_key(&pubkey2, DerivationPath::from_str("m/0'/0'/1'").unwrap(), 1) + .await + .unwrap(); + + // Wait 1 second to ensure the last key has a different (newer) timestamp + tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; + + db.add_p2pk_key(&pubkey3, DerivationPath::from_str("m/0'/0'/2'").unwrap(), 2) + .await + .unwrap(); + + // Get latest key - should be the most recently created (pubkey3) + let latest = db.latest_p2pk().await.unwrap(); + assert!(latest.is_some()); + let latest_key = latest.unwrap(); + assert_eq!(latest_key.pubkey, pubkey3); + assert_eq!(latest_key.derivation_index, 2); +} // ============================================================================= // Wallet Saga Tests diff --git a/crates/cdk-common/src/wallet/mod.rs b/crates/cdk-common/src/wallet/mod.rs index 0c02dff3a..6dd5ae548 100644 --- a/crates/cdk-common/src/wallet/mod.rs +++ b/crates/cdk-common/src/wallet/mod.rs @@ -4,6 +4,7 @@ use std::collections::HashMap; use std::fmt; use std::str::FromStr; +use bitcoin::bip32::DerivationPath; use async_trait::async_trait; use bitcoin::hashes::{sha256, Hash, HashEngine}; use cashu::amount::SplitTarget; @@ -952,6 +953,19 @@ pub trait Wallet: Send + Sync { async fn get_proofs_by_states(&self, states: Vec) -> Result; } +/// Public key generated for proof signing +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct P2PKSigningKey { + /// Public key + pub pubkey: PublicKey, + /// Derivation path + pub derivation_path: DerivationPath, + /// Derivation index + pub derivation_index: u32, + /// Created time + pub created_time: u64, +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/cdk-ffi/src/database.rs b/crates/cdk-ffi/src/database.rs index ac2e92481..614d8a3b6 100644 --- a/crates/cdk-ffi/src/database.rs +++ b/crates/cdk-ffi/src/database.rs @@ -3,6 +3,7 @@ use std::collections::HashMap; use std::sync::Arc; +use cdk_common::bitcoin::bip32::DerivationPath; use cdk_common::database::WalletDatabase as CdkWalletDatabase; use cdk_common::wallet::WalletSaga; use cdk_sql_common::pool::DatabasePool; @@ -104,6 +105,23 @@ pub trait WalletDatabase: Send + Sync { secondary_namespace: String, ) -> Result, FfiError>; + /// Add P2PK signing key to storage + async fn add_p2pk_key( + &self, + pubkey: PublicKey, + derivation_path: String, + derivation_index: u32, + ) -> Result<(), FfiError>; + + /// Get P2PK signing key from storage + async fn get_p2pk_key(&self, pubkey: PublicKey) -> Result, FfiError>; + + /// List all P2PK signing keys from storage + async fn list_p2pk_keys(&self) -> Result, FfiError>; + + /// Get the latest P2PK signing key (most recently created) + async fn latest_p2pk(&self) -> Result, FfiError>; + /// Write a value to the KV store async fn kv_write( &self, @@ -638,6 +656,76 @@ impl CdkWalletDatabase for WalletDatabaseBridge { .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into())) } + // P2PK methods + + async fn add_p2pk_key( + &self, + pubkey: &cdk::nuts::PublicKey, + derivation_path: DerivationPath, + derivation_index: u32, + ) -> Result<(), cdk::cdk_database::Error> { + let ffi_pubkey: PublicKey = (*pubkey).into(); + let ffi_derivation_path = derivation_path.to_string(); + self.ffi_db + .add_p2pk_key(ffi_pubkey, ffi_derivation_path, derivation_index) + .await + .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into())) + } + + async fn list_p2pk_keys( + &self, + ) -> Result, cdk::cdk_database::Error> { + let result = self + .ffi_db + .list_p2pk_keys() + .await + .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))?; + Ok(result + .into_iter() + .map(|k| { + k.try_into().map_err(|e: crate::error::FfiError| { + cdk::cdk_database::Error::Database(e.to_string().into()) + }) + }) + .collect::, _>>()?) + } + + async fn latest_p2pk( + &self, + ) -> Result, cdk::cdk_database::Error> { + let result = self + .ffi_db + .latest_p2pk() + .await + .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))?; + Ok(result + .map(|k| { + k.try_into().map_err(|e: crate::error::FfiError| { + cdk::cdk_database::Error::Database(e.to_string().into()) + }) + }) + .transpose()?) + } + + async fn get_p2pk_key( + &self, + pubkey: &cdk::nuts::PublicKey, + ) -> Result, cdk::cdk_database::Error> { + let ffi_pubkey: PublicKey = (*pubkey).into(); + let result = self + .ffi_db + .get_p2pk_key(ffi_pubkey) + .await + .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))?; + Ok(result + .map(|k| { + k.try_into().map_err(|e: crate::error::FfiError| { + cdk::cdk_database::Error::Database(e.to_string().into()) + }) + }) + .transpose()?) + } + // Write methods (non-transactional) async fn update_proofs( @@ -1244,6 +1332,50 @@ where .map_err(FfiError::internal) } + async fn add_p2pk_key( + &self, + pubkey: PublicKey, + derivation_path: String, + derivation_index: u32, + ) -> Result<(), FfiError> { + use std::str::FromStr; + + use cdk_common::bitcoin::bip32::DerivationPath; + + let cdk_pubkey: cdk::nuts::PublicKey = pubkey.try_into()?; + let cdk_derivation_path = + DerivationPath::from_str(&derivation_path).map_err(FfiError::internal)?; + + self.inner + .add_p2pk_key(&cdk_pubkey, cdk_derivation_path, derivation_index) + .await + .map_err(FfiError::database) + } + + async fn get_p2pk_key(&self, pubkey: PublicKey) -> Result, FfiError> { + let cdk_pubkey: cdk::nuts::PublicKey = pubkey.try_into()?; + let result = self + .inner + .get_p2pk_key(&cdk_pubkey) + .await + .map_err(FfiError::database)?; + Ok(result.map(Into::into)) + } + + async fn list_p2pk_keys(&self) -> Result, FfiError> { + let result = self + .inner + .list_p2pk_keys() + .await + .map_err(FfiError::database)?; + Ok(result.into_iter().map(Into::into).collect()) + } + + async fn latest_p2pk(&self) -> Result, FfiError> { + let result = self.inner.latest_p2pk().await.map_err(FfiError::database)?; + Ok(result.map(Into::into)) + } + async fn kv_write( &self, primary_namespace: String, @@ -1824,6 +1956,34 @@ macro_rules! impl_ffi_wallet_database { self.inner.remove_keys(id).await } + // P2PK methods + + async fn add_p2pk_key( + &self, + pubkey: PublicKey, + derivation_path: String, + derivation_index: u32, + ) -> Result<(), FfiError> { + self.inner + .add_p2pk_key(pubkey, derivation_path, derivation_index) + .await + } + + async fn get_p2pk_key( + &self, + pubkey: PublicKey, + ) -> Result, FfiError> { + self.inner.get_p2pk_key(pubkey).await + } + + async fn list_p2pk_keys(&self) -> Result, FfiError> { + self.inner.list_p2pk_keys().await + } + + async fn latest_p2pk(&self) -> Result, FfiError> { + self.inner.latest_p2pk().await + } + // ========== Saga management methods ========== async fn add_saga(&self, saga_json: String) -> Result<(), FfiError> { diff --git a/crates/cdk-ffi/src/postgres.rs b/crates/cdk-ffi/src/postgres.rs index 99fd1a038..ddb1b7a29 100644 --- a/crates/cdk-ffi/src/postgres.rs +++ b/crates/cdk-ffi/src/postgres.rs @@ -4,8 +4,8 @@ use cdk_postgres::PgConnectionPool; use crate::{ CurrencyUnit, FfiError, FfiWalletSQLDatabase, Id, KeySet, KeySetInfo, Keys, MeltQuote, - MintInfo, MintQuote, MintUrl, ProofInfo, ProofState, PublicKey, SpendingConditions, - Transaction, TransactionDirection, TransactionId, WalletDatabase, + MintInfo, MintQuote, MintUrl, P2PKSigningKey, ProofInfo, ProofState, PublicKey, + SpendingConditions, Transaction, TransactionDirection, TransactionId, WalletDatabase, }; #[derive(uniffi::Object)] diff --git a/crates/cdk-ffi/src/sqlite.rs b/crates/cdk-ffi/src/sqlite.rs index 708b9f4cb..d78c1417f 100644 --- a/crates/cdk-ffi/src/sqlite.rs +++ b/crates/cdk-ffi/src/sqlite.rs @@ -5,8 +5,8 @@ use cdk_sqlite::SqliteConnectionManager; use crate::{ CurrencyUnit, FfiError, FfiWalletSQLDatabase, Id, KeySet, KeySetInfo, Keys, MeltQuote, - MintInfo, MintQuote, MintUrl, ProofInfo, ProofState, PublicKey, SpendingConditions, - Transaction, TransactionDirection, TransactionId, WalletDatabase, + MintInfo, MintQuote, MintUrl, P2PKSigningKey, ProofInfo, ProofState, PublicKey, + SpendingConditions, Transaction, TransactionDirection, TransactionId, WalletDatabase, }; /// FFI-compatible WalletSqliteDatabase implementation that implements the WalletDatabaseFfi trait diff --git a/crates/cdk-ffi/src/types/wallet.rs b/crates/cdk-ffi/src/types/wallet.rs index 0c0548cbe..03ab1fa02 100644 --- a/crates/cdk-ffi/src/types/wallet.rs +++ b/crates/cdk-ffi/src/types/wallet.rs @@ -8,7 +8,7 @@ use super::amount::{Amount, SplitTarget}; use super::proof::{Proofs, SpendingConditions}; use crate::error::FfiError; use crate::token::Token; -use crate::{CurrencyUnit, MintUrl}; +use crate::{CurrencyUnit, MintUrl, PublicKey}; /// FFI-compatible SendMemo #[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)] @@ -84,6 +84,46 @@ impl From for cdk::wallet::SendKind { } } +/// FFI-compatible P2PKSigningKey +#[derive(Debug, Clone, uniffi::Record)] +pub struct P2PKSigningKey { + /// Public key + pub pubkey: PublicKey, + /// Derivation path as string + pub derivation_path: String, + /// Derivation index + pub derivation_index: u32, + /// Created time + pub created_time: u64, +} + +impl TryFrom for cdk_common::wallet::P2PKSigningKey { + type Error = crate::error::FfiError; + + fn try_from(key: P2PKSigningKey) -> Result { + Ok(Self { + pubkey: key.pubkey.try_into()?, + derivation_path: key + .derivation_path + .parse() + .expect("Invalid derivation path"), + derivation_index: key.derivation_index, + created_time: key.created_time, + }) + } +} + +impl From for P2PKSigningKey { + fn from(key: cdk_common::wallet::P2PKSigningKey) -> Self { + Self { + pubkey: key.pubkey.into(), + derivation_path: key.derivation_path.to_string(), + derivation_index: key.derivation_index, + created_time: key.created_time, + } + } +} + impl From for SendKind { fn from(kind: cdk::wallet::SendKind) -> Self { match kind { @@ -664,7 +704,6 @@ impl From for Restored { } } } - /// FFI-compatible options for confirming a melt operation #[derive(Debug, Clone, Default, Serialize, Deserialize, uniffi::Record)] pub struct MeltConfirmOptions { @@ -717,3 +756,5 @@ impl From for WalletKey { } } } + +pub use cdk_common::wallet::{WalletSaga, WalletSagaState}; diff --git a/crates/cdk-redb/src/wallet/migrations.rs b/crates/cdk-redb/src/wallet/migrations.rs index 13b430063..94cb718d9 100644 --- a/crates/cdk-redb/src/wallet/migrations.rs +++ b/crates/cdk-redb/src/wallet/migrations.rs @@ -12,7 +12,9 @@ use redb::{ }; use super::Error; -use crate::wallet::{KEYSETS_TABLE, KEYSET_COUNTER, KEYSET_U32_MAPPING, MINT_KEYS_TABLE}; +use crate::wallet::{ + KEYSETS_TABLE, KEYSET_COUNTER, KEYSET_U32_MAPPING, MINT_KEYS_TABLE, P2PK_SIGNING_KEYS_TABLE, +}; // const MINTS_TABLE: TableDefinition<&str, &str> = TableDefinition::new("mints_table"); @@ -201,3 +203,20 @@ pub(crate) fn migrate_03_to_04(db: Arc) -> Result { Ok(4) } + +pub(crate) fn migrate_04_to_05(db: Arc) -> Result { + tracing::info!("Starting migration from version 4 to 5: Initializing P2PK_SIGNING_KEYS_TABLE"); + let write_txn = db.begin_write().map_err(Error::from)?; + + { + // Open the table to initialize it (redb creates tables on first open) + let _ = write_txn + .open_table(P2PK_SIGNING_KEYS_TABLE) + .map_err(Error::from)?; + } + + write_txn.commit()?; + tracing::info!("Finished migration from version 4 to 5: P2PK_SIGNING_KEYS_TABLE initialized"); + + Ok(5) +} diff --git a/crates/cdk-redb/src/wallet/mod.rs b/crates/cdk-redb/src/wallet/mod.rs index de510bfa4..d6feeae2a 100644 --- a/crates/cdk-redb/src/wallet/mod.rs +++ b/crates/cdk-redb/src/wallet/mod.rs @@ -7,6 +7,7 @@ use std::str::FromStr; use std::sync::Arc; use async_trait::async_trait; +use cdk_common::bitcoin::bip32::DerivationPath; use cdk_common::database::{validate_kvstore_params, WalletDatabase}; use cdk_common::mint_url::MintUrl; use cdk_common::nut00::KnownMethod; @@ -21,9 +22,11 @@ use cdk_common::{ use redb::{Database, MultimapTableDefinition, ReadableDatabase, ReadableTable, TableDefinition}; use tracing::instrument; -use super::error::Error; +use crate::error::Error; use crate::migrations::migrate_00_to_01; -use crate::wallet::migrations::{migrate_01_to_02, migrate_02_to_03, migrate_03_to_04}; +use crate::wallet::migrations::{ + migrate_01_to_02, migrate_02_to_03, migrate_03_to_04, migrate_04_to_05, +}; mod migrations; @@ -48,11 +51,15 @@ const TRANSACTIONS_TABLE: TableDefinition<&[u8], &str> = TableDefinition::new("t // const SAGAS_TABLE: TableDefinition<&str, &str> = TableDefinition::new("wallet_sagas"); +// +const P2PK_SIGNING_KEYS_TABLE: TableDefinition<&[u8], &str> = + TableDefinition::new("p2pk_signing_keys"); + const KEYSET_U32_MAPPING: TableDefinition = TableDefinition::new("keyset_u32_mapping"); // <(primary_namespace, secondary_namespace, key), value> const KV_STORE_TABLE: TableDefinition<(&str, &str, &str), &[u8]> = TableDefinition::new("kv_store"); -const DATABASE_VERSION: u32 = 4; +const DATABASE_VERSION: u32 = 5; /// Wallet Redb Database #[derive(Debug, Clone)] @@ -116,6 +123,10 @@ impl WalletRedbDatabase { current_file_version = migrate_03_to_04(Arc::clone(&db))?; } + if current_file_version == 4 { + current_file_version = migrate_04_to_05(Arc::clone(&db))?; + } + if current_file_version != DATABASE_VERSION { tracing::warn!( "Database upgrade did not complete at {} current is {}", @@ -164,6 +175,7 @@ impl WalletRedbDatabase { let _ = write_txn.open_table(TRANSACTIONS_TABLE)?; let _ = write_txn.open_table(KEYSET_U32_MAPPING)?; let _ = write_txn.open_table(KV_STORE_TABLE)?; + let _ = write_txn.open_table(P2PK_SIGNING_KEYS_TABLE)?; table.insert("db_version", DATABASE_VERSION.to_string().as_str())?; } @@ -1467,6 +1479,98 @@ impl WalletDatabase for WalletRedbDatabase { Ok(()) } + + #[instrument(skip(self))] + async fn add_p2pk_key( + &self, + pubkey: &PublicKey, + derivation_path: DerivationPath, + derivation_index: u32, + ) -> Result<(), database::Error> { + let write_txn = self.db.begin_write().map_err(Error::from)?; + { + let mut table = write_txn + .open_table(P2PK_SIGNING_KEYS_TABLE) + .map_err(Error::from)?; + table + .insert( + pubkey.to_bytes().as_slice(), + serde_json::to_string(&wallet::P2PKSigningKey { + pubkey: *pubkey, + derivation_path, + derivation_index, + created_time: unix_time(), + }) + .map_err(Error::from)? + .as_str(), + ) + .map_err(Error::from)?; + } + write_txn.commit().map_err(Error::from)?; + Ok(()) + } + + #[instrument(skip(self))] + async fn get_p2pk_key( + &self, + pubkey: &PublicKey, + ) -> Result, database::Error> { + let read_txn = self.db.begin_read().map_err(Error::from)?; + let table = read_txn + .open_table(P2PK_SIGNING_KEYS_TABLE) + .map_err(Error::from)?; + + if let Some(key) = table + .get(pubkey.to_bytes().as_slice()) + .map_err(Error::from)? + { + return Ok(Some( + serde_json::from_str(key.value()).map_err(Error::from)?, + )); + } + + Ok(None) + } + + #[instrument(skip(self))] + async fn list_p2pk_keys(&self) -> Result, database::Error> { + let read_txn = self.db.begin_read().map_err(Error::from)?; + let table = read_txn + .open_table(P2PK_SIGNING_KEYS_TABLE) + .map_err(Error::from)?; + + let keys: Vec = table + .iter() + .map_err(Error::from)? + .flatten() + .filter_map(|(_k, v)| { + if let Ok(key) = serde_json::from_str::(v.value()) { + return Some(key); + } + + None + }) + .collect(); + + Ok(keys) + } + + #[instrument(skip(self))] + async fn latest_p2pk(&self) -> Result, database::Error> { + let read_txn = self.db.begin_read().map_err(Error::from)?; + let table = read_txn + .open_table(P2PK_SIGNING_KEYS_TABLE) + .map_err(Error::from)?; + + let latest_key = table + .iter() + .map_err(Error::from)? + .flatten() + .filter_map(|(_k, v)| serde_json::from_str::(v.value()).ok()) + .max_by_key(|key| key.created_time); + + Ok(latest_key) + } } #[cfg(test)] diff --git a/crates/cdk-sql-common/src/wallet/migrations/postgres/20251222000000_add_p2pk_table.sql b/crates/cdk-sql-common/src/wallet/migrations/postgres/20251222000000_add_p2pk_table.sql new file mode 100644 index 000000000..795dd0a98 --- /dev/null +++ b/crates/cdk-sql-common/src/wallet/migrations/postgres/20251222000000_add_p2pk_table.sql @@ -0,0 +1,6 @@ +CREATE TABLE IF NOT EXISTS p2pk_signing_key ( + pubkey BYTEA PRIMARY KEY, + derivation_index INTEGER NOT NULL, + derivation_path TEXT NOT NULL, + created_time BIGINT NOT NULL +); diff --git a/crates/cdk-sql-common/src/wallet/migrations/sqlite/20251222000000_add_p2pk_table.sql b/crates/cdk-sql-common/src/wallet/migrations/sqlite/20251222000000_add_p2pk_table.sql new file mode 100644 index 000000000..38039174d --- /dev/null +++ b/crates/cdk-sql-common/src/wallet/migrations/sqlite/20251222000000_add_p2pk_table.sql @@ -0,0 +1,6 @@ +CREATE TABLE IF NOT EXISTS p2pk_signing_key ( + pubkey BLOB PRIMARY KEY, + derivation_index INTEGER NOT NULL, + derivation_path TEXT NOT NULL, + created_time INTEGER NOT NULL +); diff --git a/crates/cdk-sql-common/src/wallet/mod.rs b/crates/cdk-sql-common/src/wallet/mod.rs index edcdb5086..7a3847257 100644 --- a/crates/cdk-sql-common/src/wallet/mod.rs +++ b/crates/cdk-sql-common/src/wallet/mod.rs @@ -6,10 +6,12 @@ use std::str::FromStr; use std::sync::Arc; use async_trait::async_trait; +use bitcoin::bip32::DerivationPath; use cdk_common::database::{ConversionError, Error, WalletDatabase}; use cdk_common::mint_url::MintUrl; use cdk_common::nuts::{MeltQuoteState, MintQuoteState}; use cdk_common::secret::Secret; +use cdk_common::util::unix_time; use cdk_common::wallet::{ self, MintQuote, ProofInfo, Transaction, TransactionDirection, TransactionId, }; @@ -685,7 +687,6 @@ where .collect::>()) } - #[instrument(skip(self))] async fn update_proofs( &self, added: Vec, @@ -1649,6 +1650,82 @@ where .await?; Ok(()) } + + // P2PK methods + + #[instrument(skip(self))] + async fn add_p2pk_key( + &self, + pubkey: &PublicKey, + derivation_path: DerivationPath, + derivation_index: u32, + ) -> Result<(), Error> { + let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?; + let query_str = r#" + INSERT INTO p2pk_signing_key (pubkey, derivation_index, derivation_path, created_time) + VALUES (:pubkey, :derivation_index, :derivation_path, :created_time) + "# + .to_string(); + + query(&query_str)? + .bind("pubkey", pubkey.to_bytes().to_vec()) + .bind("derivation_index", derivation_index) + .bind("derivation_path", derivation_path.to_string()) + .bind("created_time", unix_time() as i64) + .execute(&*conn) + .await?; + + Ok(()) + } + + #[instrument(skip(self))] + async fn get_p2pk_key( + &self, + pubkey: &PublicKey, + ) -> Result, Error> { + let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?; + let query_str = r#"SELECT pubkey, derivation_index, derivation_path, created_time FROM p2pk_signing_key WHERE pubkey = :pubkey"#.to_string(); + + query(&query_str)? + .bind("pubkey", pubkey.to_bytes().to_vec()) + .fetch_one(&*conn) + .await? + .map(sql_row_to_p2pk_signing_key) + .transpose() + } + + #[instrument(skip(self))] + async fn list_p2pk_keys(&self) -> Result, Error> { + let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?; + let query_str = r#" + SELECT pubkey, derivation_index, derivation_path, created_time FROM p2pk_signing_key ORDER BY derivation_index DESC + "#.to_string(); + + Ok(query(&query_str)? + .fetch_all(&*conn) + .await? + .into_iter() + .filter_map(|row| { + let row = sql_row_to_p2pk_signing_key(row).ok()?; + + Some(row) + }) + .collect::>()) + } + + #[instrument(skip(self))] + async fn latest_p2pk(&self) -> Result, Error> { + let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?; + let query_str = r#" + SELECT pubkey, derivation_index, derivation_path, created_time FROM p2pk_signing_key ORDER BY derivation_index DESC LIMIT 1 + "#.to_string(); + + query(&query_str)? + .fetch_one(&*conn) + .await? + .map(sql_row_to_p2pk_signing_key) + .transpose() + } } fn sql_row_to_mint_info(row: Vec) -> Result { @@ -1991,3 +2068,48 @@ fn sql_row_to_transaction(row: Vec) -> Result { saga_id, }) } + +fn sql_row_to_p2pk_signing_key(row: Vec) -> Result { + unpack_into!( + let ( + pubkey, + derivation_index, + derivation_path, + created_time + ) = row + ); + + Ok(wallet::P2PKSigningKey { + pubkey: column_as_string!(pubkey, PublicKey::from_str, PublicKey::from_slice), + derivation_index: column_as_number!(derivation_index), + derivation_path: column_as_string!(derivation_path, DerivationPath::from_str), + created_time: column_as_number!(created_time), + }) +} + +// KVStore implementations for wallet + +#[async_trait] +impl database::KVStoreDatabase for SQLWalletDatabase +where + RM: DatabasePool + 'static, +{ + type Err = Error; + + async fn kv_read( + &self, + primary_namespace: &str, + secondary_namespace: &str, + key: &str, + ) -> Result>, Error> { + crate::keyvalue::kv_read(&self.pool, primary_namespace, secondary_namespace, key).await + } + + async fn kv_list( + &self, + primary_namespace: &str, + secondary_namespace: &str, + ) -> Result, Error> { + crate::keyvalue::kv_list(&self.pool, primary_namespace, secondary_namespace).await + } +} diff --git a/crates/cdk/src/wallet/mod.rs b/crates/cdk/src/wallet/mod.rs index fff0756c2..aded01b29 100644 --- a/crates/cdk/src/wallet/mod.rs +++ b/crates/cdk/src/wallet/mod.rs @@ -6,11 +6,14 @@ use std::str::FromStr; use std::sync::Arc; use std::time::Duration; +use bitcoin::bip32::Xpriv; +use bitcoin::Network; use cdk_common::amount::FeeAndAmounts; use cdk_common::database::{self, WalletDatabase}; use cdk_common::parking_lot::RwLock; use cdk_common::subscription::WalletParams; use cdk_common::wallet::ProofInfo; +use cdk_common::{PublicKey, SecretKey, SECP256K1}; use getrandom::getrandom; pub use mint_connector::http_client::{ AuthHttpClient as BaseAuthHttpClient, HttpClient as BaseHttpClient, @@ -49,6 +52,7 @@ mod mint_connector; mod mint_metadata_cache; #[cfg(feature = "npubcash")] mod npubcash; +pub mod p2pk; pub mod payment_request; mod proofs; mod receive; @@ -870,6 +874,48 @@ impl Wallet { pub fn set_target_proof_count(&mut self, count: usize) { self.target_proof_count = count; } + + /// generates and stores public key in database + pub async fn generate_public_key(&self) -> Result { + p2pk::generate_public_key(&self.localstore, &self.seed).await + } + + /// gets public key by it's hex value + pub async fn get_public_key( + &self, + pubkey: &PublicKey, + ) -> Result, database::Error> { + p2pk::get_public_key(&self.localstore, pubkey).await + } + + /// gets list of stored public keys in database + pub async fn get_public_keys( + &self, + ) -> Result, database::Error> { + p2pk::get_public_keys(&self.localstore).await + } + + /// Gets the latest generated P2PK signing key (most recently created) + pub async fn get_latest_public_key( + &self, + ) -> Result, database::Error> { + p2pk::get_latest_public_key(&self.localstore).await + } + + /// try to get secret key from p2pk signing key in localstore + async fn get_signing_key(&self, pubkey: &PublicKey) -> Result, Error> { + let signing = self.localstore.get_p2pk_key(pubkey).await?; + if let Some(signing) = signing { + let xpriv = Xpriv::new_master(Network::Bitcoin, &self.seed)?; + return Ok(Some(SecretKey::from( + xpriv + .derive_priv(&SECP256K1, &signing.derivation_path)? + .private_key, + ))); + } + + Ok(None) + } } impl Drop for Wallet { diff --git a/crates/cdk/src/wallet/p2pk.rs b/crates/cdk/src/wallet/p2pk.rs new file mode 100644 index 000000000..aa20354ed --- /dev/null +++ b/crates/cdk/src/wallet/p2pk.rs @@ -0,0 +1,140 @@ +//! This module provides deterministic public key generation. +use std::sync::Arc; + +use bitcoin::bip32::{ChildNumber, DerivationPath, Xpriv}; +use bitcoin::Network; +use cdk_common::database::{self, WalletDatabase}; +use cdk_common::wallet::P2PKSigningKey; +use cdk_common::{PublicKey, SECP256K1}; + +use crate::error::Error; + +const CASHU_PURPOSE: u32 = 129373; +const P2PK_PURPOSE: u32 = 10; + +/// Generates and stores public key in database +pub async fn generate_public_key( + localstore: &Arc + Send + Sync>, + seed: &[u8; 64], +) -> Result { + let public_keys = localstore.list_p2pk_keys().await?; + + let mut last_derivation_index = 0; + + for public_key in public_keys { + if public_key.derivation_index >= last_derivation_index { + last_derivation_index = public_key.derivation_index + 1; + } + } + + let derivation_path = DerivationPath::from(vec![ + ChildNumber::from_hardened_idx(CASHU_PURPOSE)?, + ChildNumber::from_hardened_idx(P2PK_PURPOSE)?, + ChildNumber::from_hardened_idx(0)?, + ChildNumber::from_hardened_idx(0)?, + ChildNumber::from_normal_idx(last_derivation_index)?, + ]); + + let xpriv = Xpriv::new_master(Network::Bitcoin, seed)?; + + let derived_key = xpriv.derive_priv(&SECP256K1, &derivation_path)?.private_key; + let pubkey = PublicKey::from(derived_key.public_key(&SECP256K1)); + + localstore + .add_p2pk_key(&pubkey, derivation_path, last_derivation_index) + .await?; + Ok(pubkey) +} + +/// Gets public key by its hex value +pub async fn get_public_key( + localstore: &Arc + Send + Sync>, + pubkey: &PublicKey, +) -> Result, database::Error> { + localstore.get_p2pk_key(pubkey).await +} + +/// Gets list of stored public keys in database +pub async fn get_public_keys( + localstore: &Arc + Send + Sync>, +) -> Result, database::Error> { + localstore.list_p2pk_keys().await +} + +/// Gets the latest generated P2PK signing key (most recently created) +pub async fn get_latest_public_key( + localstore: &Arc + Send + Sync>, +) -> Result, database::Error> { + localstore.latest_p2pk().await +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + use std::sync::Arc; + + use bip39::Mnemonic; + use cdk_common::database::WalletDatabase; + + use super::*; + + #[tokio::test] + async fn nut13_test_vector() { + let localstore: Arc + Send + Sync> = + Arc::new(cdk_sqlite::wallet::memory::empty().await.unwrap()); + let mnemonic = Mnemonic::from_str( + "half depart obvious quality work element tank gorilla view sugar picture humble", + ) + .unwrap(); + + let seed = mnemonic.to_seed_normalized(""); + + let pubkey = generate_public_key(&localstore, &seed).await.unwrap(); + let pubkey_1 = generate_public_key(&localstore, &seed).await.unwrap(); + let pubkey_2 = generate_public_key(&localstore, &seed).await.unwrap(); + let pubkey_3 = generate_public_key(&localstore, &seed).await.unwrap(); + let pubkey_4 = generate_public_key(&localstore, &seed).await.unwrap(); + + assert_eq!( + pubkey.to_hex(), + "021693d45f4fdf610ae641fedb0944fb460fbb8264f21c19d2626c3da755fcbbcb".to_string() + ); + assert_eq!( + pubkey_1.to_hex(), + "0395461ab678058c0ed6aa39f38dda490eaa163e9ad27070b23ec3d06b41e07535".to_string() + ); + assert_eq!( + pubkey_2.to_hex(), + "02a05e4e593a633e9b4405f01c9632c8afde24cb613017a1aee56fd76291ad26d1".to_string() + ); + assert_eq!( + pubkey_3.to_hex(), + "033addea25c3873b93d67d536c61c9d9c993f6efd8b9dfa657951b66b5001e51dd".to_string() + ); + assert_eq!( + pubkey_4.to_hex(), + "03c964bdf42fc82b6c574615746eeca37527a24f1fdfc1b34a732c53843b5744a5".to_string() + ); + let stored_keys = localstore.list_p2pk_keys().await.unwrap(); + assert_eq!( + stored_keys[0].derivation_path.to_string(), + "129373'/10'/0'/0'/4" + ); + assert_eq!( + stored_keys[1].derivation_path.to_string(), + "129373'/10'/0'/0'/3" + ); + assert_eq!( + stored_keys[2].derivation_path.to_string(), + "129373'/10'/0'/0'/2" + ); + assert_eq!( + stored_keys[3].derivation_path.to_string(), + "129373'/10'/0'/0'/1" + ); + assert_eq!( + stored_keys[4].derivation_path.to_string(), + "129373'/10'/0'/0'/0" + ); + } +} diff --git a/crates/cdk/src/wallet/receive/saga/mod.rs b/crates/cdk/src/wallet/receive/saga/mod.rs index 908f6f9bc..05e4e4af0 100644 --- a/crates/cdk/src/wallet/receive/saga/mod.rs +++ b/crates/cdk/src/wallet/receive/saga/mod.rs @@ -124,10 +124,10 @@ impl<'a> ReceiveSaga<'a, Initial> { }) .collect::, _>>()?; - let p2pk_signing_keys: HashMap = opts + let mut p2pk_signing_keys: HashMap = opts .p2pk_signing_keys .iter() - .map(|s| (s.x_only_public_key(&SECP256K1).0, s)) + .map(|s| (s.x_only_public_key(&SECP256K1).0, s.clone())) .collect(); // Process each proof: verify DLEQ, handle P2PK/HTLC @@ -170,7 +170,6 @@ impl<'a> ReceiveSaga<'a, Initial> { // For HTLC, there is no slot 0 pubkey. But slot index for the tags still starts at 1! } } - if let Some(mut cond_pubkeys) = conditions.pubkeys { pubkeys.append(&mut cond_pubkeys); } @@ -183,6 +182,16 @@ impl<'a> ReceiveSaga<'a, Initial> { Kind::P2PK => i as u8, _ => (i + 1) as u8, // HTLC skips slot 0 since it's a hash, not a pubkey }; + let x_only_pubkey = pubkey.x_only_public_key(); + + if let std::collections::hash_map::Entry::Vacant(entry) = + p2pk_signing_keys.entry(x_only_pubkey) + { + if let Some(secret_key) = self.wallet.get_signing_key(pubkey).await? { + entry.insert(secret_key.clone()); + } + } + if let Some(ephemeral_key) = proof.p2pk_e { for signing_key in p2pk_signing_keys.values() { if let Ok(r) = @@ -200,13 +209,16 @@ impl<'a> ReceiveSaga<'a, Initial> { } } } - } else if let Some(signing) = - p2pk_signing_keys.get(&pubkey.x_only_public_key()) - { + } else if let Some(signing) = p2pk_signing_keys.get(&x_only_pubkey) { proof.sign_p2pk(signing.to_owned().clone())?; } } + match secret.kind() { + Kind::P2PK => proof.verify_p2pk()?, + Kind::HTLC => proof.verify_htlc()?, + } + if conditions.sig_flag.eq(&SigFlag::SigAll) { _sig_flag = SigFlag::SigAll; } @@ -225,6 +237,7 @@ impl<'a> ReceiveSaga<'a, Initial> { proofs, proofs_amount, active_keyset_id, + p2pk_signing_keys, }, }) } @@ -326,19 +339,11 @@ impl<'a> ReceiveSaga<'a, Prepared> { // Determine if SigAll signing is needed let sig_flag = self.determine_sig_flag()?; if sig_flag == SigFlag::SigAll { - let p2pk_signing_keys: HashMap = self - .state_data - .options - .p2pk_signing_keys - .iter() - .map(|s| (s.x_only_public_key(&SECP256K1).0, s)) - .collect(); - for blinded_message in pre_swap.swap_request.outputs_mut() { - for signing_key in p2pk_signing_keys.values() { + for signing_key in self.state_data.p2pk_signing_keys.values() { // Sign the outputs of the swap using standard P2PK since output // P2BK requires ephemeral keys which is handled at creation. - blinded_message.sign_p2pk((**signing_key).clone())? + blinded_message.sign_p2pk(signing_key.to_owned().clone())? } } } diff --git a/crates/cdk/src/wallet/receive/saga/state.rs b/crates/cdk/src/wallet/receive/saga/state.rs index 1b56fa4bf..c75210ebd 100644 --- a/crates/cdk/src/wallet/receive/saga/state.rs +++ b/crates/cdk/src/wallet/receive/saga/state.rs @@ -12,9 +12,12 @@ //! └─> amount(), into_amount() //! ``` +use std::collections::HashMap; + +use bitcoin::XOnlyPublicKey; use uuid::Uuid; -use crate::nuts::{Id, Proofs}; +use crate::nuts::{Id, Proofs, SecretKey}; use crate::wallet::receive::ReceiveOptions; use crate::Amount; @@ -44,6 +47,8 @@ pub struct Prepared { pub proofs_amount: Amount, /// Active keyset ID for the swap pub active_keyset_id: Id, + /// P2PK signing keys (from options + wallet database lookups) + pub p2pk_signing_keys: HashMap, } /// Finalized state - receive operation completed successfully. From 8dfa437e7f1603ce8be277dd704555c1844a6f1d Mon Sep 17 00:00:00 2001 From: lescuer97 Date: Wed, 25 Mar 2026 12:58:29 +0100 Subject: [PATCH 2/9] fmt --- crates/cdk-common/src/wallet/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/cdk-common/src/wallet/mod.rs b/crates/cdk-common/src/wallet/mod.rs index 6dd5ae548..d8c6e0332 100644 --- a/crates/cdk-common/src/wallet/mod.rs +++ b/crates/cdk-common/src/wallet/mod.rs @@ -4,8 +4,8 @@ use std::collections::HashMap; use std::fmt; use std::str::FromStr; -use bitcoin::bip32::DerivationPath; use async_trait::async_trait; +use bitcoin::bip32::DerivationPath; use bitcoin::hashes::{sha256, Hash, HashEngine}; use cashu::amount::SplitTarget; use cashu::nuts::nut07::ProofState; From 4e2a515173f146747fbbb8ec128fe586a63c1ad8 Mon Sep 17 00:00:00 2001 From: lescuer97 Date: Thu, 26 Mar 2026 18:12:08 +0100 Subject: [PATCH 3/9] PR fixes --- crates/cdk-ffi/src/types/wallet.rs | 2 -- crates/cdk-sql-common/src/wallet/mod.rs | 27 ------------------------- crates/cdk/src/wallet/p2pk.rs | 6 +++--- 3 files changed, 3 insertions(+), 32 deletions(-) diff --git a/crates/cdk-ffi/src/types/wallet.rs b/crates/cdk-ffi/src/types/wallet.rs index 03ab1fa02..01ee2d0a1 100644 --- a/crates/cdk-ffi/src/types/wallet.rs +++ b/crates/cdk-ffi/src/types/wallet.rs @@ -756,5 +756,3 @@ impl From for WalletKey { } } } - -pub use cdk_common::wallet::{WalletSaga, WalletSagaState}; diff --git a/crates/cdk-sql-common/src/wallet/mod.rs b/crates/cdk-sql-common/src/wallet/mod.rs index 7a3847257..7cf5f14dd 100644 --- a/crates/cdk-sql-common/src/wallet/mod.rs +++ b/crates/cdk-sql-common/src/wallet/mod.rs @@ -2086,30 +2086,3 @@ fn sql_row_to_p2pk_signing_key(row: Vec) -> Result database::KVStoreDatabase for SQLWalletDatabase -where - RM: DatabasePool + 'static, -{ - type Err = Error; - - async fn kv_read( - &self, - primary_namespace: &str, - secondary_namespace: &str, - key: &str, - ) -> Result>, Error> { - crate::keyvalue::kv_read(&self.pool, primary_namespace, secondary_namespace, key).await - } - - async fn kv_list( - &self, - primary_namespace: &str, - secondary_namespace: &str, - ) -> Result, Error> { - crate::keyvalue::kv_list(&self.pool, primary_namespace, secondary_namespace).await - } -} diff --git a/crates/cdk/src/wallet/p2pk.rs b/crates/cdk/src/wallet/p2pk.rs index aa20354ed..fcde95fa0 100644 --- a/crates/cdk/src/wallet/p2pk.rs +++ b/crates/cdk/src/wallet/p2pk.rs @@ -9,8 +9,8 @@ use cdk_common::{PublicKey, SECP256K1}; use crate::error::Error; -const CASHU_PURPOSE: u32 = 129373; -const P2PK_PURPOSE: u32 = 10; +const P2PK_PURPOSE: u32 = 129373; +const P2PK_ACCOUNT: u32 = 10; /// Generates and stores public key in database pub async fn generate_public_key( @@ -28,8 +28,8 @@ pub async fn generate_public_key( } let derivation_path = DerivationPath::from(vec![ - ChildNumber::from_hardened_idx(CASHU_PURPOSE)?, ChildNumber::from_hardened_idx(P2PK_PURPOSE)?, + ChildNumber::from_hardened_idx(P2PK_ACCOUNT)?, ChildNumber::from_hardened_idx(0)?, ChildNumber::from_hardened_idx(0)?, ChildNumber::from_normal_idx(last_derivation_index)?, From 709ab36f168271c07ce732a45a64c582c2bb80bd Mon Sep 17 00:00:00 2001 From: lescuer97 Date: Thu, 26 Mar 2026 18:12:29 +0100 Subject: [PATCH 4/9] add wallet trait --- crates/cdk-common/src/wallet/mod.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/crates/cdk-common/src/wallet/mod.rs b/crates/cdk-common/src/wallet/mod.rs index d8c6e0332..6a9a3d71a 100644 --- a/crates/cdk-common/src/wallet/mod.rs +++ b/crates/cdk-common/src/wallet/mod.rs @@ -951,6 +951,22 @@ pub trait Wallet: Send + Sync { /// The `Spent` state is typically excluded since spent proofs are removed /// from the database. async fn get_proofs_by_states(&self, states: Vec) -> Result; + + // P2PK proofs + /// generates and stores public key in database + async fn generate_public_key(&self) -> Result; + + /// gets public key by it's hex value + async fn get_public_key(&self, pubkey: &PublicKey) -> Result, Error>; + + /// gets list of stored public keys in database + async fn get_public_keys(&self) -> Result, Error>; + + /// Gets the latest generated P2PK signing key (most recently created) + async fn get_latest_public_key(&self) -> Result, Error>; + + /// try to get secret key from p2pk signing key in localstore + async fn get_signing_key(&self, pubkey: &PublicKey) -> Result, Error>; } /// Public key generated for proof signing From d390d565227c4818600d81b5cb9861f84b9d2bd0 Mon Sep 17 00:00:00 2001 From: lescuer97 Date: Thu, 26 Mar 2026 18:27:41 +0100 Subject: [PATCH 5/9] add functions to trait --- crates/cdk-common/src/wallet/mod.rs | 13 +++++---- crates/cdk-ffi/src/wallet_trait.rs | 40 +++++++++++++++++++++++++++ crates/cdk/src/wallet/wallet_trait.rs | 38 ++++++++++++++++++++++++- 3 files changed, 85 insertions(+), 6 deletions(-) diff --git a/crates/cdk-common/src/wallet/mod.rs b/crates/cdk-common/src/wallet/mod.rs index 6a9a3d71a..f97cfe7f3 100644 --- a/crates/cdk-common/src/wallet/mod.rs +++ b/crates/cdk-common/src/wallet/mod.rs @@ -954,19 +954,22 @@ pub trait Wallet: Send + Sync { // P2PK proofs /// generates and stores public key in database - async fn generate_public_key(&self) -> Result; + async fn generate_public_key(&self) -> Result; /// gets public key by it's hex value - async fn get_public_key(&self, pubkey: &PublicKey) -> Result, Error>; + async fn get_public_key( + &self, + pubkey: &PublicKey, + ) -> Result, Self::Error>; /// gets list of stored public keys in database - async fn get_public_keys(&self) -> Result, Error>; + async fn get_public_keys(&self) -> Result, Self::Error>; /// Gets the latest generated P2PK signing key (most recently created) - async fn get_latest_public_key(&self) -> Result, Error>; + async fn get_latest_public_key(&self) -> Result, Self::Error>; /// try to get secret key from p2pk signing key in localstore - async fn get_signing_key(&self, pubkey: &PublicKey) -> Result, Error>; + async fn get_signing_key(&self, pubkey: &PublicKey) -> Result, Self::Error>; } /// Public key generated for proof signing diff --git a/crates/cdk-ffi/src/wallet_trait.rs b/crates/cdk-ffi/src/wallet_trait.rs index b1d7ea639..c9c4d3783 100644 --- a/crates/cdk-ffi/src/wallet_trait.rs +++ b/crates/cdk-ffi/src/wallet_trait.rs @@ -445,4 +445,44 @@ impl WalletTraitDef for Wallet { let proofs = WalletTraitDef::get_proofs_by_states(self.inner().as_ref(), states).await?; Ok(proofs) } + + /// generates and stores public key in database + async fn generate_public_key(&self) -> Result { + let quote = WalletTraitDef::generate_public_key(self.inner().as_ref()).await?; + Ok(quote) + } + + /// gets public key by it's hex value + async fn get_public_key( + &self, + pubkey: &cdk::nuts::PublicKey, + ) -> Result, Self::Error> { + let pubkey = WalletTraitDef::get_public_key(self.inner().as_ref(), pubkey).await?; + Ok(pubkey) + } + + /// gets list of stored public keys in database + async fn get_public_keys( + &self, + ) -> Result, Self::Error> { + let pubkeys = WalletTraitDef::get_public_keys(self.inner().as_ref()).await?; + Ok(pubkeys) + } + + /// Gets the latest generated P2PK signing key (most recently created) + async fn get_latest_public_key( + &self, + ) -> Result, Self::Error> { + let pubkey = WalletTraitDef::get_latest_public_key(self.inner().as_ref()).await?; + Ok(pubkey) + } + + /// try to get secret key from p2pk signing key in localstore + async fn get_signing_key( + &self, + pubkey: &cdk::nuts::PublicKey, + ) -> Result, Self::Error> { + let signing_key = WalletTraitDef::get_signing_key(self.inner().as_ref(), pubkey).await?; + Ok(signing_key) + } } diff --git a/crates/cdk/src/wallet/wallet_trait.rs b/crates/cdk/src/wallet/wallet_trait.rs index c63fd7ca5..ff7b237c0 100644 --- a/crates/cdk/src/wallet/wallet_trait.rs +++ b/crates/cdk/src/wallet/wallet_trait.rs @@ -17,10 +17,11 @@ use cdk_common::wallet::{ MeltQuote, MintQuote, ReceiveOptions, Restored, SendOptions, Transaction, TransactionDirection, TransactionId, Wallet as WalletTrait, }; -use cdk_common::Amount; +use cdk_common::{Amount, PublicKey, SecretKey}; use tracing::instrument; use uuid::Uuid; +use crate::wallet::p2pk; use crate::wallet::subscription::ActiveSubscription; use crate::Error; @@ -365,4 +366,39 @@ impl WalletTrait for super::Wallet { ) -> Result { self.get_proofs_by_states(states).await } + /// generates and stores public key in database + async fn generate_public_key(&self) -> Result { + return p2pk::generate_public_key(&self.localstore, &self.seed).await; + } + + /// gets public key by it's hex value + async fn get_public_key( + &self, + pubkey: &PublicKey, + ) -> Result, Self::Error> { + let pubkey = self.get_public_key(pubkey).await?; + Ok(pubkey) + } + + /// gets list of stored public keys in database + async fn get_public_keys( + &self, + ) -> Result, Self::Error> { + let pubkeys = self.get_public_keys().await?; + Ok(pubkeys) + } + + /// Gets the latest generated P2PK signing key (most recently created) + async fn get_latest_public_key( + &self, + ) -> Result, Self::Error> { + let pubkey = self.get_latest_public_key().await?; + Ok(pubkey) + } + + /// try to get secret key from p2pk signing key in localstore + async fn get_signing_key(&self, pubkey: &PublicKey) -> Result, Self::Error> { + let signing_key = self.get_signing_key(pubkey).await?; + Ok(signing_key) + } } From e0542960fe7957d3c1e31a2a1788ecdf920261e8 Mon Sep 17 00:00:00 2001 From: lescuer97 Date: Thu, 26 Mar 2026 18:52:36 +0100 Subject: [PATCH 6/9] change how P2PK generation works --- crates/cdk/src/wallet/mod.rs | 35 ++++++- crates/cdk/src/wallet/p2pk.rs | 145 ++++++++++++-------------- crates/cdk/src/wallet/wallet_trait.rs | 3 +- 3 files changed, 96 insertions(+), 87 deletions(-) diff --git a/crates/cdk/src/wallet/mod.rs b/crates/cdk/src/wallet/mod.rs index aded01b29..a612c823f 100644 --- a/crates/cdk/src/wallet/mod.rs +++ b/crates/cdk/src/wallet/mod.rs @@ -6,7 +6,7 @@ use std::str::FromStr; use std::sync::Arc; use std::time::Duration; -use bitcoin::bip32::Xpriv; +use bitcoin::bip32::{ChildNumber, DerivationPath, Xpriv}; use bitcoin::Network; use cdk_common::amount::FeeAndAmounts; use cdk_common::database::{self, WalletDatabase}; @@ -35,6 +35,7 @@ use crate::nuts::{ RestoreRequest, SpendingConditions, State, }; use crate::wallet::mint_metadata_cache::MintMetadataCache; +use crate::wallet::p2pk::{P2PK_ACCOUNT, P2PK_PURPOSE}; use crate::Amount; mod auth; @@ -877,7 +878,31 @@ impl Wallet { /// generates and stores public key in database pub async fn generate_public_key(&self) -> Result { - p2pk::generate_public_key(&self.localstore, &self.seed).await + let public_keys = self.localstore.list_p2pk_keys().await?; + + let mut last_derivation_index = 0; + + for public_key in public_keys { + if public_key.derivation_index >= last_derivation_index { + last_derivation_index = public_key.derivation_index + 1; + } + } + + let derivation_path = DerivationPath::from(vec![ + ChildNumber::from_hardened_idx(P2PK_PURPOSE)?, + ChildNumber::from_hardened_idx(P2PK_ACCOUNT)?, + ChildNumber::from_hardened_idx(0)?, + ChildNumber::from_hardened_idx(0)?, + ChildNumber::from_normal_idx(last_derivation_index)?, + ]); + + let pubkey = p2pk::generate_public_key(&derivation_path, &self.seed).await?; + + self.localstore + .add_p2pk_key(&pubkey, derivation_path, last_derivation_index) + .await?; + + Ok(pubkey) } /// gets public key by it's hex value @@ -885,21 +910,21 @@ impl Wallet { &self, pubkey: &PublicKey, ) -> Result, database::Error> { - p2pk::get_public_key(&self.localstore, pubkey).await + self.localstore.get_p2pk_key(pubkey).await } /// gets list of stored public keys in database pub async fn get_public_keys( &self, ) -> Result, database::Error> { - p2pk::get_public_keys(&self.localstore).await + self.localstore.list_p2pk_keys().await } /// Gets the latest generated P2PK signing key (most recently created) pub async fn get_latest_public_key( &self, ) -> Result, database::Error> { - p2pk::get_latest_public_key(&self.localstore).await + self.localstore.latest_p2pk().await } /// try to get secret key from p2pk signing key in localstore diff --git a/crates/cdk/src/wallet/p2pk.rs b/crates/cdk/src/wallet/p2pk.rs index fcde95fa0..b57c1d1d3 100644 --- a/crates/cdk/src/wallet/p2pk.rs +++ b/crates/cdk/src/wallet/p2pk.rs @@ -1,87 +1,41 @@ //! This module provides deterministic public key generation. -use std::sync::Arc; -use bitcoin::bip32::{ChildNumber, DerivationPath, Xpriv}; +use bitcoin::bip32::{DerivationPath, Xpriv}; use bitcoin::Network; -use cdk_common::database::{self, WalletDatabase}; -use cdk_common::wallet::P2PKSigningKey; use cdk_common::{PublicKey, SECP256K1}; use crate::error::Error; -const P2PK_PURPOSE: u32 = 129373; -const P2PK_ACCOUNT: u32 = 10; +/// purpose used for key derivation +pub const P2PK_PURPOSE: u32 = 129373; + +/// account used for P2PK derivation +pub const P2PK_ACCOUNT: u32 = 10; /// Generates and stores public key in database pub async fn generate_public_key( - localstore: &Arc + Send + Sync>, + derivation_path: &DerivationPath, seed: &[u8; 64], ) -> Result { - let public_keys = localstore.list_p2pk_keys().await?; - - let mut last_derivation_index = 0; - - for public_key in public_keys { - if public_key.derivation_index >= last_derivation_index { - last_derivation_index = public_key.derivation_index + 1; - } - } - - let derivation_path = DerivationPath::from(vec![ - ChildNumber::from_hardened_idx(P2PK_PURPOSE)?, - ChildNumber::from_hardened_idx(P2PK_ACCOUNT)?, - ChildNumber::from_hardened_idx(0)?, - ChildNumber::from_hardened_idx(0)?, - ChildNumber::from_normal_idx(last_derivation_index)?, - ]); - let xpriv = Xpriv::new_master(Network::Bitcoin, seed)?; let derived_key = xpriv.derive_priv(&SECP256K1, &derivation_path)?.private_key; let pubkey = PublicKey::from(derived_key.public_key(&SECP256K1)); - localstore - .add_p2pk_key(&pubkey, derivation_path, last_derivation_index) - .await?; Ok(pubkey) } -/// Gets public key by its hex value -pub async fn get_public_key( - localstore: &Arc + Send + Sync>, - pubkey: &PublicKey, -) -> Result, database::Error> { - localstore.get_p2pk_key(pubkey).await -} - -/// Gets list of stored public keys in database -pub async fn get_public_keys( - localstore: &Arc + Send + Sync>, -) -> Result, database::Error> { - localstore.list_p2pk_keys().await -} - -/// Gets the latest generated P2PK signing key (most recently created) -pub async fn get_latest_public_key( - localstore: &Arc + Send + Sync>, -) -> Result, database::Error> { - localstore.latest_p2pk().await -} - #[cfg(test)] mod tests { use std::str::FromStr; - use std::sync::Arc; use bip39::Mnemonic; - use cdk_common::database::WalletDatabase; + use bitcoin::bip32::{ChildNumber, DerivationPath}; use super::*; #[tokio::test] async fn nut13_test_vector() { - let localstore: Arc + Send + Sync> = - Arc::new(cdk_sqlite::wallet::memory::empty().await.unwrap()); let mnemonic = Mnemonic::from_str( "half depart obvious quality work element tank gorilla view sugar picture humble", ) @@ -89,11 +43,57 @@ mod tests { let seed = mnemonic.to_seed_normalized(""); - let pubkey = generate_public_key(&localstore, &seed).await.unwrap(); - let pubkey_1 = generate_public_key(&localstore, &seed).await.unwrap(); - let pubkey_2 = generate_public_key(&localstore, &seed).await.unwrap(); - let pubkey_3 = generate_public_key(&localstore, &seed).await.unwrap(); - let pubkey_4 = generate_public_key(&localstore, &seed).await.unwrap(); + let derivation_path_0 = DerivationPath::from(vec![ + ChildNumber::from_hardened_idx(P2PK_PURPOSE).unwrap(), + ChildNumber::from_hardened_idx(P2PK_ACCOUNT).unwrap(), + ChildNumber::from_hardened_idx(0).unwrap(), + ChildNumber::from_hardened_idx(0).unwrap(), + ChildNumber::from_normal_idx(0).unwrap(), + ]); + let pubkey = generate_public_key(&derivation_path_0, &seed) + .await + .unwrap(); + + let derivation_path_1 = DerivationPath::from(vec![ + ChildNumber::from_hardened_idx(P2PK_PURPOSE).unwrap(), + ChildNumber::from_hardened_idx(P2PK_ACCOUNT).unwrap(), + ChildNumber::from_hardened_idx(0).unwrap(), + ChildNumber::from_hardened_idx(0).unwrap(), + ChildNumber::from_normal_idx(1).unwrap(), + ]); + let pubkey_1 = generate_public_key(&derivation_path_1, &seed) + .await + .unwrap(); + let derivation_path_2 = DerivationPath::from(vec![ + ChildNumber::from_hardened_idx(P2PK_PURPOSE).unwrap(), + ChildNumber::from_hardened_idx(P2PK_ACCOUNT).unwrap(), + ChildNumber::from_hardened_idx(0).unwrap(), + ChildNumber::from_hardened_idx(0).unwrap(), + ChildNumber::from_normal_idx(2).unwrap(), + ]); + let pubkey_2 = generate_public_key(&derivation_path_2, &seed) + .await + .unwrap(); + let derivation_path_3 = DerivationPath::from(vec![ + ChildNumber::from_hardened_idx(P2PK_PURPOSE).unwrap(), + ChildNumber::from_hardened_idx(P2PK_ACCOUNT).unwrap(), + ChildNumber::from_hardened_idx(0).unwrap(), + ChildNumber::from_hardened_idx(0).unwrap(), + ChildNumber::from_normal_idx(2).unwrap(), + ]); + let pubkey_3 = generate_public_key(&derivation_path_3, &seed) + .await + .unwrap(); + let derivation_path_4 = DerivationPath::from(vec![ + ChildNumber::from_hardened_idx(P2PK_PURPOSE).unwrap(), + ChildNumber::from_hardened_idx(P2PK_ACCOUNT).unwrap(), + ChildNumber::from_hardened_idx(0).unwrap(), + ChildNumber::from_hardened_idx(0).unwrap(), + ChildNumber::from_normal_idx(2).unwrap(), + ]); + let pubkey_4 = generate_public_key(&derivation_path_4, &seed) + .await + .unwrap(); assert_eq!( pubkey.to_hex(), @@ -115,26 +115,11 @@ mod tests { pubkey_4.to_hex(), "03c964bdf42fc82b6c574615746eeca37527a24f1fdfc1b34a732c53843b5744a5".to_string() ); - let stored_keys = localstore.list_p2pk_keys().await.unwrap(); - assert_eq!( - stored_keys[0].derivation_path.to_string(), - "129373'/10'/0'/0'/4" - ); - assert_eq!( - stored_keys[1].derivation_path.to_string(), - "129373'/10'/0'/0'/3" - ); - assert_eq!( - stored_keys[2].derivation_path.to_string(), - "129373'/10'/0'/0'/2" - ); - assert_eq!( - stored_keys[3].derivation_path.to_string(), - "129373'/10'/0'/0'/1" - ); - assert_eq!( - stored_keys[4].derivation_path.to_string(), - "129373'/10'/0'/0'/0" - ); + + assert_eq!(derivation_path_1.to_string(), "129373'/10'/0'/0'/4"); + assert_eq!(derivation_path_3.to_string(), "129373'/10'/0'/0'/3"); + assert_eq!(derivation_path_2.to_string(), "129373'/10'/0'/0'/2"); + assert_eq!(derivation_path_1.to_string(), "129373'/10'/0'/0'/1"); + assert_eq!(derivation_path_0.to_string(), "129373'/10'/0'/0'/0"); } } diff --git a/crates/cdk/src/wallet/wallet_trait.rs b/crates/cdk/src/wallet/wallet_trait.rs index ff7b237c0..33459b4f5 100644 --- a/crates/cdk/src/wallet/wallet_trait.rs +++ b/crates/cdk/src/wallet/wallet_trait.rs @@ -21,7 +21,6 @@ use cdk_common::{Amount, PublicKey, SecretKey}; use tracing::instrument; use uuid::Uuid; -use crate::wallet::p2pk; use crate::wallet::subscription::ActiveSubscription; use crate::Error; @@ -368,7 +367,7 @@ impl WalletTrait for super::Wallet { } /// generates and stores public key in database async fn generate_public_key(&self) -> Result { - return p2pk::generate_public_key(&self.localstore, &self.seed).await; + return self.generate_public_key().await; } /// gets public key by it's hex value From 95c6baa1548f43d5c5c5539a294fa724bbe3ec1c Mon Sep 17 00:00:00 2001 From: lescuer97 Date: Thu, 26 Mar 2026 19:01:13 +0100 Subject: [PATCH 7/9] remove the public nature of P2PK --- crates/cdk/src/wallet/mod.rs | 2 +- crates/cdk/src/wallet/p2pk.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/cdk/src/wallet/mod.rs b/crates/cdk/src/wallet/mod.rs index a612c823f..03aa00bb5 100644 --- a/crates/cdk/src/wallet/mod.rs +++ b/crates/cdk/src/wallet/mod.rs @@ -53,7 +53,7 @@ mod mint_connector; mod mint_metadata_cache; #[cfg(feature = "npubcash")] mod npubcash; -pub mod p2pk; +mod p2pk; pub mod payment_request; mod proofs; mod receive; diff --git a/crates/cdk/src/wallet/p2pk.rs b/crates/cdk/src/wallet/p2pk.rs index b57c1d1d3..06c1c9983 100644 --- a/crates/cdk/src/wallet/p2pk.rs +++ b/crates/cdk/src/wallet/p2pk.rs @@ -79,7 +79,7 @@ mod tests { ChildNumber::from_hardened_idx(P2PK_ACCOUNT).unwrap(), ChildNumber::from_hardened_idx(0).unwrap(), ChildNumber::from_hardened_idx(0).unwrap(), - ChildNumber::from_normal_idx(2).unwrap(), + ChildNumber::from_normal_idx(3).unwrap(), ]); let pubkey_3 = generate_public_key(&derivation_path_3, &seed) .await @@ -89,7 +89,7 @@ mod tests { ChildNumber::from_hardened_idx(P2PK_ACCOUNT).unwrap(), ChildNumber::from_hardened_idx(0).unwrap(), ChildNumber::from_hardened_idx(0).unwrap(), - ChildNumber::from_normal_idx(2).unwrap(), + ChildNumber::from_normal_idx(4).unwrap(), ]); let pubkey_4 = generate_public_key(&derivation_path_4, &seed) .await @@ -116,7 +116,7 @@ mod tests { "03c964bdf42fc82b6c574615746eeca37527a24f1fdfc1b34a732c53843b5744a5".to_string() ); - assert_eq!(derivation_path_1.to_string(), "129373'/10'/0'/0'/4"); + assert_eq!(derivation_path_4.to_string(), "129373'/10'/0'/0'/4"); assert_eq!(derivation_path_3.to_string(), "129373'/10'/0'/0'/3"); assert_eq!(derivation_path_2.to_string(), "129373'/10'/0'/0'/2"); assert_eq!(derivation_path_1.to_string(), "129373'/10'/0'/0'/1"); From cb9f89a41552100ddc1cb82d37790a233d0a213d Mon Sep 17 00:00:00 2001 From: lescuer97 Date: Thu, 26 Mar 2026 19:08:21 +0100 Subject: [PATCH 8/9] comment the purpose --- crates/cdk/src/wallet/p2pk.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/cdk/src/wallet/p2pk.rs b/crates/cdk/src/wallet/p2pk.rs index 06c1c9983..6888a5ab4 100644 --- a/crates/cdk/src/wallet/p2pk.rs +++ b/crates/cdk/src/wallet/p2pk.rs @@ -6,7 +6,9 @@ use cdk_common::{PublicKey, SECP256K1}; use crate::error::Error; -/// purpose used for key derivation +/// This purpose are being used because in base of this PR: https://github.com/cashubtc/nuts/pull/331 +/// It's not the same purpose as the cashu purpose because of production code already being used in +/// the coco wallet pub const P2PK_PURPOSE: u32 = 129373; /// account used for P2PK derivation From 2361f07747a7b5dd83b604a38cbcc14765d97304 Mon Sep 17 00:00:00 2001 From: lescuer97 Date: Fri, 27 Mar 2026 14:41:00 +0100 Subject: [PATCH 9/9] fix pr comments --- crates/cdk-cli/src/sub_commands/get_public_keys.rs | 3 ++- crates/cdk-ffi/src/types/wallet.rs | 7 +++++-- crates/cdk-redb/src/wallet/mod.rs | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/crates/cdk-cli/src/sub_commands/get_public_keys.rs b/crates/cdk-cli/src/sub_commands/get_public_keys.rs index 56a544d98..2f1c345b7 100644 --- a/crates/cdk-cli/src/sub_commands/get_public_keys.rs +++ b/crates/cdk-cli/src/sub_commands/get_public_keys.rs @@ -59,7 +59,8 @@ pub async fn get_public_keys( let list_public_keys = wallet.get_public_keys().await?; if list_public_keys.is_empty() { - println!("\npublic not found!\n"); + println!("\n public not found! \n"); + return Ok(()); } println!("\npublic keys found:\n"); for public_key in list_public_keys { diff --git a/crates/cdk-ffi/src/types/wallet.rs b/crates/cdk-ffi/src/types/wallet.rs index 01ee2d0a1..bc2f2fd43 100644 --- a/crates/cdk-ffi/src/types/wallet.rs +++ b/crates/cdk-ffi/src/types/wallet.rs @@ -2,6 +2,7 @@ use std::collections::HashMap; +use cdk_common::bitcoin; use serde::{Deserialize, Serialize}; use super::amount::{Amount, SplitTarget}; @@ -100,13 +101,15 @@ pub struct P2PKSigningKey { impl TryFrom for cdk_common::wallet::P2PKSigningKey { type Error = crate::error::FfiError; - fn try_from(key: P2PKSigningKey) -> Result { + fn try_from(key: P2PKSigningKey) -> Result { Ok(Self { pubkey: key.pubkey.try_into()?, derivation_path: key .derivation_path .parse() - .expect("Invalid derivation path"), + .map_err(|e: bitcoin::bip32::Error| FfiError::Internal { + error_message: e.to_string(), + })?, derivation_index: key.derivation_index, created_time: key.created_time, }) diff --git a/crates/cdk-redb/src/wallet/mod.rs b/crates/cdk-redb/src/wallet/mod.rs index d6feeae2a..af07c92aa 100644 --- a/crates/cdk-redb/src/wallet/mod.rs +++ b/crates/cdk-redb/src/wallet/mod.rs @@ -1567,7 +1567,7 @@ impl WalletDatabase for WalletRedbDatabase { .map_err(Error::from)? .flatten() .filter_map(|(_k, v)| serde_json::from_str::(v.value()).ok()) - .max_by_key(|key| key.created_time); + .max_by_key(|key| key.derivation_index); Ok(latest_key) }