From: Vihiga Tyonum Date: Tue, 14 Jul 2026 01:37:57 +0000 (+0100) Subject: fix(bip322): Re-enable bip322 feature X-Git-Tag: v4.0.0~5^2~1 X-Git-Url: http://internal-gitweb-vhost/blockdata/script/encode/-script/ui-kit/struct.EncoderStringWriter.html?a=commitdiff_plain;h=90270f74bf3f3b7b9c96a4e85e911878d6ec3698;p=bdk-cli fix(bip322): Re-enable bip322 feature - update bdk_bip322 crate with replacement crate bdk_message_signer - re-enable bip322 feature - update documentation for add_global_xpubs fn --- diff --git a/Cargo.lock b/Cargo.lock index 66273d7..915b4ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -251,6 +251,7 @@ dependencies = [ "bdk_electrum", "bdk_esplora", "bdk_kyoto", + "bdk_message_signer", "bdk_redb", "bdk_sp", "bdk_testenv", @@ -345,6 +346,16 @@ dependencies = [ "bip157", ] +[[package]] +name = "bdk_message_signer" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e0c999b3dcc448ceb6337658c363df3197bbed987b663810497bd9c20ea4ef" +dependencies = [ + "bdk_wallet", + "bitcoin", +] + [[package]] name = "bdk_redb" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index 3411ad4..e97625c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,7 +39,7 @@ shlex = { version = "1.3.0", optional = true } payjoin = { version = "0.25.0", features = ["v1", "v2", "io", "_test-utils"], optional = true} reqwest = { version = "0.13.2", default-features = false, features = ["rustls"], optional = true } url = { version = "2.5.8", optional = true } -# bdk_bip322 = { version = "0.1.0", optional = true } +bdk_message_signer = { version = "0.2.0", optional = true } bitcoin-payment-instructions = { version = "0.7.0", optional = true} [features] @@ -61,7 +61,7 @@ dns_payment = ["bitcoin-payment-instructions"] # Internal features _payjoin-dependencies = ["payjoin", "reqwest", "url", "sqlite"] -# bip322 = ["bdk_bip322"] +bip322 = ["bdk_message_signer"] # Use this to consensus verify transactions at sync time verify = [] diff --git a/src/commands.rs b/src/commands.rs index 2891162..613a4f6 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -13,8 +13,8 @@ //! All subcommands are defined in the below enums. #![allow(clippy::large_enum_variant)] -// #[cfg(feature = "bip322")] -// use crate::handlers::offline::{SignMessageCommand, VerifyMessageCommand}; +#[cfg(feature = "bip322")] +use crate::handlers::offline::{SignMessageCommand, VerifyMessageCommand}; use crate::handlers::{ config::{ListWalletsCommand, SaveConfigCommand}, descriptor::DescriptorCommand, @@ -362,11 +362,11 @@ pub enum OfflineWalletSubCommand { /// Combines multiple PSBTs into one. CombinePsbt(CombinePsbtCommand), /// Sign a message using BIP322 - // #[cfg(feature = "bip322")] - // SignMessage(SignMessageCommand), + #[cfg(feature = "bip322")] + SignMessage(SignMessageCommand), /// Verify a BIP322 signature - // #[cfg(feature = "bip322")] - // VerifyMessage(VerifyMessageCommand), + #[cfg(feature = "bip322")] + VerifyMessage(VerifyMessageCommand), /// Lock UTXO(s) so they're excluded from coin selection. LockUtxo(LockUtxoCommand), /// Unlock previously locked UTXO(s). diff --git a/src/error.rs b/src/error.rs index 96af63a..dfd9f33 100644 --- a/src/error.rs +++ b/src/error.rs @@ -155,9 +155,9 @@ pub enum BDKCliError { #[cfg(feature = "payjoin")] #[error("Payjoin database error: {0}")] PayjoinDb(#[from] crate::handlers::payjoin::db::Error), - // #[cfg(feature = "bip322")] - // #[error("BIP-322 error: {0}")] - // Bip322Error(#[from] bdk_bip322::error::Error), + #[cfg(feature = "bip322")] + #[error("BIP-322 error: {0}")] + Bip322Error(#[from] bdk_message_signer::error::Error), } impl From for BDKCliError { diff --git a/src/handlers/offline.rs b/src/handlers/offline.rs index 048dcfc..993d10c 100644 --- a/src/handlers/offline.rs +++ b/src/handlers/offline.rs @@ -28,12 +28,12 @@ use { bdk_wallet::keys::{DescriptorPublicKey, DescriptorSecretKey, SinglePubKey}, std::collections::HashMap, }; -// #[cfg(feature = "bip322")] -// use { -// crate::utils::parse_signature_format, -// crate::utils::types::MessageResult, -// bdk_bip322::{BIP322, MessageProof}, -// }; +#[cfg(feature = "bip322")] +use { + crate::utils::parse_signature_format, + crate::utils::types::MessageResult, + bdk_message_signer::{MessageProof, MessageSigner}, +}; impl OfflineWalletSubCommand { pub fn execute(&self, ctx: &mut AppContext>) -> Result<(), Error> { @@ -73,14 +73,14 @@ impl OfflineWalletSubCommand { Self::CombinePsbt(combine_psbt_command) => combine_psbt_command .execute(ctx)? .write_out(std::io::stdout()), - // #[cfg(feature = "bip322")] - // Self::SignMessage(sign_message_command) => sign_message_command - // .execute(ctx)? - // .write_out(std::io::stdout()), - // #[cfg(feature = "bip322")] - // Self::VerifyMessage(verify_message_command) => verify_message_command - // .execute(ctx)? - // .write_out(std::io::stdout()), + #[cfg(feature = "bip322")] + Self::SignMessage(sign_message_command) => sign_message_command + .execute(ctx)? + .write_out(std::io::stdout()), + #[cfg(feature = "bip322")] + Self::VerifyMessage(verify_message_command) => verify_message_command + .execute(ctx)? + .write_out(std::io::stdout()), Self::LockUtxo(lock_utxo) => lock_utxo.execute(ctx)?.write_out(std::io::stdout()), Self::UnlockUtxo(unlock_utxo) => unlock_utxo.execute(ctx)?.write_out(std::io::stdout()), Self::LockedUtxos(locked_utxos) => { @@ -204,7 +204,7 @@ pub struct CreateTxCommand { #[arg(long = "enable_rbf", short = 'r', default_value_t = true)] pub enable_rbf: bool, - /// Make a PSBT that can be signed by offline signers and hardware wallets. Forces the addition of `non_witness_utxo` and more details to let the signer identify the change output. + /// Make a PSBT that can be signed by offline signers and hardware wallets. Forces the addition of `PSBT_GLOBAL_XPUB` and more details to let the signer identify the change output. #[arg(long = "offline_signer")] pub offline_signer: bool, @@ -773,7 +773,6 @@ impl AppCommand>> for CombinePsbtCommand { } } -/** #[cfg(feature = "bip322")] #[derive(Debug, Parser, Clone, PartialEq)] pub struct SignMessageCommand { @@ -861,12 +860,11 @@ impl AppCommand>> for VerifyMessageCommand { }) } } -**/ #[derive(Parser, Debug, Clone, PartialEq)] pub struct LockUtxoCommand { /// Outpoint(s) to lock, format TXID:VOUT. - #[arg(env = "TXID:VOUT", long = "utxo", required = true, value_parser = parse_outpoint)] + #[arg(env = "TXID", long = "utxo", required = true, value_parser = parse_outpoint)] pub utxos: Vec, } @@ -891,7 +889,7 @@ impl AppCommand>> for LockUtxoCommand { #[derive(Parser, Debug, Clone, PartialEq)] pub struct UnlockUtxoCommand { /// Outpoint(s) to unlock, format TXID:VOUT. - #[arg(env = "TXID:VOUT", long = "utxo", required = true, value_parser = parse_outpoint)] + #[arg(env = "TXID", long = "utxo", required = true, value_parser = parse_outpoint)] pub utxos: Vec, } diff --git a/src/utils/common.rs b/src/utils/common.rs index 0fdc2aa..a5f8c15 100644 --- a/src/utils/common.rs +++ b/src/utils/common.rs @@ -1,8 +1,8 @@ use crate::{commands::WalletOpts, config::WalletConfig, error::BDKCliError as Error}; -// #[cfg(feature = "bip322")] -// use bdk_bip322::SignatureFormat; #[cfg(feature = "cbf")] use bdk_kyoto::{Info, Receiver, UnboundedReceiver, Warning}; +#[cfg(feature = "bip322")] +use bdk_message_signer::SignatureFormat; #[cfg(feature = "silent-payments")] use bdk_sp::encoding::SilentPaymentCode; use bdk_wallet::bitcoin::{Address, Network, OutPoint, ScriptBuf}; @@ -197,20 +197,20 @@ pub(crate) fn parse_sp_code_value_pairs(s: &str) -> Result<(SilentPaymentCode, u Ok((key, value)) } -// /// 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(), -// )), -// } -// } +/// 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(), + )), + } +} pub fn command_requires_db(command: &OfflineWalletSubCommand) -> bool { match command { @@ -232,11 +232,11 @@ pub fn command_requires_db(command: &OfflineWalletSubCommand) -> bool { | OfflineWalletSubCommand::FinalizePsbt(_) | OfflineWalletSubCommand::CombinePsbt(_) => false, - // #[cfg(feature = "bip322")] - // OfflineWalletSubCommand::SignMessage(_) => true, + #[cfg(feature = "bip322")] + OfflineWalletSubCommand::SignMessage(_) => true, - // #[cfg(feature = "bip322")] - // OfflineWalletSubCommand::VerifyMessage(_) => true, + #[cfg(feature = "bip322")] + OfflineWalletSubCommand::VerifyMessage(_) => true, #[cfg(feature = "silent-payments")] OfflineWalletSubCommand::CreateSpTx(_) => true, #[cfg(feature = "dns_payment")] diff --git a/src/utils/types.rs b/src/utils/types.rs index 51855b0..8a10d03 100644 --- a/src/utils/types.rs +++ b/src/utils/types.rs @@ -110,18 +110,18 @@ pub struct KeychainPair { pub internal: T, } -// #[cfg(feature = "bip322")] -// #[derive(Serialize, Debug, Default)] -// pub struct MessageResult { -// #[serde(skip_serializing_if = "Option::is_none")] -// pub proof: Option, - -// #[serde(skip_serializing_if = "Option::is_none")] -// pub valid: Option, - -// #[serde(skip_serializing_if = "Option::is_none")] -// pub proven_amount: Option, -// } +#[cfg(feature = "bip322")] +#[derive(Serialize, Debug, Default)] +pub struct MessageResult { + #[serde(skip_serializing_if = "Option::is_none")] + pub proof: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub valid: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub proven_amount: Option, +} #[derive(Serialize, Debug)] pub struct StatusResult { diff --git a/tests/integration/offline.rs b/tests/integration/offline.rs index 1c0fed2..f19f623 100644 --- a/tests/integration/offline.rs +++ b/tests/integration/offline.rs @@ -150,117 +150,117 @@ mod test_offline { .failure() .stderr(predicate::str::contains("Invalid")); } - /** - #[cfg(feature = "bip322")] - // #[test] - fn test_sign_message_and_verify_message() { - let (cli, mut cmd_init) = setup_wallet_config(); - cmd_init.assert().success(); - - let message = "bdk-cli integration test"; - - // Reveal exactly one address and reuse it for both sign and verify. - let addr_output = cli - .wallet_cmd(&["--wallet", WALLET_NAME, "new_address"]) - .output() - .expect("Failed to generate address"); - assert!( - addr_output.status.success(), - "new_address failed: {}", - String::from_utf8_lossy(&addr_output.stderr) - ); - let addr_json: Value = serde_json::from_slice(&addr_output.stdout).unwrap(); - let address = addr_json["address"] - .as_str() - .expect("missing address") - .to_string(); - println!("DEBUG signing address: {address}"); - - // Sign (default signature_type = "simple"). - let sign_output = cli - .wallet_cmd(&[ - "--wallet", - WALLET_NAME, - "sign_message", - "--message", - message, - "--address", - &address, - ]) - .output() - .expect("Failed to sign message"); - assert!( - sign_output.status.success(), - "sign_message failed: {}", - String::from_utf8_lossy(&sign_output.stderr) - ); - let sign_json: Value = serde_json::from_slice(&sign_output.stdout).unwrap(); - let proof = sign_json["proof"].as_str().expect("missing proof"); - println!("DEBUG proof: {proof}"); - println!("DEBUG verifying same address: {address}"); - - // Verify with the identical address + message. - cli.wallet_cmd(&[ + + #[cfg(feature = "bip322")] + #[test] + fn test_sign_message_and_verify_message() { + let (cli, mut cmd_init) = setup_wallet_config(); + cmd_init.assert().success(); + + let message = "bdk-cli integration test"; + + // Reveal exactly one address and reuse it for both sign and verify. + let addr_output = cli + .wallet_cmd(&["--wallet", WALLET_NAME, "new_address"]) + .output() + .expect("Failed to generate address"); + assert!( + addr_output.status.success(), + "new_address failed: {}", + String::from_utf8_lossy(&addr_output.stderr) + ); + let addr_json: Value = serde_json::from_slice(&addr_output.stdout).unwrap(); + let address = addr_json["address"] + .as_str() + .expect("missing address") + .to_string(); + println!("DEBUG signing address: {address}"); + + // Sign (default signature_type = "simple"). + let sign_output = cli + .wallet_cmd(&[ "--wallet", WALLET_NAME, - "verify_message", - "--address", - &address, + "sign_message", "--message", message, - "--proof", - proof, + "--address", + &address, ]) - .assert() - .success() - .stdout(predicate::str::contains("\"valid\": true")); - } - - #[cfg(feature = "bip322")] - // #[test] - fn test_verify_message_rejects_tampered_message() { - let (cli, mut cmd_init) = setup_wallet_config(); - cmd_init.assert().success(); - - let addr_output = cli - .wallet_cmd(&["--wallet", WALLET_NAME, "new_address"]) - .output() - .expect("Failed to generate address"); - let addr_json: Value = serde_json::from_slice(&addr_output.stdout).unwrap(); - let address = addr_json["address"].as_str().unwrap(); - - let sign_output = cli - .wallet_cmd(&[ - "--wallet", - WALLET_NAME, - "sign_message", - "--message", - "original message", - "--address", - address, - ]) - .output() - .expect("Failed to sign message"); - let sign_json: Value = serde_json::from_slice(&sign_output.stdout).unwrap(); - let proof = sign_json["proof"].as_str().unwrap(); - - // A tampered message should fail - cli.wallet_cmd(&[ + .output() + .expect("Failed to sign message"); + assert!( + sign_output.status.success(), + "sign_message failed: {}", + String::from_utf8_lossy(&sign_output.stderr) + ); + let sign_json: Value = serde_json::from_slice(&sign_output.stdout).unwrap(); + let proof = sign_json["proof"].as_str().expect("missing proof"); + println!("DEBUG proof: {proof}"); + println!("DEBUG verifying same address: {address}"); + + // Verify with the identical address + message. + cli.wallet_cmd(&[ + "--wallet", + WALLET_NAME, + "verify_message", + "--address", + &address, + "--message", + message, + "--proof", + proof, + ]) + .assert() + .success() + .stdout(predicate::str::contains("\"valid\": true")); + } + + #[cfg(feature = "bip322")] + #[test] + fn test_verify_message_rejects_tampered_message() { + let (cli, mut cmd_init) = setup_wallet_config(); + cmd_init.assert().success(); + + let addr_output = cli + .wallet_cmd(&["--wallet", WALLET_NAME, "new_address"]) + .output() + .expect("Failed to generate address"); + let addr_json: Value = serde_json::from_slice(&addr_output.stdout).unwrap(); + let address = addr_json["address"].as_str().unwrap(); + + let sign_output = cli + .wallet_cmd(&[ "--wallet", WALLET_NAME, - "verify_message", - "--proof", - proof, + "sign_message", "--message", - "tampered message", + "original message", "--address", address, ]) - .assert() - .success() - .stdout(predicate::str::contains("\"valid\": false")); - } - **/ + .output() + .expect("Failed to sign message"); + let sign_json: Value = serde_json::from_slice(&sign_output.stdout).unwrap(); + let proof = sign_json["proof"].as_str().unwrap(); + + // A tampered message should fail + cli.wallet_cmd(&[ + "--wallet", + WALLET_NAME, + "verify_message", + "--proof", + proof, + "--message", + "tampered message", + "--address", + address, + ]) + .assert() + .success() + .stdout(predicate::str::contains("\"valid\": false")); + } + #[test] fn test_create_tx_send_all_rejects_multiple_recipients() { let (cli, mut cmd_init) = setup_wallet_config(); diff --git a/tests/integration/online.rs b/tests/integration/online.rs index 1a8f2ee..e6d7108 100644 --- a/tests/integration/online.rs +++ b/tests/integration/online.rs @@ -813,111 +813,111 @@ mod test_online { ); } } - /** - #[cfg(feature = "bip322")] - #[test] - fn test_verify_message_proof_of_funds_uses_persisted_utxos() { - let env = TestEnv::new().expect("Failed to start bdk_testenv"); - let server_url = env.electrsd.electrum_url.as_str(); - let temp_dir = TempDir::new().unwrap(); - let cli = BdkCli::new("regtest", Some(temp_dir.path().to_path_buf())); - // A wpkh wallet - let desc = cli.cmd("descriptor", &["--type", "wpkh"]).output().unwrap(); - let desc_val: Value = serde_json::from_slice(&desc.stdout).unwrap(); - let ext_desc = desc_val["private_descriptors"]["external"] - .as_str() - .unwrap(); - let int_desc = desc_val["private_descriptors"]["internal"] - .as_str() - .unwrap(); - cli.build_base_cmd() - .arg("wallet") - .arg("--wallet") - .arg(WALLET_NAME) - .arg("config") - .arg("--ext-descriptor") - .arg(ext_desc) - .arg("--int-descriptor") - .arg(int_desc) - .arg("--client-type") - .arg("electrum") - .arg("--database-type") - .arg("sqlite") - .arg("--url") - .arg(server_url) - .assert() - .success(); + #[cfg(feature = "bip322")] + #[test] + fn test_verify_message_proof_of_funds_uses_persisted_utxos() { + let env = TestEnv::new().expect("Failed to start bdk_testenv"); + let server_url = env.electrsd.electrum_url.as_str(); + let temp_dir = TempDir::new().unwrap(); + let cli = BdkCli::new("regtest", Some(temp_dir.path().to_path_buf())); - // Fund a single small UTXO - let address = cli_new_address(&cli); - let node_addr = env - .rpc_client() - .get_new_address(None, None) - .unwrap() - .assume_checked(); - env.mine_blocks(101, Some(node_addr)).unwrap(); - env.wait_until_electrum_sees_block(Duration::from_secs(10)) - .unwrap(); - let txid = env.send(&address, Amount::from_sat(5_000)).unwrap(); - env.wait_until_electrum_sees_txid(txid, Duration::from_secs(10)) - .unwrap(); - env.mine_blocks(3, None).unwrap(); - env.wait_until_electrum_sees_block(Duration::from_secs(10)) - .unwrap(); - cli_full_scan(&cli); + // A wpkh wallet + let desc = cli.cmd("descriptor", &["--type", "wpkh"]).output().unwrap(); + let desc_val: Value = serde_json::from_slice(&desc.stdout).unwrap(); + let ext_desc = desc_val["private_descriptors"]["external"] + .as_str() + .unwrap(); + let int_desc = desc_val["private_descriptors"]["internal"] + .as_str() + .unwrap(); + cli.build_base_cmd() + .arg("wallet") + .arg("--wallet") + .arg(WALLET_NAME) + .arg("config") + .arg("--ext-descriptor") + .arg(ext_desc) + .arg("--int-descriptor") + .arg(int_desc) + .arg("--client-type") + .arg("electrum") + .arg("--database-type") + .arg("sqlite") + .arg("--url") + .arg(server_url) + .assert() + .success(); - // The funded outpoint to prove control of. - let unspent = run_wallet_json(&cli, &["unspent"]); - let outpoint = unspent["items"][0]["outpoint"] - .as_str() - .expect("funded wallet should have one UTXO") - .to_string(); - let addr = address.to_string(); - - // Produce a proof-of-funds over that UTXO. - let proof = run_wallet_json( - &cli, - &[ - "sign_message", - "--message", - "proof-of-funds", - "--address", - &addr, - "--signature-type", - "fullproofoffunds", - "--utxos", - &outpoint, - ], - )["proof"] - .as_str() - .expect("sign_message should return a proof") - .to_string(); + // Fund a single small UTXO + let address = cli_new_address(&cli); + let node_addr = env + .rpc_client() + .get_new_address(None, None) + .unwrap() + .assume_checked(); + env.mine_blocks(101, Some(node_addr)).unwrap(); + env.wait_until_electrum_sees_block(Duration::from_secs(10)) + .unwrap(); + let txid = env.send(&address, Amount::from_sat(5_000)).unwrap(); + env.wait_until_electrum_sees_txid(txid, Duration::from_secs(10)) + .unwrap(); + env.mine_blocks(3, None).unwrap(); + env.wait_until_electrum_sees_block(Duration::from_secs(10)) + .unwrap(); + cli_full_scan(&cli); + + // The funded outpoint to prove control of. + let unspent = run_wallet_json(&cli, &["unspent"]); + let outpoint = unspent["items"][0]["outpoint"] + .as_str() + .expect("funded wallet should have one UTXO") + .to_string(); + let addr = address.to_string(); + + // Produce a proof-of-funds over that UTXO. + let proof = run_wallet_json( + &cli, + &[ + "sign_message", + "--message", + "proof-of-funds", + "--address", + &addr, + "--signature-type", + "fullproofoffunds", + "--utxos", + &outpoint, + ], + )["proof"] + .as_str() + .expect("sign_message should return a proof") + .to_string(); + + let result = run_wallet_json( + &cli, + &[ + "verify_message", + "--proof", + &proof, + "--message", + "proof-of-funds", + "--address", + &addr, + ], + ); + assert_eq!( + result["valid"].as_bool(), + Some(true), + "proof-of-funds should verify against the persisted wallet: {result}" + ); + assert_eq!( + result["proven_amount"].as_u64(), + Some(5_000), + "proven_amount should equal the funded UTXO value: {result}" + ); + } - let result = run_wallet_json( - &cli, - &[ - "verify_message", - "--proof", - &proof, - "--message", - "proof-of-funds", - "--address", - &addr, - ], - ); - assert_eq!( - result["valid"].as_bool(), - Some(true), - "proof-of-funds should verify against the persisted wallet: {result}" - ); - assert_eq!( - result["proven_amount"].as_u64(), - Some(5_000), - "proven_amount should equal the funded UTXO value: {result}" - ); - } - */ // `create_dns_tx` with a plain `--to` recipient #[cfg(feature = "dns_payment")] #[test]