diff --git a/crates/cashu/src/nuts/nut10.rs b/crates/cashu/src/nuts/nut10.rs index d8f3cd4a0..fb5817551 100644 --- a/crates/cashu/src/nuts/nut10.rs +++ b/crates/cashu/src/nuts/nut10.rs @@ -39,7 +39,7 @@ pub(crate) struct SpendingRequirements { pub refund_path: Option, } -/// NUT13 Error +/// NUT10 Error #[derive(Debug, Error)] pub enum Error { /// Secret error @@ -48,6 +48,10 @@ pub enum Error { /// Serde Json error #[error(transparent)] SerdeJsonError(#[from] serde_json::Error), + + /// Proof does not contain enough signature for lock + #[error("proof does not contain enough proofs to be spendable")] + NotEnoughSignatures, } /// NUT10 Secret Kind diff --git a/crates/cashu/src/nuts/nut11/mod.rs b/crates/cashu/src/nuts/nut11/mod.rs index 7f69a1122..37ddf0a65 100644 --- a/crates/cashu/src/nuts/nut11/mod.rs +++ b/crates/cashu/src/nuts/nut11/mod.rs @@ -488,8 +488,6 @@ pub struct Conditions { #[serde(skip_serializing_if = "Option::is_none")] pub refund_keys: Option>, /// Number of signatures required - /// - /// Default is 1 #[serde(skip_serializing_if = "Option::is_none")] pub num_sigs: Option, /// Signature flag diff --git a/crates/cdk-cli/src/main.rs b/crates/cdk-cli/src/main.rs index 7d19bf7cd..0463ad0f7 100644 --- a/crates/cdk-cli/src/main.rs +++ b/crates/cdk-cli/src/main.rs @@ -122,6 +122,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] @@ -326,5 +330,16 @@ async fn main() -> Result<()> { ) .await } + Commands::GeneratePublicKey(sub_command_args) => { + sub_commands::generate_public_key::generate_public_key( + &multi_mint_wallet, + sub_command_args, + ) + .await + } + Commands::GetPublicKeys(sub_command_args) => { + sub_commands::get_public_keys::get_public_keys(&multi_mint_wallet, sub_command_args) + .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..bcf5cff10 --- /dev/null +++ b/crates/cdk-cli/src/sub_commands/generate_public_key.rs @@ -0,0 +1,18 @@ +use anyhow::Result; +use cdk::wallet::MultiMintWallet; +use clap::Args; + +#[derive(Args)] +pub struct GeneratePublicKeySubCommand {} + +pub async fn generate_public_key( + multi_mint_wallet: &MultiMintWallet, + _sub_command_args: &GeneratePublicKeySubCommand, +) -> Result<()> { + let public_key = multi_mint_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..efe6214bb --- /dev/null +++ b/crates/cdk-cli/src/sub_commands/get_public_keys.rs @@ -0,0 +1,44 @@ +use anyhow::Result; +use cdk::wallet::MultiMintWallet; +use clap::Args; + +#[derive(Args)] +pub struct GetPublicKeysSubCommand { + /// Show the latest public key + #[arg(long)] + pub latest: bool, +} + +pub async fn get_public_keys( + multi_mint_wallet: &MultiMintWallet, + sub_command_args: &GetPublicKeysSubCommand, +) -> Result<()> { + if sub_command_args.latest { + let latest_public_key = multi_mint_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 = multi_mint_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 fa654537c..b44cff3b0 100644 --- a/crates/cdk-cli/src/sub_commands/mod.rs +++ b/crates/cdk-cli/src/sub_commands/mod.rs @@ -6,6 +6,8 @@ pub mod check_pending; 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 3eceff2d7..8da900e6d 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; @@ -168,4 +169,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 2250b2ac8..94617b860 100644 --- a/crates/cdk-common/src/database/wallet/test/mod.rs +++ b/crates/cdk-common/src/database/wallet/test/mod.rs @@ -10,6 +10,7 @@ use std::str::FromStr; use std::sync::atomic::{AtomicU64, Ordering}; use std::time::{SystemTime, UNIX_EPOCH}; +use bitcoin::bip32::DerivationPath; use cashu::nut00::KnownMethod; use cashu::secret::Secret; use cashu::{Amount, CurrencyUnit, SecretKey}; @@ -1019,6 +1020,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); +} /// Unit test that is expected to be passed for a correct wallet database implementation #[macro_export] @@ -1059,7 +1177,12 @@ macro_rules! wallet_db_test { kvstore_list, kvstore_update, kvstore_remove, - kvstore_namespace_isolation + kvstore_namespace_isolation, + add_and_get_p2pk_key, + list_p2pk_keys_empty, + list_p2pk_keys_multiple, + latest_p2pk_empty, + latest_p2pk_with_keys ); }; ($make_db_fn:ident, $($name:ident),+ $(,)?) => { diff --git a/crates/cdk-common/src/wallet.rs b/crates/cdk-common/src/wallet.rs index f20c4288a..37f699ff1 100644 --- a/crates/cdk-common/src/wallet.rs +++ b/crates/cdk-common/src/wallet.rs @@ -4,6 +4,7 @@ use std::collections::HashMap; use std::fmt; use std::str::FromStr; +use bitcoin::bip32::DerivationPath; use bitcoin::hashes::{sha256, Hash, HashEngine}; use cashu::util::hex; use cashu::{nut00, PaymentMethod, Proofs, PublicKey}; @@ -374,6 +375,18 @@ impl TryFrom for TransactionId { } } +#[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 f438bb52f..8427ecc78 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::{ KVStoreDatabase as CdkKVStoreDatabase, WalletDatabase as CdkWalletDatabase, }; @@ -105,6 +106,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, @@ -564,6 +582,73 @@ 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: 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: 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: FfiError| cdk::cdk_database::Error::Database(e.to_string().into())) + }) + .transpose()?) + } + // Write methods (non-transactional) async fn update_proofs( @@ -1000,6 +1085,50 @@ where .map_err(FfiError::database) } + 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, @@ -1442,6 +1571,31 @@ macro_rules! impl_ffi_wallet_database { async fn remove_keys(&self, id: Id) -> Result<(), FfiError> { self.inner.remove_keys(id).await } + 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 + } } }; } diff --git a/crates/cdk-ffi/src/multi_mint_wallet.rs b/crates/cdk-ffi/src/multi_mint_wallet.rs index 34c3f1b2b..bfa630c02 100644 --- a/crates/cdk-ffi/src/multi_mint_wallet.rs +++ b/crates/cdk-ffi/src/multi_mint_wallet.rs @@ -686,6 +686,30 @@ impl MultiMintWallet { } Ok(result) } + + /// Get the latest generated P2PK signing key (most recently created) + pub async fn get_latest_public_key(&self) -> Result, FfiError> { + let result = self + .inner + .get_latest_public_key() + .await + .map_err(FfiError::database)?; + Ok(result.map(Into::into)) + } + + pub async fn generate_public_key(&self) -> Result { + let result = self.inner.generate_public_key().await?; + Ok(result.into()) + } + + pub async fn get_public_keys(&self) -> Result, FfiError> { + let result = self + .inner + .get_public_keys() + .await + .map_err(FfiError::database)?; + Ok(result.into_iter().map(Into::into).collect()) + } } /// Payment request methods for MultiMintWallet diff --git a/crates/cdk-ffi/src/postgres.rs b/crates/cdk-ffi/src/postgres.rs index 83997b747..14bdeec33 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 d3aa6b6a6..126910428 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 5a5f6f0bf..2a33c38cc 100644 --- a/crates/cdk-ffi/src/types/wallet.rs +++ b/crates/cdk-ffi/src/types/wallet.rs @@ -9,6 +9,7 @@ use super::amount::{Amount, SplitTarget}; use super::proof::{Proofs, SpendingConditions}; use crate::error::FfiError; use crate::token::Token; +use crate::types::keys::PublicKey; /// FFI-compatible SendMemo #[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)] @@ -462,3 +463,43 @@ impl From for MeltOptions { } } } + +/// FFI-compatible P2PKSigningKey +#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)] +pub struct P2PKSigningKey { + pub pubkey: PublicKey, + pub derivation_path: String, + pub derivation_index: u32, + pub created_time: u64, +} + +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 TryFrom for cdk_common::wallet::P2PKSigningKey { + type Error = FfiError; + + fn try_from(key: P2PKSigningKey) -> Result { + use std::str::FromStr; + + use cdk_common::bitcoin::bip32::DerivationPath; + + let derivation_path = + DerivationPath::from_str(&key.derivation_path).map_err(FfiError::internal)?; + + Ok(Self { + pubkey: key.pubkey.try_into()?, + derivation_path, + derivation_index: key.derivation_index, + created_time: key.created_time, + }) + } +} diff --git a/crates/cdk-redb/src/wallet/migrations.rs b/crates/cdk-redb/src/wallet/migrations.rs index 948e9ab1c..6b6717263 100644 --- a/crates/cdk-redb/src/wallet/migrations.rs +++ b/crates/cdk-redb/src/wallet/migrations.rs @@ -11,7 +11,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"); @@ -200,3 +202,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 3e8f02dc8..0acc0224f 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::common::ProofInfo; use cdk_common::database::{validate_kvstore_params, KVStoreDatabase, WalletDatabase}; use cdk_common::mint_url::MintUrl; @@ -22,7 +23,9 @@ use tracing::instrument; use super::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; @@ -45,11 +48,15 @@ const KEYSET_COUNTER: TableDefinition<&str, u32> = TableDefinition::new("keyset_ // const TRANSACTIONS_TABLE: TableDefinition<&[u8], &str> = TableDefinition::new("transactions"); +// +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)] @@ -113,6 +120,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 {}", @@ -161,6 +172,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())?; } @@ -991,6 +1003,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) + } } #[async_trait] 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 3ab67f7d8..25d9c0a62 100644 --- a/crates/cdk-sql-common/src/wallet/mod.rs +++ b/crates/cdk-sql-common/src/wallet/mod.rs @@ -6,11 +6,13 @@ use std::str::FromStr; use std::sync::Arc; use async_trait::async_trait; +use bitcoin::bip32::DerivationPath; use cdk_common::common::ProofInfo; 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, Transaction, TransactionDirection, TransactionId}; use cdk_common::{ database, Amount, CurrencyUnit, Id, KeySet, KeySetInfo, Keys, MintInfo, PaymentMethod, Proof, @@ -739,7 +741,6 @@ where .collect::>()) } - #[instrument(skip(self))] async fn update_proofs( &self, added: Vec, @@ -1313,6 +1314,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 created_time 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 created_time 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 { @@ -1556,6 +1633,24 @@ fn sql_row_to_transaction(row: Vec) -> Result { }) } +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] diff --git a/crates/cdk/src/wallet/mod.rs b/crates/cdk/src/wallet/mod.rs index 780ce7a82..8d8f22335 100644 --- a/crates/cdk/src/wallet/mod.rs +++ b/crates/cdk/src/wallet/mod.rs @@ -7,10 +7,13 @@ use std::sync::atomic::AtomicBool; 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::{PublicKey, SecretKey, SECP256K1}; use getrandom::getrandom; use subscription::{ActiveSubscription, SubscriptionManager}; #[cfg(any(feature = "auth", feature = "npubcash"))] @@ -52,6 +55,7 @@ mod mint_metadata_cache; pub mod multi_mint_wallet; #[cfg(feature = "npubcash")] mod npubcash; +pub mod p2pk; pub mod payment_request; mod proofs; mod receive; @@ -794,6 +798,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/multi_mint_wallet.rs b/crates/cdk/src/wallet/multi_mint_wallet.rs index dca6db324..148d85046 100644 --- a/crates/cdk/src/wallet/multi_mint_wallet.rs +++ b/crates/cdk/src/wallet/multi_mint_wallet.rs @@ -11,8 +11,10 @@ use std::sync::Arc; use anyhow::Result; use cdk_common::database::WalletDatabase; use cdk_common::task::spawn; -use cdk_common::wallet::{MeltQuote, Transaction, TransactionDirection, TransactionId}; -use cdk_common::{database, KeySetInfo}; +use cdk_common::wallet::{ + MeltQuote, P2PKSigningKey, Transaction, TransactionDirection, TransactionId, +}; +use cdk_common::{database, KeySetInfo, PublicKey}; use tokio::sync::RwLock; use tracing::instrument; use zeroize::Zeroize; @@ -29,6 +31,7 @@ use crate::nuts::{CurrencyUnit, MeltOptions, Proof, Proofs, SpendingConditions, use crate::types::Melted; #[cfg(all(feature = "tor", not(target_arch = "wasm32")))] use crate::wallet::mint_connector::transport::tor_transport::TorAsync; +use crate::wallet::p2pk; use crate::wallet::types::MintQuote; use crate::{Amount, Wallet}; @@ -2221,6 +2224,29 @@ impl MultiMintWallet { wallet.melt_human_readable_quote(address, amount_msat).await } + + /// 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 + } } impl Drop for MultiMintWallet { @@ -2512,4 +2538,124 @@ mod tests { let config = WalletConfig::new().with_metadata_cache_ttl(Some(ttl)); assert_eq!(config.metadata_cache_ttl, Some(ttl)); } + // P2PK function tests + + #[tokio::test] + async fn test_generate_public_key() { + let multi_wallet = create_test_multi_wallet().await; + + let pubkey = multi_wallet.generate_public_key().await.unwrap(); + + // Verify the public key is valid (33 bytes compressed public key) + let pubkey_bytes = pubkey.to_bytes(); + assert_eq!(pubkey_bytes.len(), 33); + } + + #[tokio::test] + async fn test_generate_multiple_public_keys() { + let multi_wallet = create_test_multi_wallet().await; + + let pubkey1 = multi_wallet.generate_public_key().await.unwrap(); + let pubkey2 = multi_wallet.generate_public_key().await.unwrap(); + let pubkey3 = multi_wallet.generate_public_key().await.unwrap(); + + // Verify each key is unique + assert_ne!(pubkey1, pubkey2); + assert_ne!(pubkey2, pubkey3); + assert_ne!(pubkey1, pubkey3); + } + + #[tokio::test] + async fn test_get_public_key_not_found() { + let multi_wallet = create_test_multi_wallet().await; + + // Create a random public key that doesn't exist in the database + // Using a hardcoded valid compressed public key bytes + let fake_pubkey_hex = "02a1633cafcc01ebfb6d78e39f687a1f0995c62fc95f51ead10a02ee0be551b5dc"; + let fake_pubkey = PublicKey::from_hex(fake_pubkey_hex).unwrap(); + + let result = multi_wallet.get_public_key(&fake_pubkey).await.unwrap(); + assert!(result.is_none()); + } + + #[tokio::test] + async fn test_get_public_key_found() { + let multi_wallet = create_test_multi_wallet().await; + + // Generate a public key first + let generated_pubkey = multi_wallet.generate_public_key().await.unwrap(); + + // Retrieve it using get_public_key + let result = multi_wallet + .get_public_key(&generated_pubkey) + .await + .unwrap(); + + assert!(result.is_some()); + let signing_key = result.unwrap(); + assert_eq!(signing_key.pubkey, generated_pubkey); + } + + #[tokio::test] + async fn test_get_public_keys_empty() { + let multi_wallet = create_test_multi_wallet().await; + + let keys = multi_wallet.get_public_keys().await.unwrap(); + assert!(keys.is_empty()); + } + + #[tokio::test] + async fn test_get_public_keys_multiple() { + let multi_wallet = create_test_multi_wallet().await; + + // Generate 3 public keys + let pubkey1 = multi_wallet.generate_public_key().await.unwrap(); + let pubkey2 = multi_wallet.generate_public_key().await.unwrap(); + let pubkey3 = multi_wallet.generate_public_key().await.unwrap(); + + let keys = multi_wallet.get_public_keys().await.unwrap(); + + assert_eq!(keys.len(), 3); + + // Verify all generated keys are in the list + let pubkeys: Vec = keys.iter().map(|k| k.pubkey.clone()).collect(); + assert!(pubkeys.contains(&pubkey1)); + assert!(pubkeys.contains(&pubkey2)); + assert!(pubkeys.contains(&pubkey3)); + } + + #[tokio::test] + async fn test_get_latest_public_key_none() { + let multi_wallet = create_test_multi_wallet().await; + + let result = multi_wallet.get_latest_public_key().await.unwrap(); + assert!(result.is_none()); + } + + #[tokio::test] + async fn test_get_latest_public_key_returns_most_recent() { + let multi_wallet = create_test_multi_wallet().await; + + // Generate 3 public keys with delays to ensure distinct created_time timestamps + let _pubkey1 = multi_wallet.generate_public_key().await.unwrap(); + tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; + let _pubkey2 = multi_wallet.generate_public_key().await.unwrap(); + tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; + let pubkey3 = multi_wallet.generate_public_key().await.unwrap(); + + // Get the latest key + let result = multi_wallet.get_latest_public_key().await.unwrap(); + + assert!(result.is_some()); + let latest_key = result.unwrap(); + + // The latest key should be pubkey3 (last generated, most recent created_time) + assert_eq!( + latest_key.pubkey, pubkey3, + "Latest key should be the last generated key (pubkey3)" + ); + + // The derivation index should be 2 (the highest) + assert_eq!(latest_key.derivation_index, 2); + } } diff --git a/crates/cdk/src/wallet/p2pk.rs b/crates/cdk/src/wallet/p2pk.rs new file mode 100644 index 000000000..e1625a89e --- /dev/null +++ b/crates/cdk/src/wallet/p2pk.rs @@ -0,0 +1,140 @@ +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 = 129372; +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(); + + let stored_keys = localstore.list_p2pk_keys().await.unwrap(); + assert_eq!( + pubkey.to_hex(), + "03381fbf0996b81d49c35bae17a70d71db9a9e802b1af5c2516fc90381f4741e06".to_string() + ); + assert_eq!( + pubkey_1.to_hex(), + "039bbb7a9cd234da13a113cdd8e037a25c66bbf3a77139d652786a1d7e9d73e600".to_string() + ); + assert_eq!( + pubkey_2.to_hex(), + "02ffd52ed54761750d75b67342544cc8da8a0994f84c46d546e0ab574dd3651a29".to_string() + ); + assert_eq!( + pubkey_3.to_hex(), + "02751ab780960ff177c2300e440fddc0850238a78782a1cab7b0ae03c41978d92d".to_string() + ); + assert_eq!( + pubkey_4.to_hex(), + "0391a9ba1c3caf39ca0536d44419a6ceeda922ee61aa651a72a60171499c02b423".to_string() + ); + assert_eq!(stored_keys.len(), 5); + assert_eq!( + stored_keys[0].derivation_path.to_string(), + "129372'/10'/0'/0'/0" + ); + assert_eq!( + stored_keys[1].derivation_path.to_string(), + "129372'/10'/0'/0'/1" + ); + assert_eq!( + stored_keys[2].derivation_path.to_string(), + "129372'/10'/0'/0'/2" + ); + assert_eq!( + stored_keys[3].derivation_path.to_string(), + "129372'/10'/0'/0'/3" + ); + assert_eq!( + stored_keys[4].derivation_path.to_string(), + "129372'/10'/0'/0'/4" + ); + } +} diff --git a/crates/cdk/src/wallet/receive.rs b/crates/cdk/src/wallet/receive.rs index e08f9d618..6e2af6ef9 100644 --- a/crates/cdk/src/wallet/receive.rs +++ b/crates/cdk/src/wallet/receive.rs @@ -56,10 +56,10 @@ impl Wallet { }) .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(); for proof in &mut proofs { @@ -99,11 +99,28 @@ impl Wallet { } } for pubkey in pubkeys { - if let Some(signing) = p2pk_signing_keys.get(&pubkey.x_only_public_key()) { - proof.sign_p2pk(signing.to_owned().clone())?; + match p2pk_signing_keys.get(&pubkey.x_only_public_key()) { + Some(signing) => { + proof.sign_p2pk(signing.to_owned().clone())?; + } + None => { + let secret_key_option = self.get_signing_key(&pubkey).await?; + if let Some(secret_key) = secret_key_option { + // cache secret key so it only has to be locked up one for the duration of the receive operation + p2pk_signing_keys + .insert(pubkey.x_only_public_key(), secret_key.clone()); + + proof.sign_p2pk(secret_key.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; }