diff --git a/Cargo.lock b/Cargo.lock index 562d768..09699d0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -192,10 +192,20 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bdk-bip322" +version = "0.1.0" +source = "git+https://github.com/aagbotemi/bdk-bip322.git?branch=master#8aa8195ba378e340cbdf6ab6644b1c641fdfa48c" +dependencies = [ + "bdk_wallet", + "bitcoin", +] + [[package]] name = "bdk-cli" version = "2.0.0" dependencies = [ + "bdk-bip322", "bdk_bitcoind_rpc", "bdk_electrum", "bdk_esplora", diff --git a/Cargo.toml b/Cargo.toml index b7d610a..bb7829e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,7 @@ shlex = { version = "1.3.0", optional = true } payjoin = { version = "1.0.0-rc.1", features = ["v1", "v2", "io", "_test-utils"], optional = true} reqwest = { version = "0.12.23", default-features = false, optional = true } url = { version = "2.5.4", optional = true } +bdk-bip322 = { git = "https://github.com/aagbotemi/bdk-bip322.git", branch = "master", optional = true } [features] default = ["repl", "sqlite"] @@ -55,6 +56,7 @@ rpc = ["bdk_bitcoind_rpc", "_payjoin-dependencies"] # Internal features _payjoin-dependencies = ["payjoin", "reqwest", "url"] +bip322 = ["bdk-bip322"] # Use this to consensus verify transactions at sync time verify = [] diff --git a/src/commands.rs b/src/commands.rs index 14ad9ea..889699e 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -405,6 +405,35 @@ pub enum OfflineWalletSubCommand { #[arg(env = "BASE64_PSBT", required = true)] psbt: Vec, }, + /// Sign a message using BIP322 + #[cfg(feature = "bip322")] + SignMessage { + /// The message to sign + #[arg(long)] + message: String, + /// The signature format (e.g., Legacy, Simple, Full) + #[arg(long, default_value = "simple")] + signature_type: String, + /// Address to sign + #[arg(long)] + address: String, + /// Optional list of specific UTXOs for proof-of-funds (only for `FullWithProofOfFunds`) + #[arg(long)] + utxos: Option>, + }, + /// Verify a BIP322 signature + #[cfg(feature = "bip322")] + VerifyMessage { + /// The signature proof to verify + #[arg(long)] + proof: String, + /// The message that was signed + #[arg(long)] + message: String, + /// The address associated with the signature + #[arg(long)] + address: String, + }, } /// Wallet subcommands that needs a blockchain backend. diff --git a/src/error.rs b/src/error.rs index 3690d4f..9846866 100644 --- a/src/error.rs +++ b/src/error.rs @@ -140,6 +140,10 @@ pub enum BDKCliError { #[cfg(feature = "payjoin")] #[error("Payjoin create request error: {0}")] PayjoinCreateRequest(#[from] payjoin::send::v2::CreateRequestError), + + #[cfg(feature = "bip322")] + #[error("BIP-322 error: {0}")] + Bip322Error(#[from] bdk_bip322::error::Error), } impl From for BDKCliError { diff --git a/src/handlers.rs b/src/handlers.rs index 2138e4c..18a65ff 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -69,6 +69,11 @@ use std::str::FromStr; ))] use std::sync::Arc; +#[cfg(feature = "bip322")] +use crate::error::BDKCliError; +#[cfg(feature = "bip322")] +use bdk_bip322::{BIP322, MessageProof, MessageVerificationResult}; + #[cfg(any( feature = "electrum", feature = "esplora", @@ -591,6 +596,47 @@ pub fn handle_offline_wallet_subcommand( &json!({ "psbt": BASE64_STANDARD.encode(final_psbt.serialize()) }), )?) } + #[cfg(feature = "bip322")] + SignMessage { + message, + signature_type, + address, + utxos, + } => { + let address: Address = parse_address(&address)?; + let signature_format = parse_signature_format(&signature_type)?; + + if !wallet.is_mine(address.script_pubkey()) { + return Err(Error::Generic(format!( + "Address {} does not belong to this wallet.", + address + ))); + } + + let proof: MessageProof = + wallet.sign_message(message.as_str(), signature_format, &address, utxos)?; + + Ok(json!({"proof": proof.to_base64()}).to_string()) + } + #[cfg(feature = "bip322")] + VerifyMessage { + proof, + message, + address, + } => { + let address: Address = parse_address(&address)?; + let parsed_proof: MessageProof = MessageProof::from_base64(&proof) + .map_err(|e| BDKCliError::Generic(format!("Invalid proof: {e}")))?; + + let is_valid: MessageVerificationResult = + wallet.verify_message(&parsed_proof, &message, &address)?; + + Ok(json!({ + "valid": is_valid.valid, + "proven_amount": is_valid.proven_amount.map(|a| a.to_sat()) // optional field + }) + .to_string()) + } } } diff --git a/src/utils.rs b/src/utils.rs index 76e56a0..cf60c38 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -56,6 +56,9 @@ use bdk_wallet::descriptor::Segwitv0; use bdk_wallet::keys::{GeneratableKey, GeneratedKey, bip39::WordCount}; use serde_json::{Value, json}; +#[cfg(feature = "bip322")] +use bdk_bip322::SignatureFormat; + /// Parse the recipient (Address,Amount) argument from cli input. pub(crate) fn parse_recipient(s: &str) -> Result<(ScriptBuf, u64), String> { let parts: Vec<_> = s.split(':').collect(); @@ -95,6 +98,21 @@ pub(crate) fn parse_address(address_str: &str) -> Result { Ok(unchecked_address.assume_checked()) } +/// Function to parse the signature format from a string +#[cfg(feature = "bip322")] +pub(crate) fn parse_signature_format(format_str: &str) -> Result { + match format_str.to_lowercase().as_str() { + "legacy" => Ok(SignatureFormat::Legacy), + "simple" => Ok(SignatureFormat::Simple), + "full" => Ok(SignatureFormat::Full), + "fullproofoffunds" => Ok(SignatureFormat::FullProofOfFunds), + _ => Err(Error::Generic( + "Invalid signature format. Use 'legacy', 'simple', 'full', or 'fullproofoffunds'" + .to_string(), + )), + } +} + /// Prepare bdk-cli home directory /// /// This function is called to check if [`crate::CliOpts`] datadir is set.