diff --git a/.github/workflows/cont_integration.yml b/.github/workflows/cont_integration.yml index 4848b2d..4d9980f 100644 --- a/.github/workflows/cont_integration.yml +++ b/.github/workflows/cont_integration.yml @@ -16,7 +16,7 @@ jobs: matrix: rust: - toolchain: stable - - toolchain: 1.75.0 + - toolchain: 1.85.0 steps: - name: Checkout uses: actions/checkout@v4 @@ -53,7 +53,7 @@ jobs: - name: Install Rust toolchain uses: dtolnay/rust-toolchain@v1 with: - toolchain: 1.84.0 + toolchain: 1.85.0 components: clippy - name: Clippy run: cargo clippy --all-targets --all-features -- -D warnings diff --git a/Cargo.toml b/Cargo.toml index c07ba61..08ab67e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,17 +1,13 @@ [package] name = "bdk-bip322" version = "0.1.0" -edition = "2021" -rust-version = "1.75.0" +edition = "2024" +rust-version = "1.85.0" [dependencies] -bitcoin = { version = "0.32.5", features = [ "base64" ], default-features = false } +bitcoin = { version = "0.32.8", default-features = false } +bdk_wallet = { version = "2.3.0", features = ["test-utils"], default-features = false } [features] default = ["std"] std = [] - -[dev-dependencies] -bdk_wallet = { version = "1.2.0", features = ["rusqlite"] } -tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] } -anyhow = "1" diff --git a/README.md b/README.md index eb4c737..4fd3d44 100644 --- a/README.md +++ b/README.md @@ -1,67 +1,71 @@ -# bdk‑bip322 +# `bdk‑bip322` -A Rust library implementing the [BIP‑322 Generic Signed Message Format](https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki) for Bitcoin, proving fund availabilty without actually moving them or commiting to a message. +**Note:** This is an experimental crate exploring a descriptor-based implementation of BIP-322 within the Bitcoin Dev Kit (BDK) ecosystem. + +A Rust library implementing the [BIP‑322: Generic Signed Message Format](https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki) for Bitcoin, built on top of the Bitcoin Dev Kit (BDK) ecosystem. + +`bdk-bip322` enables cryptographic proof of control over Bitcoin addresses and +funds without moving coins or broadcasting transactions, while securely +committing to arbitrary messages. ## Overview -BIP-322 provides a standardized way to sign arbitrary messages with Bitcoin addresses, allowing users to cryptographically prove they control the private keys associated with specific Bitcoin addresses without creating transactions. This is particularly useful for: -- Verifying ownership of funds to exchanges or other parties -- Proving reserves by businesses or custodians -- Authenticating identity in Bitcoin-based applications -- Validating control of addresses in disputes or support cases +BIP-322 defines a standardized, script-agnostic mechanism for signing and +verifying messages with Bitcoin addresses. Unlike legacy `signmessage`, +BIP-322 works across modern script types (SegWit, Taproot) and enables advanced use cases such as proof-of-funds. + +This library provides a **descriptor-based, wallet-native** implementation of +BIP-322, designed for seamless integration with `bdk_wallet`. + +### Common use cases +- Proving ownership of Bitcoin addresses +- Cryptographic proof of reserves or funds +- User authentication in Bitcoin-based applications +- Verifying control of addresses for support or dispute resolution +- Hardware-wallet compatible message signing via PSBTs ## Integration Designed to integrate with the [Bitcoin Dev Kit](https://bitcoindevkit.org/) ecosystem: -- **bdk‑wallet** for key derivation and database‑backed wallets. +- **bdk‑wallet** — descriptor-based wallets, key management, and persistence. +- **PSBT-based workflows** — compatible with hardware and air-gapped signers +No private keys or WIFs are passed directly to this library. ## Minimum Supported Rust Version (MSRV) -This library should compile with any combination of features with Rust 1.75.0 or newer. - +This crate supports **Rust 1.85.0 or newer** across all feature combinations. -## Features +## Supported Signature Formats - **Legacy**: Original P2PKH `signmessage`/`verifymessage` compatibility - **Simple**: SegWit‑only witness stack format - **Full**: Complete PSBT/transaction‑based format (any script, including Taproot) -- **Proof of Funds**: Include additional UTXO inputs to prove control over funds - - -## Real‑World Examples -See the [examples/](/examples) folder for end‑to‑end demos: - -1. Legacy: Sign & verify a P2PKH address with SignatureFormat::Legacy -```bash -cargo run --example sign_verify_legacy -``` - -2. Simple: Sign & verify an address with SignatureFormat::Simple -```bash -cargo run --example sign_verify_simple +- **FullProofOfFunds**: Extends Full format with additional UTXO inputs to prove fund ownership. + +## Usage +### Signing a Message +```rs +use bdk_wallet::{Wallet, KeychainKind}; +use bdk_bip322::{BIP322, SignatureFormat}; + +// `wallet` is already created and synced +let address = wallet.peek_address(KeychainKind::External, 0).address; + +let proof = wallet.sign_bip322( + "Hello Bitcoin", + SignatureFormat::Simple, + &address, + None, +)?; ``` - -3. Full: Sign & verify an address with SignatureFormat::Full -```bash -cargo run --example sign_verify_full +### Verifying a Signature +```rs +let result = wallet.verify_bip322( + &proof, + "Hello Bitcoin", + SignatureFormat::Simple, + &address, +)?; + +assert!(result.valid); ``` -## API Reference -- **Signer::new(priv_wif, message, address, format, proof_of_funds)**: -Build a signer for any BIP‑322 format. - -- **signer.sign() -> Result**: -Produce a Base64‑encoded signature. - -- **Verifier::new(address, signature, message, format, priv_wif_opt)**: -Build a verifier (for Legacy, you must supply the WIF). - -- **verifier.verify() -> Result**: -Returns true if the signature is valid. - ## Contributing Found a bug, have an issue or a feature request? Feel free to open an issue on GitHub. -- Fork the repo - -- Create a feature branch - -- Open a pull request - -Please follow the existing code style, run `cargo fmt` and `cargo clippy`, and include tests for new functionality. \ No newline at end of file diff --git a/clippy.toml b/clippy.toml deleted file mode 100644 index ac9f3cd..0000000 --- a/clippy.toml +++ /dev/null @@ -1 +0,0 @@ -msrv="1.75.0" \ No newline at end of file diff --git a/examples/sign_verify_full.rs b/examples/sign_verify_full.rs deleted file mode 100644 index e9e786f..0000000 --- a/examples/sign_verify_full.rs +++ /dev/null @@ -1,70 +0,0 @@ -use anyhow::Ok; -use bdk_bip322::{SignatureFormat, Signer, Verifier}; -use bdk_wallet::{ - keys::DescriptorSecretKey, miniscript::ToPublicKey, rusqlite::Connection, KeychainKind, Wallet, -}; -use bitcoin::{key::Secp256k1, Address, Network, PrivateKey}; - -const DB_PATH: &str = "bdk-example-esplora-async.sqlite"; -const NETWORK: Network = Network::Signet; -const EXTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)"; -const INTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)"; - -#[tokio::main] -async fn main() -> Result<(), anyhow::Error> { - let mut conn = Connection::open(DB_PATH)?; - - let wallet_opt = Wallet::load() - .descriptor(KeychainKind::External, Some(EXTERNAL_DESC)) - .descriptor(KeychainKind::Internal, Some(INTERNAL_DESC)) - .extract_keys() - .check_network(NETWORK) - .load_wallet(&mut conn)?; - let mut wallet = match wallet_opt { - Some(wallet) => wallet, - None => Wallet::create(EXTERNAL_DESC, INTERNAL_DESC) - .network(NETWORK) - .create_wallet(&mut conn)?, - }; - - wallet.persist(&mut conn)?; - - let private_key_option: Option = wallet - .get_signers(KeychainKind::External) - .signers() - .iter() - .filter_map(|signer| signer.descriptor_secret_key()) - .find_map(|descriptor_secret| { - if let DescriptorSecretKey::XPrv(single_priv) = descriptor_secret { - Some(PrivateKey::new(single_priv.xkey.private_key, NETWORK)) - } else { - None - } - }); - - let secp = Secp256k1::new(); - - let compressed_priv = private_key_option.unwrap(); - let pubkey = compressed_priv.public_key(&secp); - let xonly_pubkey = pubkey.to_x_only_pubkey(); - - let address = Address::p2tr(&secp, xonly_pubkey, None, NETWORK).to_string(); - - let message = "HELLO WORLD".to_string(); - let private_key_wif = private_key_option.unwrap().to_wif(); - - let signer = Signer::new( - private_key_wif, - message.clone(), - address.clone(), - SignatureFormat::Full, - ); - let signature = signer.sign().unwrap(); - - let verifier = Verifier::new(address, signature, message, SignatureFormat::Full, None); - let verify = verifier.verify().unwrap(); - - assert!(verify); - - Ok(()) -} diff --git a/examples/sign_verify_legacy.rs b/examples/sign_verify_legacy.rs deleted file mode 100644 index 29554ba..0000000 --- a/examples/sign_verify_legacy.rs +++ /dev/null @@ -1,72 +0,0 @@ -use anyhow::Ok; -use bdk_bip322::{SignatureFormat, Signer, Verifier}; -use bdk_wallet::{keys::DescriptorSecretKey, rusqlite::Connection, KeychainKind, Wallet}; -use bitcoin::{key::Secp256k1, Address, Network, PrivateKey}; - -const DB_PATH: &str = "bdk-example-esplora-async.sqlite"; -const NETWORK: Network = Network::Signet; -const EXTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)"; -const INTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)"; - -#[tokio::main] -async fn main() -> Result<(), anyhow::Error> { - let mut conn = Connection::open(DB_PATH)?; - - let wallet_opt = Wallet::load() - .descriptor(KeychainKind::External, Some(EXTERNAL_DESC)) - .descriptor(KeychainKind::Internal, Some(INTERNAL_DESC)) - .extract_keys() - .check_network(NETWORK) - .load_wallet(&mut conn)?; - let mut wallet = match wallet_opt { - Some(wallet) => wallet, - None => Wallet::create(EXTERNAL_DESC, INTERNAL_DESC) - .network(NETWORK) - .create_wallet(&mut conn)?, - }; - - wallet.persist(&mut conn)?; - - let private_key_option: Option = wallet - .get_signers(KeychainKind::External) - .signers() - .iter() - .filter_map(|signer| signer.descriptor_secret_key()) - .find_map(|descriptor_secret| { - if let DescriptorSecretKey::XPrv(single_priv) = descriptor_secret { - Some(PrivateKey::new(single_priv.xkey.private_key, NETWORK)) - } else { - None - } - }); - - let secp = Secp256k1::new(); - - let compressed_priv = private_key_option.unwrap(); - let pubkey = compressed_priv.public_key(&secp); - let address = Address::p2pkh(pubkey, NETWORK).to_string(); - - let message = "HELLO WORLD".to_string(); - let private_key_wif = private_key_option.unwrap().to_wif(); - - let signer = Signer::new( - private_key_wif.clone(), - message.clone(), - address.clone(), - SignatureFormat::Legacy, - ); - let signature = signer.sign().unwrap(); - - let verifier = Verifier::new( - address, - signature, - message, - SignatureFormat::Legacy, - Some(private_key_wif), - ); - let verify = verifier.verify().unwrap(); - - assert!(verify); - - Ok(()) -} diff --git a/examples/sign_verify_simple.rs b/examples/sign_verify_simple.rs deleted file mode 100644 index 252569c..0000000 --- a/examples/sign_verify_simple.rs +++ /dev/null @@ -1,66 +0,0 @@ -use anyhow::Ok; -use bdk_bip322::{SignatureFormat, Signer, Verifier}; -use bdk_wallet::{keys::DescriptorSecretKey, rusqlite::Connection, KeychainKind, Wallet}; -use bitcoin::{key::Secp256k1, Address, Network, PrivateKey}; - -const DB_PATH: &str = "bdk-example-esplora-async.sqlite"; -const NETWORK: Network = Network::Signet; -const EXTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)"; -const INTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)"; - -#[tokio::main] -async fn main() -> Result<(), anyhow::Error> { - let mut conn = Connection::open(DB_PATH)?; - - let wallet_opt = Wallet::load() - .descriptor(KeychainKind::External, Some(EXTERNAL_DESC)) - .descriptor(KeychainKind::Internal, Some(INTERNAL_DESC)) - .extract_keys() - .check_network(NETWORK) - .load_wallet(&mut conn)?; - let mut wallet = match wallet_opt { - Some(wallet) => wallet, - None => Wallet::create(EXTERNAL_DESC, INTERNAL_DESC) - .network(NETWORK) - .create_wallet(&mut conn)?, - }; - - wallet.persist(&mut conn)?; - - let private_key_option: Option = wallet - .get_signers(KeychainKind::External) - .signers() - .iter() - .filter_map(|signer| signer.descriptor_secret_key()) - .find_map(|descriptor_secret| { - if let DescriptorSecretKey::XPrv(single_priv) = descriptor_secret { - Some(PrivateKey::new(single_priv.xkey.private_key, NETWORK)) - } else { - None - } - }); - - let secp = Secp256k1::new(); - - let compressed_priv = private_key_option.unwrap(); - let pubkey = compressed_priv.public_key(&secp); - let address = Address::p2wpkh(&pubkey.try_into().unwrap(), NETWORK).to_string(); - - let message = "HELLO WORLD".to_string(); - let private_key_wif = private_key_option.unwrap().to_wif(); - - let signer = Signer::new( - private_key_wif, - message.clone(), - address.clone(), - SignatureFormat::Simple, - ); - let signature = signer.sign().unwrap(); - - let verifier = Verifier::new(address, signature, message, SignatureFormat::Simple, None); - let verify = verifier.verify().unwrap(); - - assert!(verify); - - Ok(()) -} diff --git a/src/error.rs b/src/error.rs index 84e3be1..3920379 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,7 +1,14 @@ //! Error types for BIP‑322 operations. - -/// All possible errors that can occur when signing or verifying a BIP‑322 message. -use alloc::string::String; +//! +//! All possible errors that can occur when signing or verifying a BIP‑322 message. +use alloc::{boxed::Box, string::String}; +use bdk_wallet::signer::SignerError; +use bitcoin::{ + OutPoint, Txid, + consensus::encode::Error as ConsensusError, + io::Error as IoError, + psbt::{Error as PsbtError, ExtractTxError}, +}; use core::fmt; /// Error types for BIP322 message signing and verification operations. @@ -10,12 +17,6 @@ use core::fmt; /// message signing or verification process. #[derive(Debug)] pub enum Error { - /// Error encountered when extracting data, such as from a PSBT - ExtractionError(String), - /// The provided private key is invalid - InvalidPrivateKey, - /// The provided Bitcoin address is invalid - InvalidAddress, /// The format of the data is invalid for the given context InvalidFormat(String), /// The message does not meet requirements @@ -26,12 +27,8 @@ pub enum Error { InvalidPublicKey(String), /// Unable to compute the signature hash for signing SighashError, - /// Error encountered when decoding Base64 data - Base64DecodeError, /// The digital signature is invalid InvalidSignature(String), - /// Error encountered when decoding Bitcoin consensus data - DecodeError(String), /// The address is not a Segwit address NotSegwitAddress, /// The Segwit version is not supported for the given context @@ -40,29 +37,73 @@ pub enum Error { InvalidSighashType, /// The transaction witness data is invalid InvalidWitness(String), + /// Signer Error + SignerError(SignerError), + /// Bitcoin IoError + IoError(IoError), + /// ExtractTxError + ExtractTxError(Box), + /// PsbtError + PsbtError(PsbtError), + /// ConsensusError + ConsensusError(ConsensusError), + /// Transaction not found in wallet + TransactionNotFound(Txid), + /// UTXO not found in wallet + UtxoNotFound(OutPoint), } impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - Self::ExtractionError(e) => write!(f, "Unable to extract {}", e), - Self::InvalidPrivateKey => write!(f, "Invalid private key"), - Self::InvalidAddress => write!(f, "Invalid address"), Self::InvalidFormat(e) => write!(f, "Only valid for {} format", e), Self::InvalidMessage => write!(f, "Message hash is not secure"), Self::UnsupportedType => write!(f, "Type is not supported"), Self::InvalidPublicKey(e) => write!(f, "Invalid public key {}", e), Self::SighashError => write!(f, "Unable to compute signature hash"), - Self::Base64DecodeError => write!(f, "Base64 decoding failed"), Self::InvalidSignature(e) => write!(f, "Invalid Signature - {}", e), - Self::DecodeError(e) => write!(f, "Consensus decode error - {}", e), Self::NotSegwitAddress => write!(f, "Not a Segwit address"), Self::UnsupportedSegwitVersion(e) => write!(f, "Only Segwit {} is supported", e), Self::InvalidSighashType => write!(f, "Sighash type is invalid"), Self::InvalidWitness(e) => write!(f, "Invalid Witness - {}", e), + Self::SignerError(err) => write!(f, "Signer error: {}", err), + Self::IoError(err) => write!(f, "Bitcoin IO Error: {}", err), + Self::ExtractTxError(err) => write!(f, "Extract TX Error: {:?}", err), + Self::PsbtError(err) => write!(f, "Psbt Error: {:?}", err), + Self::ConsensusError(err) => write!(f, "Consensus Error: {:?}", err), + Self::TransactionNotFound(err) => write!(f, "Transaction: {:?} not found", err), + Self::UtxoNotFound(err) => write!(f, "UTXO: {:?} not found", err), } } } +impl From for Error { + fn from(err: SignerError) -> Self { + Error::SignerError(err) + } +} + +impl From for Error { + fn from(err: IoError) -> Self { + Error::IoError(err) + } +} +impl From for Error { + fn from(err: ExtractTxError) -> Self { + Error::ExtractTxError(Box::new(err)) + } +} + +impl From for Error { + fn from(err: PsbtError) -> Self { + Error::PsbtError(err) + } +} +impl From for Error { + fn from(err: ConsensusError) -> Self { + Error::ConsensusError(err) + } +} + #[cfg(feature = "std")] impl std::error::Error for Error {} diff --git a/src/lib.rs b/src/lib.rs index a8038b8..07a332e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,10 +1,9 @@ -//! A no_std Rust library implementing BIP‑322 Generic Signed Message Format. +//! A no_std Rust library implementing [BIP‑322: Generic Signed Message Format](https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki). //! //! This crate provides: //! - Construction of virtual `to_spend` and `to_sign` transactions //! - Signing and verification for Legacy, Simple, and Full BIP‑322 formats //! - Optional “proof of funds” support via additional UTXO inputs -#![warn(missing_docs)] #![no_std] #[macro_use] @@ -14,11 +13,135 @@ pub extern crate alloc; extern crate std; pub mod error; -pub mod signer; +pub mod sign; pub mod utils; -pub mod verifier; +pub mod verify; pub use error::*; -pub use signer::*; +#[allow(unused_imports)] +pub use sign::*; pub use utils::*; -pub use verifier::*; +pub use verify::*; + +use crate::Error; +use alloc::{string::String, vec::Vec}; +use bitcoin::{Address, Amount, OutPoint, Psbt}; + +/// Represents the different formats supported by the BIP322 message signing protocol. +/// +/// BIP322 defines multiple formats for signatures to accommodate different use cases +/// and maintain backward compatibility with legacy signing methods. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum SignatureFormat { + /// Legacy Bitcoin Core message signing format (P2PKH only). + Legacy, + /// A simplified version of the BIP322 format that includes only essential data. + Simple, + /// Full BIP-322 format with complete transaction data. + Full, + /// The Full BIP322 format with Proof-of-funds(utxo) capabiility. + FullWithProofOfFunds, +} + +/// Main trait providing BIP-322 signing and verification functionality. +/// +/// This trait is implemented for `bdk_wallet::Wallet` to provide seamless +/// integration with BDK wallets. +/// +/// # Examples +/// +/// ```no_run +/// use bdk_wallet::{Wallet, KeychainKind}; +/// use bdk_bip322::{BIP322, SignatureFormat}; +/// +/// # fn main() -> Result<(), bdk_bip322::error::Error> { +/// # let mut wallet: Wallet = unimplemented!(); +/// let address = wallet.peek_address(KeychainKind::External, 0).address; +/// +/// // Sign a message +/// let proof = wallet.sign_bip322( +/// "Hello Bitcoin", +/// SignatureFormat::Simple, +/// &address, +/// None, +/// )?; +/// +/// // Verify the signature +/// let result = wallet.verify_bip322( +/// &proof, +/// "Hello Bitcoin", +/// SignatureFormat::Simple, +/// &address, +/// )?; +/// +/// assert!(result.valid); +/// # Ok(()) +/// # } +/// ``` +pub trait BIP322 { + /// Sign a message for a specific address using BIP-322. + /// + /// # Arguments + /// + /// * `message` - The message to sign (as UTF-8 text) + /// * `sig_type` - The signature format to use + /// * `address` - The address to sign with (must be owned by wallet) + /// * `utxos` - Optional list of specific UTXOs for proof-of-funds (only for `FullWithProofOfFunds`) + /// + /// # Returns + /// + /// Returns either a complete signature or a PSBT for external signing or [`Error`] when there's an error + fn sign_bip322( + &mut self, + message: &str, + sig_type: SignatureFormat, + address: &Address, + utxos: Option>, + ) -> Result; + + /// Verify a BIP-322 message signature. + /// + /// # Arguments + /// + /// * `proof` - The signature proof to verify + /// * `message` - The original message that was signed + /// * `sig_type` - The signature format used + /// * `address` - The address that supposedly signed the message + /// + /// # Returns + /// + /// Returns verification result with validity and optional proven amount or [`Error`] when there's an error + fn verify_bip322( + &mut self, + proof: &Bip322Proof, + message: &str, + sig_type: SignatureFormat, + address: &Address, + ) -> Result; +} + +/// Result of a BIP-322 signature verification. +pub struct Bip322VerificationResult { + /// Whether the signature is valid for the given message and address + pub valid: bool, + /// The total amount proven for FullWithProofOfFunds signatures. + /// + /// This is `Some` only when using `FullWithProofOfFunds` format and + /// additional UTXOs were included in the signature. For other formats, + /// this will always be `None`. + pub proven_amount: Option, +} + +/// Result of a BIP-322 signing operation. +/// +/// Signing can result in either a complete signature (when the wallet has +/// private keys) or a PSBT ready for external signing (e.g., hardware wallets). +#[derive(Debug)] +pub enum Bip322Proof { + /// Signature was created successfully. + /// + /// Contains the base64-encoded signature string ready for sharing. + Signed(String), + /// PSBT ready for external signing. + Psbt(Psbt), +} diff --git a/src/sign.rs b/src/sign.rs new file mode 100644 index 0000000..26ccd14 --- /dev/null +++ b/src/sign.rs @@ -0,0 +1,525 @@ +//! The signature generation implementation for BIP-322 for message signing +//! according to the BIP-322 standard. + +use crate::{ + Bip322Proof, Bip322VerificationResult, SignatureFormat, validate_witness, verify_psbt_proof, + verify_signed_proof, +}; +use alloc::{string::ToString, vec::Vec}; + +use bdk_wallet::SignOptions; +use bdk_wallet::{KeychainKind, Wallet}; +use bitcoin::{ + Address, EcdsaSighashType, OutPoint, Psbt, ScriptBuf, Sequence, TapSighashType, Transaction, + TxIn, TxOut, Witness, + base64::{Engine, engine::general_purpose}, + consensus::Encodable, + psbt::PsbtSighashType, +}; + +use crate::{BIP322, Error, to_sign, to_spend}; + +impl BIP322 for Wallet { + fn sign_bip322( + &mut self, + message: &str, + sig_type: SignatureFormat, + address: &Address, + utxos: Option>, + ) -> Result { + let script_pubkey = address.script_pubkey(); + + // Create the virtual to_spend and to_sign transactions + let to_spend = to_spend(&script_pubkey, message); + let mut to_sign = to_sign(&to_spend)?; + + // Handle proof-of-funds by adding additional inputs + if sig_type == SignatureFormat::FullWithProofOfFunds { + add_proof_of_funds_inputs(&mut to_sign, self, &script_pubkey, utxos)?; + } else if utxos.is_some() { + return Err(Error::InvalidFormat( + "UTXOs parameter only supported for FullWithProofOfFunds format".to_string(), + )); + } + + let mut psbt = Psbt::from_unsigned_tx(to_sign)?; + + configure_psbt_inputs(&mut psbt, self, &script_pubkey, &to_spend)?; + + let sign_options = SignOptions { + trust_witness_utxo: true, + ..Default::default() + }; + + let finalized = self.sign(&mut psbt, sign_options)?; + + if finalized { + encode_signature(&psbt, sig_type, &script_pubkey) + } else { + Ok(Bip322Proof::Psbt(psbt)) + } + } + + fn verify_bip322( + &mut self, + proof: &Bip322Proof, + message: &str, + sig_type: SignatureFormat, + address: &Address, + ) -> Result { + let script_pubkey = address.script_pubkey(); + + match proof { + Bip322Proof::Signed(tx) => { + verify_signed_proof(self, message, sig_type, address, &script_pubkey, tx) + } + Bip322Proof::Psbt(psbt) => verify_psbt_proof(psbt, script_pubkey), + } + } +} + +/// Adds proof-of-funds inputs to the to_sign transaction. +/// +/// Collects UTXOs belonging to the signing address and adds them as +/// additional inputs to prove control over funds. +fn add_proof_of_funds_inputs( + to_sign: &mut Transaction, + wallet: &Wallet, + script_pubkey: &ScriptBuf, + utxos: Option>, +) -> Result<(), Error> { + let address_utxos: Vec<(OutPoint, TxOut)> = if let Some(specific_utxos) = utxos { + wallet + .list_unspent() + .filter(|utxo| { + specific_utxos.contains(&utxo.outpoint) + && utxo.txout.script_pubkey == *script_pubkey + }) + .map(|utxo| (utxo.outpoint, utxo.txout)) + .collect() + } else { + wallet + .list_unspent() + .filter(|utxo| utxo.txout.script_pubkey == *script_pubkey) + .map(|utxo| (utxo.outpoint, utxo.txout)) + .collect() + }; + + if address_utxos.is_empty() { + return Err(Error::InvalidFormat( + "No UTXOs available for proof-of-funds".to_string(), + )); + } + + // Add each UTXO as an input + for (outpoint, _) in &address_utxos { + to_sign.input.push(TxIn { + previous_output: *outpoint, + script_sig: ScriptBuf::new(), + sequence: Sequence::ZERO, + witness: Witness::new(), + }); + } + + Ok(()) +} + +/// Configures PSBT inputs with necessary witness/non-witness UTXO data. +/// +/// Sets up each PSBT input with the correct sighash type and UTXO information +/// based on the address type (SegWit vs legacy). +fn configure_psbt_inputs( + psbt: &mut Psbt, + wallet: &Wallet, + script_pubkey: &ScriptBuf, + to_spend: &Transaction, +) -> Result<(), Error> { + for (i, (psbt_input, tx_input)) in psbt + .inputs + .iter_mut() + .zip(psbt.unsigned_tx.input.iter()) + .enumerate() + { + // Set appropriate sighash type + psbt_input.sighash_type = if script_pubkey.is_p2tr() { + Some(PsbtSighashType::from(TapSighashType::All)) + } else { + Some(PsbtSighashType::from(EcdsaSighashType::All)) + }; + + if i == 0 { + if script_pubkey.is_p2tr() || script_pubkey.is_p2wpkh() || script_pubkey.is_p2wsh() { + psbt_input.witness_utxo = Some(to_spend.output[0].clone()); + + if script_pubkey.is_p2wsh() { + // Add witness script for P2WSH + let external_desc = wallet.public_descriptor(KeychainKind::External); + + if let Ok(derived_desc) = external_desc.at_derivation_index(0) { + let script = derived_desc.script_pubkey(); + psbt_input.witness_script = Some(script); + } + } + } else { + // Legacy P2PKH requires full transaction + psbt_input.non_witness_utxo = Some(to_spend.clone()) + } + } else { + let utxo = wallet + .get_utxo(tx_input.previous_output) + .ok_or(Error::UtxoNotFound(tx_input.previous_output))?; + + let txout = utxo.txout; + + if txout.script_pubkey.is_p2tr() + || txout.script_pubkey.is_p2wpkh() + || txout.script_pubkey.is_p2wsh() + { + psbt_input.witness_utxo = Some(txout.clone()); + + if txout.script_pubkey.is_p2wsh() { + let external_desc = wallet.public_descriptor(KeychainKind::External); + if let Ok(derived_desc) = external_desc.at_derivation_index(0) { + let script = derived_desc.explicit_script().unwrap(); + psbt_input.witness_script = Some(script); + } + } + } else { + // Legacy input requires full transaction + let tx = wallet + .get_tx(tx_input.previous_output.txid) + .ok_or(Error::TransactionNotFound(tx_input.previous_output.txid))?; + psbt_input.non_witness_utxo = Some(tx.tx_node.tx.as_ref().clone()); + } + } + } + + Ok(()) +} + +/// Encodes the finalized signature according to the signature format. +/// +/// Extracts the appropriate data from the signed PSBT and encodes it +/// as a base64 string. +fn encode_signature( + psbt: &Psbt, + sig_type: SignatureFormat, + script_pubkey: &ScriptBuf, +) -> Result { + let mut buffer = Vec::new(); + + match sig_type { + SignatureFormat::Legacy => { + if !script_pubkey.is_p2pkh() { + return Err(Error::InvalidFormat( + "Legacy format only supported for P2PKH addresses".to_string(), + )); + } + let script_sig = + psbt.inputs[0] + .final_script_sig + .as_ref() + .ok_or(Error::InvalidFormat( + "No final script_sig found".to_string(), + ))?; + + if script_sig.is_empty() { + return Err(Error::InvalidFormat("Empty script_sig".to_string())); + } + + let legacy_signature = general_purpose::STANDARD.encode(script_sig.as_bytes()); + Ok(Bip322Proof::Signed(legacy_signature)) + } + SignatureFormat::Simple => { + let witness = psbt.inputs[0] + .final_script_witness + .as_ref() + .ok_or(Error::InvalidFormat("No final witness found".to_string()))?; + + validate_witness(witness, script_pubkey)?; + + witness.consensus_encode(&mut buffer)?; + let simple_signature = general_purpose::STANDARD.encode(&buffer); + Ok(Bip322Proof::Signed(simple_signature)) + } + SignatureFormat::Full | SignatureFormat::FullWithProofOfFunds => { + let tx = psbt.clone().extract_tx()?; + + tx.consensus_encode(&mut buffer)?; + let full_signature = general_purpose::STANDARD.encode(&buffer); + Ok(Bip322Proof::Signed(full_signature)) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use bdk_wallet::{ + KeychainKind, + test_utils::{get_funded_wallet, get_funded_wallet_single}, + }; + use bitcoin::Amount; + + #[test] + fn test_legacy_format() { + const EXTERNAL_DESC: &str = "pkh(tprv8ZgxMBicQKsPfGXKjYNsw4gayjfBsq6FHxvNZ8LSBdz4DSTeBPd7cjvVQXTdMH9NJBVwNrNKLDr58dcrf4YmWLYBs4KogJhSgUELXuo1JwH/44'/1'/0'/0/*)"; + const INTERNAL_DESC: &str = "pkh(tprv8ZgxMBicQKsPfGXKjYNsw4gayjfBsq6FHxvNZ8LSBdz4DSTeBPd7cjvVQXTdMH9NJBVwNrNKLDr58dcrf4YmWLYBs4KogJhSgUELXuo1JwH/44'/1'/0'/1/*)"; + + let (mut wallet, _) = get_funded_wallet(EXTERNAL_DESC, INTERNAL_DESC); + let address = wallet.peek_address(KeychainKind::External, 0).address; + + let sig_type = SignatureFormat::Legacy; + + let sign = wallet + .sign_bip322("HELLO WORLD", sig_type, &address, None) + .unwrap(); + + let verify = wallet + .verify_bip322(&sign, "HELLO WORLD", sig_type, &address) + .unwrap(); + + assert!(verify.valid) + } + + #[test] + fn test_simple_format_p2pwkh() { + const EXTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)"; + const INTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)"; + + let (mut wallet, _) = get_funded_wallet(EXTERNAL_DESC, INTERNAL_DESC); + let address = wallet.peek_address(KeychainKind::External, 0).address; + + let sig_type = SignatureFormat::Simple; + + let sign = wallet + .sign_bip322("HELLO WORLD", sig_type, &address, None) + .unwrap(); + + let verify = wallet + .verify_bip322(&sign, "HELLO WORLD", sig_type, &address) + .unwrap(); + + assert!(verify.valid) + } + + #[test] + fn test_simple_format_p2tr() { + const EXTERNAL_DESC: &str = "tr(tprv8ZgxMBicQKsPd3krDUsBAmtnRsK3rb8u5yi1zhQgMhF1tR8MW7xfE4rnrbbsrbPR52e7rKapu6ztw1jXveJSCGHEriUGZV7mCe88duLp5pj/86'/1'/0'/0/*)"; + const INTERNAL_DESC: &str = "tr(tprv8ZgxMBicQKsPd3krDUsBAmtnRsK3rb8u5yi1zhQgMhF1tR8MW7xfE4rnrbbsrbPR52e7rKapu6ztw1jXveJSCGHEriUGZV7mCe88duLp5pj/86'/1'/0'/1/*)"; + + let (mut wallet, _) = get_funded_wallet(EXTERNAL_DESC, INTERNAL_DESC); + let address = wallet.peek_address(KeychainKind::External, 0).address; + + let sig_type = SignatureFormat::Simple; + + let sign = wallet + .sign_bip322("HELLO WORLD", sig_type, &address, None) + .unwrap(); + + let verify = wallet + .verify_bip322(&sign, "HELLO WORLD", sig_type, &address) + .unwrap(); + + assert!(verify.valid) + } + + #[test] + fn test_simple_format_p2wsh_single_script() { + let (mut wallet, _) = get_funded_wallet_single( + "wsh(and_v(v:pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW),older(6)))", + ); + let address = wallet.peek_address(KeychainKind::External, 0).address; + + let sig_type = SignatureFormat::Simple; + + let sign = wallet + .sign_bip322("HELLO WORLD", sig_type, &address, None) + .unwrap(); + + let verify = wallet + .verify_bip322(&sign, "HELLO WORLD", sig_type, &address) + .unwrap(); + + assert!(verify.valid) + } + + #[test] + fn test_full_format_p2pwkh() { + const EXTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)"; + const INTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)"; + + let (mut wallet, _) = get_funded_wallet(EXTERNAL_DESC, INTERNAL_DESC); + let address = wallet.peek_address(KeychainKind::External, 0).address; + + let sig_type = SignatureFormat::Full; + + let sign = wallet + .sign_bip322("HELLO WORLD", sig_type, &address, None) + .unwrap(); + + let verify = wallet + .verify_bip322(&sign, "HELLO WORLD", sig_type, &address) + .unwrap(); + + assert!(verify.valid) + } + + #[test] + fn test_full_format_p2tr() { + const EXTERNAL_DESC: &str = "tr(tprv8ZgxMBicQKsPd3krDUsBAmtnRsK3rb8u5yi1zhQgMhF1tR8MW7xfE4rnrbbsrbPR52e7rKapu6ztw1jXveJSCGHEriUGZV7mCe88duLp5pj/86'/1'/0'/0/*)"; + const INTERNAL_DESC: &str = "tr(tprv8ZgxMBicQKsPd3krDUsBAmtnRsK3rb8u5yi1zhQgMhF1tR8MW7xfE4rnrbbsrbPR52e7rKapu6ztw1jXveJSCGHEriUGZV7mCe88duLp5pj/86'/1'/0'/1/*)"; + + let (mut wallet, _) = get_funded_wallet(EXTERNAL_DESC, INTERNAL_DESC); + let address = wallet.peek_address(KeychainKind::External, 0).address; + + let sig_type = SignatureFormat::Full; + + let sign = wallet + .sign_bip322("HELLO WORLD", sig_type, &address, None) + .unwrap(); + + let verify = wallet + .verify_bip322(&sign, "HELLO WORLD", sig_type, &address) + .unwrap(); + + assert!(verify.valid) + } + + #[test] + fn test_full_format_p2wsh_single_script() { + let (mut wallet, _) = get_funded_wallet_single( + "wsh(and_v(v:pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW),older(6)))", + ); + let address = wallet.peek_address(KeychainKind::External, 0).address; + + let sig_type = SignatureFormat::Full; + + let sign = wallet + .sign_bip322("HELLO WORLD", sig_type, &address, None) + .unwrap(); + + let verify = wallet + .verify_bip322(&sign, "HELLO WORLD", sig_type, &address) + .unwrap(); + + assert!(verify.valid) + } + + #[test] + fn test_full_with_proof_of_funds_format_p2pwkh() { + const EXTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)"; + const INTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)"; + + let (mut wallet, _) = get_funded_wallet(EXTERNAL_DESC, INTERNAL_DESC); + let address = wallet.peek_address(KeychainKind::External, 0).address; + + let utxos: Vec<_> = wallet + .list_unspent() + .filter(|utxo| utxo.txout.script_pubkey == address.script_pubkey()) + .map(|utxo| utxo.outpoint) + .collect(); + + assert!(!utxos.is_empty(), "No UTXOs found for address"); + + let sig_type = SignatureFormat::FullWithProofOfFunds; + + let sign = wallet + .sign_bip322("HELLO WORLD", sig_type, &address, Some(utxos)) + .unwrap(); + + let verify = wallet + .verify_bip322(&sign, "HELLO WORLD", sig_type, &address) + .unwrap(); + + assert!(verify.valid) + } + + #[test] + fn test_full_with_proof_of_funds_format_p2tr() { + const EXTERNAL_DESC: &str = "tr(tprv8ZgxMBicQKsPd3krDUsBAmtnRsK3rb8u5yi1zhQgMhF1tR8MW7xfE4rnrbbsrbPR52e7rKapu6ztw1jXveJSCGHEriUGZV7mCe88duLp5pj/86'/1'/0'/0/*)"; + const INTERNAL_DESC: &str = "tr(tprv8ZgxMBicQKsPd3krDUsBAmtnRsK3rb8u5yi1zhQgMhF1tR8MW7xfE4rnrbbsrbPR52e7rKapu6ztw1jXveJSCGHEriUGZV7mCe88duLp5pj/86'/1'/0'/1/*)"; + + let (mut wallet, _) = get_funded_wallet(EXTERNAL_DESC, INTERNAL_DESC); + let address = wallet.peek_address(KeychainKind::External, 0).address; + + let utxos: Vec<_> = wallet + .list_unspent() + .filter(|utxo| utxo.txout.script_pubkey == address.script_pubkey()) + .map(|utxo| utxo.outpoint) + .collect(); + + assert!(!utxos.is_empty(), "No UTXOs found for address"); + + let sig_type = SignatureFormat::FullWithProofOfFunds; + + let sign = wallet + .sign_bip322("HELLO WORLD", sig_type, &address, Some(utxos)) + .unwrap(); + + let verify = wallet + .verify_bip322(&sign, "HELLO WORLD", sig_type, &address) + .unwrap(); + + assert!(verify.valid) + } + + #[test] + fn test_full_with_proof_of_funds_format_p2wsh_single_script() { + const DESCRIPTOR: &str = "wsh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))"; + + let (mut wallet, _) = get_funded_wallet_single(DESCRIPTOR); + let address = wallet.peek_address(KeychainKind::External, 0).address; + + let utxos: Vec<_> = wallet + .list_unspent() + .filter(|utxo| utxo.txout.script_pubkey == address.script_pubkey()) + .map(|utxo| utxo.outpoint) + .collect(); + + assert!(!utxos.is_empty(), "No UTXOs found for address"); + + let sig_type = SignatureFormat::FullWithProofOfFunds; + + let sign = wallet + .sign_bip322("HELLO WORLD", sig_type, &address, Some(utxos)) + .unwrap(); + + let verify = wallet + .verify_bip322(&sign, "HELLO WORLD", sig_type, &address) + .unwrap(); + + assert!(verify.valid) + } + + #[test] + fn test_full_with_proof_of_funds_psbt() { + const DESCRIPTOR: &str = + "wsh(and_v(v:pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW),older(6)))"; + + let (mut wallet, _) = get_funded_wallet_single(DESCRIPTOR); + let address = wallet.peek_address(KeychainKind::External, 0).address; + + let utxos: Vec<_> = wallet + .list_unspent() + .filter(|utxo| utxo.txout.script_pubkey == address.script_pubkey()) + .map(|utxo| utxo.outpoint) + .collect(); + + assert!(!utxos.is_empty(), "No UTXOs found for address"); + + let sig_type = SignatureFormat::FullWithProofOfFunds; + + let sign = wallet + .sign_bip322("HELLO WORLD", sig_type, &address, Some(utxos)) + .unwrap(); + + let verify = wallet + .verify_bip322(&sign, "HELLO WORLD", sig_type, &address) + .unwrap(); + + assert!(verify.valid); + assert_eq!(verify.proven_amount.unwrap(), Amount::from_sat(50000)); + assert_ne!(verify.proven_amount.unwrap(), Amount::from_sat(0)) + } +} diff --git a/src/signer.rs b/src/signer.rs deleted file mode 100644 index ecec46a..0000000 --- a/src/signer.rs +++ /dev/null @@ -1,308 +0,0 @@ -//! The signature generation implementation for BIP-322 for message signing -//! according to the BIP-322 standard. - -use alloc::{ - str::FromStr, - string::{String, ToString}, - vec::Vec, -}; -use bitcoin::{ - base64::{prelude::BASE64_STANDARD, Engine}, - consensus::serialize, - key::{Keypair, TapTweak}, - secp256k1::{ecdsa::Signature, Message}, - sighash::{self, SighashCache}, - sign_message::signed_msg_hash, - Address, Amount, EcdsaSighashType, PrivateKey, Psbt, PublicKey, ScriptBuf, TapSighashType, - Transaction, TxOut, Witness, -}; - -use crate::{to_sign, to_spend, Error, SecpCtx, SignatureFormat}; - -/// Signer encapsulates all the data and functionality required to sign a message -/// according to the BIP322 specification. It supports multiple signature formats: -/// - **Legacy:** Produces a standard ECDSA signature for P2PKH addresses. -/// - **Simple:** Creates a simplified signature that encodes witness data. -/// - **Full:** Constructs a complete transaction with witness details and signs it. -/// -/// # Fields -/// - `private_key_str`: A WIF-encoded private key as a `String`. -/// - `message`: The message to be signed. -/// - `address_str`: The Bitcoin address associated with the signing process. -/// - `signature_type`: The signature format to use, defined by `SignatureFormat`. -pub struct Signer { - private_key_str: String, - message: String, - address_str: String, - signature_type: SignatureFormat, -} - -impl Signer { - /// Creates a new instance of `Signer` with the specified parameters. - /// - /// # Arguments - /// - `private_key_str`: A WIF-encoded private key as a `String`. - /// - `message`: The message to be signed. - /// - `address`: The Bitcoin address for which the signature is intended. - /// - `signature_type`: The BIP322 signature format to be used. Can be one of Legacy, Simple, or Full. - /// - /// # Returns - /// An instance of `Signer`. - /// - /// # Example - /// ``` - /// # use bdk_bip322::{Signer, SignatureFormat}; - /// - /// let signer = Signer::new( - /// "c...".to_string(), - /// "Hello, Bitcoin!".to_string(), - /// "1BitcoinAddress...".to_string(), - /// SignatureFormat::Legacy, - /// ); - /// ``` - pub fn new( - private_key_str: String, - message: String, - address_str: String, - signature_type: SignatureFormat, - ) -> Self { - Self { - private_key_str, - message, - address_str, - signature_type, - } - } - - /// Signs a message using a provided private key, message, and address with a specified - /// BIP322 format (Legacy, Simple, or Full). - /// - /// - **Legacy:** Generates a traditional ECDSA signature for P2PKH addresses. - /// - **Simple:** Constructs a simplified signature by signing the message and encoding - /// the witness data. - /// - **Full:** Creates a comprehensive signature by building and signing an entire - /// transaction, including all witness details. - /// - /// The function extracts the necessary key and script information from the input, - /// processes any optional proof of funds, and returns the resulting signature as a - /// Base64-encoded string. - /// - /// # Errors - /// Returns a `BIP322Error` if any signing steps fail. - pub fn sign(&self) -> Result { - let secp = SecpCtx::new(); - let private_key = - PrivateKey::from_wif(&self.private_key_str).map_err(|_| Error::InvalidPrivateKey)?; - let pubkey = private_key.public_key(&secp); - - let script_pubkey = Address::from_str(&self.address_str) - .map_err(|_| Error::InvalidAddress)? - .assume_checked() - .script_pubkey(); - - match &self.signature_type { - SignatureFormat::Legacy => { - if !script_pubkey.is_p2pkh() { - return Err(Error::InvalidFormat("legacy".to_string())); - } - - let sig_serialized = self.sign_legacy(&private_key)?; - Ok(BASE64_STANDARD.encode(sig_serialized)) - } - SignatureFormat::Simple => { - let witness = self.sign_message(&private_key, pubkey, &script_pubkey)?; - - Ok(BASE64_STANDARD.encode(serialize(&witness.input[0].witness.clone()))) - } - SignatureFormat::Full => { - let transaction = self.sign_message(&private_key, pubkey, &script_pubkey)?; - - Ok(BASE64_STANDARD.encode(serialize(&transaction))) - } - } - } - - /// Constructs a transaction that includes a signature for the provided message - /// according to the BIP322 message signing protocol. - /// - /// This function builds the transaction to be signed by selecting the appropriate - /// signing method based on the script type: - /// - P2WPKH - /// - P2TR - /// - P2SH - /// - /// Optionally, if a proof of funds is provided, additional inputs are appended - /// to support advanced verification scenarios. On success, the function returns a - /// complete transaction - /// - /// # Errors - /// Returns a `BIP322Error` if the script type is unsupported or if any part of - /// the signing process fails. - fn sign_message( - &self, - private_key: &PrivateKey, - pubkey: PublicKey, - script_pubkey: &ScriptBuf, - ) -> Result { - let to_spend = to_spend(script_pubkey, &self.message); - let mut to_sign = to_sign( - &to_spend.output[0].script_pubkey, - to_spend.compute_txid(), - to_spend.lock_time, - to_spend.input[0].sequence, - Some(to_spend.input[0].witness.clone()), - )?; - - let mut sighash_cache = SighashCache::new(&to_sign.unsigned_tx); - - let witness = if script_pubkey.is_p2wpkh() { - self.sign_p2sh_p2wpkh(&mut sighash_cache, to_spend, private_key, pubkey, true)? - } else if script_pubkey.is_p2tr() || script_pubkey.is_p2wsh() { - self.sign_p2tr(&mut sighash_cache, to_spend, to_sign.clone(), private_key)? - } else if script_pubkey.is_p2sh() { - self.sign_p2sh_p2wpkh(&mut sighash_cache, to_spend, private_key, pubkey, false)? - } else { - return Err(Error::UnsupportedType); - }; - - to_sign.inputs[0].final_script_witness = Some(witness); - - let transaction = to_sign - .extract_tx() - .map_err(|_| Error::ExtractionError("transaction".to_string()))?; - - Ok(transaction) - } - - fn sign_legacy(&self, private_key: &PrivateKey) -> Result, Error> { - let secp = SecpCtx::new(); - - let message_hash = signed_msg_hash(&self.message); - let message = &Message::from_digest_slice(message_hash.as_ref()) - .map_err(|_| Error::InvalidMessage)?; - - let mut signature: Signature = secp.sign_ecdsa(message, &private_key.inner); - signature.normalize_s(); - let mut sig_serialized = signature.serialize_der().to_vec(); - sig_serialized.push(EcdsaSighashType::All as u8); - - Ok(sig_serialized) - } - - fn sign_p2sh_p2wpkh( - &self, - sighash_cache: &mut SighashCache<&Transaction>, - to_spend: Transaction, - private_key: &PrivateKey, - pubkey: PublicKey, - is_segwit: bool, - ) -> Result { - let secp = SecpCtx::new(); - let sighash_type = EcdsaSighashType::All; - - let wpubkey_hash = &pubkey - .wpubkey_hash() - .map_err(|e| Error::InvalidPublicKey(e.to_string()))?; - - let sighash = sighash_cache - .p2wpkh_signature_hash( - 0, - &if is_segwit { - to_spend.output[0].script_pubkey.clone() - } else { - ScriptBuf::new_p2wpkh(wpubkey_hash) - }, - to_spend.output[0].value, - sighash_type, - ) - .map_err(|_| Error::SighashError)?; - - let msg = - &Message::from_digest_slice(sighash.as_ref()).map_err(|_| Error::InvalidMessage)?; - - let signature = secp.sign_ecdsa(msg, &private_key.inner); - let mut sig_serialized = signature.serialize_der().to_vec(); - sig_serialized.push(sighash_type as u8); - - Ok(Witness::from(vec![ - sig_serialized, - pubkey.inner.serialize().to_vec(), - ])) - } - - fn sign_p2tr( - &self, - sighash_cache: &mut SighashCache<&Transaction>, - to_spend: Transaction, - mut to_sign: Psbt, - private_key: &PrivateKey, - ) -> Result { - let secp = SecpCtx::new(); - let keypair = Keypair::from_secret_key(&secp, &private_key.inner); - let key_pair = keypair - .tap_tweak(&secp, to_sign.inputs[0].tap_merkle_root) - .to_keypair(); - let x_only_public_key = keypair.x_only_public_key().0; - - let sighash_type = TapSighashType::All; - - to_sign.inputs[0].tap_internal_key = Some(x_only_public_key); - - let sighash = sighash_cache - .taproot_key_spend_signature_hash( - 0, - &sighash::Prevouts::All(&[TxOut { - value: Amount::from_sat(0), - script_pubkey: to_spend.output[0].clone().script_pubkey, - }]), - sighash_type, - ) - .map_err(|_| Error::SighashError)?; - - let msg = - &Message::from_digest_slice(sighash.as_ref()).map_err(|_| Error::InvalidMessage)?; - - let signature = secp.sign_schnorr_no_aux_rand(msg, &key_pair); - let mut sig_serialized = signature.serialize().to_vec(); - sig_serialized.push(sighash_type as u8); - - Ok(Witness::from(vec![sig_serialized])) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - const PRIVATE_KEY: &str = "L3VFeEujGtevx9w18HD1fhRbCH67Az2dpCymeRE1SoPK6XQtaN2k"; - const SEGWIT_ADDRESS: &str = "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l"; - const HELLO_WORLD_MESSAGE: &str = "Hello World"; - - #[test] - fn test_sign_with_segwit_address() { - let simple_sign = Signer::new( - PRIVATE_KEY.to_string(), - HELLO_WORLD_MESSAGE.to_string(), - SEGWIT_ADDRESS.to_string(), - SignatureFormat::Simple, - ); - let sign_message = simple_sign.sign().unwrap(); - - let sign_empty_msg = Signer::new( - PRIVATE_KEY.to_string(), - "".to_string(), - SEGWIT_ADDRESS.to_string(), - SignatureFormat::Simple, - ); - let sign_empty_msg_sig = sign_empty_msg.sign().unwrap(); - - assert_eq!( - sign_message, - "AkgwRQIhAOzyynlqt93lOKJr+wmmxIens//zPzl9tqIOua93wO6MAiBi5n5EyAcPScOjf1lAqIUIQtr3zKNeavYabHyR8eGhowEhAsfxIAMZZEKUPYWI4BruhAQjzFT8FSFSajuFwrDL1Yhy" - ); - assert_eq!( - sign_empty_msg_sig, - "AkgwRQIhAPkJ1Q4oYS0htvyuSFHLxRQpFAY56b70UvE7Dxazen0ZAiAtZfFz1S6T6I23MWI2lK/pcNTWncuyL8UL+oMdydVgzAEhAsfxIAMZZEKUPYWI4BruhAQjzFT8FSFSajuFwrDL1Yhy" - ); - } -} diff --git a/src/utils.rs b/src/utils.rs index 14a38a9..9ebe974 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,41 +1,27 @@ //! The utility methods for BIP-322 for message signing //! according to the BIP-322 standard. - use alloc::{string::ToString, vec}; use bitcoin::{ + Amount, OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Witness, absolute::LockTime, - hashes::{sha256, Hash, HashEngine}, - opcodes::{all::OP_RETURN, OP_0}, + hashes::{Hash, HashEngine, sha256}, + opcodes::{OP_0, all::OP_RETURN}, script::Builder, secp256k1::{All, Secp256k1}, transaction::Version, - Amount, OutPoint, Psbt, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Txid, Witness, }; use crate::Error; -/// Represents the different formats supported by the BIP322 message signing protocol. -/// -/// BIP322 defines multiple formats for signatures to accommodate different use cases -/// and maintain backward compatibility with legacy signing methods. -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum SignatureFormat { - /// The legacy Bitcoin message signature format used before BIP322. - Legacy, - /// A simplified version of the BIP322 format that includes only essential data. - Simple, - /// The Full BIP322 format that includes all signature data. - Full, -} - -const TAG: &str = "BIP0322-signed-message"; +/// The tag used for BIP-322 message hashing according to BIP-340 tagged hashes +pub const BIP322_TAG: &str = "BIP0322-signed-message"; /// Creates a tagged hash of a message according to the BIP322 specification. pub fn tagged_message_hash(message: &[u8]) -> sha256::Hash { let mut engine = sha256::Hash::engine(); - let tag_hash = sha256::Hash::hash(TAG.as_bytes()); + let tag_hash = sha256::Hash::hash(BIP322_TAG.as_bytes()); engine.input(&tag_hash[..]); engine.input(&tag_hash[..]); engine.input(message); @@ -43,14 +29,8 @@ pub fn tagged_message_hash(message: &[u8]) -> sha256::Hash { sha256::Hash::from_engine(engine) } -/// Constructs the "to_spend" transaction according to the BIP322 specification. +/// Creates the virtual "to_spend" transaction for BIP-322. pub fn to_spend(script_pubkey: &ScriptBuf, message: &str) -> Transaction { - let txid = Txid::from_slice(&[0u8; 32]).expect("Txid slice error"); - - let outpoint = OutPoint { - txid, - vout: 0xFFFFFFFF, - }; let message_hash = tagged_message_hash(message.as_bytes()); let script_sig = Builder::new() .push_opcode(OP_0) @@ -61,72 +41,80 @@ pub fn to_spend(script_pubkey: &ScriptBuf, message: &str) -> Transaction { version: Version(0), lock_time: LockTime::ZERO, input: vec![TxIn { - previous_output: outpoint, + previous_output: OutPoint::default(), script_sig, sequence: Sequence::ZERO, witness: Witness::new(), }], output: vec![TxOut { - value: Amount::from_sat(0), + value: Amount::ZERO, script_pubkey: script_pubkey.clone(), }], } } -/// Constructs a transaction according to the BIP322 specification. -/// -/// This transaction will be signed to prove ownership of the private key -/// corresponding to the script_pubkey. -/// -/// Returns a PSBT (Partially Signed Bitcoin Transaction) ready for signing -/// or a [`BIP322Error`] if something goes wrong. -pub fn to_sign( - script_pubkey: &ScriptBuf, - txid: Txid, - lock_time: LockTime, - sequence: Sequence, - witness: Option, -) -> Result { - let outpoint = OutPoint { txid, vout: 0x00 }; +/// Creates the virtual "to_sign" transaction for BIP-322. +pub fn to_sign(to_spend: &Transaction) -> Result { + let outpoint = OutPoint { + txid: to_spend.compute_txid(), + vout: 0x00, + }; let script_pub_key = Builder::new().push_opcode(OP_RETURN).into_script(); let tx = Transaction { version: Version(0), - lock_time, + lock_time: LockTime::ZERO, input: vec![TxIn { previous_output: outpoint, - sequence, + sequence: Sequence::ZERO, script_sig: ScriptBuf::new(), witness: Witness::new(), }], output: vec![TxOut { - value: Amount::from_sat(0), + value: Amount::ZERO, script_pubkey: script_pub_key, }], }; + Ok(tx) +} - let mut psbt = - Psbt::from_unsigned_tx(tx).map_err(|_| Error::ExtractionError("psbt".to_string()))?; +/// Secp256k1 context type used throughout the crate +pub(crate) type SecpCtx = Secp256k1; - psbt.inputs[0].witness_utxo = Some(TxOut { - value: Amount::from_sat(0), - script_pubkey: script_pubkey.clone(), - }); +/// Validates witness structure matches the script type. +pub fn validate_witness(witness: &Witness, script_pubkey: &ScriptBuf) -> Result<(), Error> { + if witness.is_empty() { + return Err(Error::InvalidFormat("Empty witness".to_string())); + } - psbt.inputs[0].final_script_witness = witness; + if script_pubkey.is_p2wpkh() && witness.len() != 2 { + return Err(Error::InvalidFormat( + "P2WPKH requires exactly 2 witness elements".to_string(), + )); + } else if script_pubkey.is_p2tr() && witness.is_empty() { + return Err(Error::InvalidFormat( + "P2TR requires at least 1 witness element".to_string(), + )); + } else if script_pubkey.is_p2wsh() && witness.len() < 2 { + return Err(Error::InvalidFormat( + "P2WSH requires at least 2 witness elements".to_string(), + )); + } else if !(script_pubkey.is_p2wpkh() || script_pubkey.is_p2wsh() || script_pubkey.is_p2tr()) { + return Err(Error::InvalidFormat( + "Simple format only supports P2WPKH, P2WSH, or P2TR script types".to_string(), + )); + } - Ok(psbt) + Ok(()) } -pub(crate) type SecpCtx = Secp256k1; - #[cfg(test)] mod tests { + use super::*; + use alloc::string::ToString; use bitcoin::Address; use core::str::FromStr; - use super::*; - const HELLO_WORLD_MESSAGE: &str = "Hello World"; const SEGWIT_ADDRESS: &str = "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l"; @@ -167,31 +155,16 @@ mod tests { ); // Test case for empty message - to_sign - let tx_sign_empty_msg = to_sign( - &tx_spend_empty_msg.output[0].script_pubkey, - tx_spend_empty_msg.compute_txid(), - tx_spend_empty_msg.lock_time, - tx_spend_empty_msg.input[0].sequence, - Some(tx_spend_empty_msg.input[0].witness.clone()), - ) - .unwrap(); + let tx_sign_empty_msg = to_sign(&tx_spend_empty_msg).unwrap(); assert_eq!( - tx_sign_empty_msg.unsigned_tx.compute_txid().to_string(), + tx_sign_empty_msg.compute_txid().to_string(), "1e9654e951a5ba44c8604c4de6c67fd78a27e81dcadcfe1edf638ba3aaebaed6" ); // Test case for HELLO_WORLD_MESSAGE - to_sign - let tx_sign_hw_msg = to_sign( - &tx_spend_hello_world_msg.output[0].script_pubkey, - tx_spend_hello_world_msg.compute_txid(), - tx_spend_hello_world_msg.lock_time, - tx_spend_hello_world_msg.input[0].sequence, - Some(tx_spend_hello_world_msg.input[0].witness.clone()), - ) - .unwrap(); - + let tx_sign_hw_msg = to_sign(&tx_spend_hello_world_msg).unwrap(); assert_eq!( - tx_sign_hw_msg.unsigned_tx.compute_txid().to_string(), + tx_sign_hw_msg.compute_txid().to_string(), "88737ae86f2077145f93cc4b153ae9a1cb8d56afa511988c149c5c8c9d93bddf" ); } diff --git a/src/verifier.rs b/src/verifier.rs deleted file mode 100644 index 67437d9..0000000 --- a/src/verifier.rs +++ /dev/null @@ -1,647 +0,0 @@ -//! The verification implementation of generated signature for BIP-322 for -//! message signing according to the BIP-322 standard. - -use alloc::{ - str::FromStr, - string::{String, ToString}, -}; - -use bitcoin::{ - base64::{prelude::BASE64_STANDARD, Engine}, - consensus::Decodable, - io::Cursor, - opcodes::all::OP_RETURN, - secp256k1::{ecdsa::Signature, schnorr, Message}, - sighash::{self, SighashCache}, - sign_message::signed_msg_hash, - Address, Amount, EcdsaSighashType, OutPoint, PrivateKey, Psbt, PublicKey, ScriptBuf, - TapSighashType, Transaction, TxOut, Witness, WitnessVersion, XOnlyPublicKey, -}; - -use crate::{to_sign, to_spend, Error, SecpCtx, SignatureFormat}; - -/// Verifier encapsulates the data and functionality required to verify a message -/// signature according to the BIP322 protocol. It supports verifying signatures produced -/// using different signature formats: -/// - **Legacy:** Standard ECDSA signatures. -/// - **Simple:** Simplified signatures that encapsulate witness data. -/// - **Full:** Fully signed transactions with witness details. -/// -/// # Fields -/// - `address_str`: The Bitcoin address as a string against which the signature will be verified. -/// - `signature`: A Base64-encoded signature string. -/// - `message`: The original message that was signed. -/// - `signature_type`: The signature format used during signing, defined by `SignatureFormat`. -/// - `priv_key`: An optional private key string. Required for verifying legacy signatures. -pub struct Verifier { - address_str: String, - signature: String, - message: String, - signature_type: SignatureFormat, - private_key_str: Option, -} - -impl Verifier { - /// Creates a new instance of `Verifier` with the given parameters. - /// - /// # Arguments - /// - `address_str`: The Bitcoin address (as a string) associated with the signature. - /// - `signature`: The Base64-encoded signature to verify. - /// - `message`: The original message that was signed. - /// - `signature_type`: The BIP322 signature format that was used (Legacy, Simple, or Full). - /// - `priv_key`: An optional private key string used for legacy verification. - /// - /// # Returns - /// An instance of `Verifier`. - /// - /// # Example - /// ``` - /// # use bdk_bip322::{Verifier, SignatureFormat}; - /// - /// let verifier = Verifier::new( - /// "1BitcoinAddress...".to_string(), - /// "Base64EncodedSignature==".to_string(), - /// "Hello, Bitcoin!".to_string(), - /// SignatureFormat::Legacy, - /// Some("c...".to_string()), - /// ); - /// ``` - pub fn new( - address_str: String, - signature: String, - message: String, - signature_type: SignatureFormat, - private_key_str: Option, - ) -> Self { - Self { - address_str, - signature, - message, - signature_type, - private_key_str, - } - } - - /// Verifies a BIP322 message signature against the provided address and message. - /// - /// The verification logic differs depending on the signature format: - /// - Legacy - /// - Simple - /// - Full - /// - /// Returns `true` if the signature is valid, or an error if the decoding or verification - /// process fails. - pub fn verify(&self) -> Result { - let address = Address::from_str(&self.address_str) - .map_err(|_| Error::InvalidAddress)? - .assume_checked(); - let script_pubkey = address.script_pubkey(); - - let signature_bytes = BASE64_STANDARD - .decode(&self.signature) - .map_err(|_| Error::Base64DecodeError)?; - - match &self.signature_type { - SignatureFormat::Legacy => { - let pk = &self - .private_key_str - .as_ref() - .ok_or(Error::InvalidPrivateKey)?; - let private_key = PrivateKey::from_wif(pk).map_err(|_| Error::InvalidPrivateKey)?; - self.verify_legacy(&signature_bytes, private_key) - } - SignatureFormat::Simple => { - let mut cursor = Cursor::new(signature_bytes); - let witness = Witness::consensus_decode_from_finite_reader(&mut cursor) - .map_err(|_| Error::DecodeError("witness".to_string()))?; - - let to_spend_witness = to_spend(&script_pubkey, &self.message); - let to_sign_witness = to_sign( - &to_spend_witness.output[0].script_pubkey, - to_spend_witness.compute_txid(), - to_spend_witness.lock_time, - to_spend_witness.input[0].sequence, - Some(witness), - ) - .map_err(|_| Error::ExtractionError("psbt".to_string()))? - .extract_tx() - .map_err(|_| Error::ExtractionError("transaction".to_string()))?; - - self.verify_message(address, to_sign_witness) - } - SignatureFormat::Full => { - let mut cursor = Cursor::new(signature_bytes); - let transaction = Transaction::consensus_decode_from_finite_reader(&mut cursor) - .map_err(|_| Error::DecodeError("transaction".to_string()))?; - - self.verify_message(address, transaction) - } - } - } - - /// Verifies a BIP322-signed message by reconstructing the underlying transaction data - /// and checking the signature against the provided address and message. - /// - /// This function performs the following steps: - /// 1. Constructs a corresponding signing transaction (`to_sign`) using the witness data - /// from the given transaction. - /// 2. It delegates the verification process to the appropriate helper function: - /// - P2WPKH - /// - P2TR - /// - P2SH - /// 3. If none of the supported script types match, the function returns `Ok(false)`. - /// - /// # Returns - /// A `Result` containing: - /// - `Ok(true)` if the signature is valid. - /// - `Ok(false)` if the signature does not match the expected verification criteria. - /// - An error of type `BIP322Error` if the verification process fails at any step, - /// such as during transaction reconstruction or when decoding the witness data. - fn verify_message(&self, address: Address, transaction: Transaction) -> Result { - let script_pubkey = address.script_pubkey(); - - let to_spend = to_spend(&script_pubkey, &self.message); - let to_sign = to_sign( - &to_spend.output[0].script_pubkey, - to_spend.compute_txid(), - to_spend.lock_time, - to_spend.input[0].sequence, - Some(transaction.input[0].witness.clone()), - )?; - - if script_pubkey.is_p2wpkh() { - let verify = self.verify_p2sh_p2wpkh(address, transaction, to_spend, to_sign, true)?; - - return Ok(verify); - } else if script_pubkey.is_p2tr() || script_pubkey.is_p2wsh() { - let verify = self.verify_p2tr(address, to_spend, to_sign, transaction)?; - - return Ok(verify); - } else if script_pubkey.is_p2sh() { - let verify = self.verify_p2sh_p2wpkh(address, transaction, to_spend, to_sign, false)?; - - return Ok(verify); - } - - Ok(false) - } - - fn verify_legacy( - &self, - signature_bytes: &[u8], - private_key: PrivateKey, - ) -> Result { - let secp = SecpCtx::new(); - - let sig_without_sighash = &signature_bytes[..signature_bytes.len() - 1]; - - let pub_key = PublicKey::from_private_key(&secp, &private_key); - - let message_hash = signed_msg_hash(&self.message); - let msg = &Message::from_digest_slice(message_hash.as_ref()) - .map_err(|_| Error::InvalidMessage)?; - - let sig = Signature::from_der(sig_without_sighash) - .map_err(|e| Error::InvalidSignature(e.to_string()))?; - - let verify = secp.verify_ecdsa(msg, &sig, &pub_key.inner).is_ok(); - Ok(verify) - } - - fn verify_p2sh_p2wpkh( - &self, - address: Address, - to_sign_witness: Transaction, - to_spend: Transaction, - to_sign: Psbt, - is_segwit: bool, - ) -> Result { - let secp = SecpCtx::new(); - let pub_key = PublicKey::from_slice(&to_sign_witness.input[0].witness[1]) - .map_err(|e| Error::InvalidPublicKey(e.to_string()))?; - - if is_segwit { - let wp = address.witness_program().ok_or(Error::NotSegwitAddress)?; - - if wp.version() != WitnessVersion::V0 { - return Err(Error::UnsupportedSegwitVersion("v0".to_string())); - } - } - - let to_spend_outpoint = OutPoint { - txid: to_spend.compute_txid(), - vout: 0, - }; - - if to_spend_outpoint != to_sign.unsigned_tx.input[0].previous_output { - return Err(Error::InvalidSignature( - "to_sign must spend to_spend output".to_string(), - )); - } - - if to_sign.unsigned_tx.output[0].script_pubkey - != ScriptBuf::from_bytes(vec![OP_RETURN.to_u8()]) - { - return Err(Error::InvalidSignature( - "to_sign output must be OP_RETURN".to_string(), - )); - } - - let witness = to_sign.inputs[0] - .final_script_witness - .clone() - .ok_or(Error::InvalidWitness("missing data".to_string()))?; - - let encoded_signature = &witness.to_vec()[0]; - let witness_pub_key = &witness.to_vec()[1]; - let signature_length = encoded_signature.len(); - - if witness.len() != 2 { - return Err(Error::InvalidWitness("invalid witness length".to_string())); - } - - if pub_key.to_bytes() != *witness_pub_key { - return Err(Error::InvalidPublicKey("public key mismatch".to_string())); - } - - let signature = Signature::from_der(&encoded_signature.as_slice()[..signature_length - 1]) - .map_err(|e| Error::InvalidSignature(e.to_string()))?; - let sighash_type = - EcdsaSighashType::from_consensus(encoded_signature[signature_length - 1] as u32); - - if !(sighash_type == EcdsaSighashType::All) { - return Err(Error::InvalidSighashType); - } - - let mut sighash_cache = SighashCache::new(to_sign.unsigned_tx); - let wpubkey_hash = &pub_key - .wpubkey_hash() - .map_err(|e| Error::InvalidPublicKey(e.to_string()))?; - - let sighash = sighash_cache - .p2wpkh_signature_hash( - 0, - &if is_segwit { - to_spend.output[0].script_pubkey.clone() - } else { - ScriptBuf::new_p2wpkh(wpubkey_hash) - }, - to_spend.output[0].value, - sighash_type, - ) - .map_err(|_| Error::SighashError)?; - - let msg = - &Message::from_digest_slice(sighash.as_ref()).map_err(|_| Error::InvalidMessage)?; - - Ok(secp.verify_ecdsa(msg, &signature, &pub_key.inner).is_ok()) - } - - fn verify_p2tr( - &self, - address: Address, - to_spend: Transaction, - to_sign: Psbt, - to_sign_witness: Transaction, - ) -> Result { - let secp = SecpCtx::new(); - let script_pubkey = address.script_pubkey(); - let witness_program = script_pubkey.as_bytes(); - - let pubkey_bytes = &witness_program[2..]; - - let pub_key = XOnlyPublicKey::from_slice(pubkey_bytes) - .map_err(|e| Error::InvalidPublicKey(e.to_string()))?; - - let wp = address.witness_program().ok_or(Error::NotSegwitAddress)?; - - if wp.version() != WitnessVersion::V1 { - return Err(Error::UnsupportedSegwitVersion("v1".to_string())); - } - - let to_spend_outpoint = OutPoint { - txid: to_spend.compute_txid(), - vout: 0, - }; - - if to_spend_outpoint != to_sign.unsigned_tx.input[0].previous_output { - return Err(Error::InvalidSignature( - "to_sign must spend to_spend output".to_string(), - )); - } - - if to_sign_witness.output[0].script_pubkey != ScriptBuf::from_bytes(vec![OP_RETURN.to_u8()]) - { - return Err(Error::InvalidSignature( - "to_sign output must be OP_RETURN".to_string(), - )); - } - - let witness = to_sign.inputs[0] - .final_script_witness - .clone() - .ok_or(Error::InvalidWitness("missing data".to_string()))?; - - let encoded_signature = &witness.to_vec()[0]; - if witness.len() != 1 { - return Err(Error::InvalidWitness("invalid witness length".to_string())); - } - - let signature = schnorr::Signature::from_slice(&encoded_signature.as_slice()[..64]) - .map_err(|e| Error::InvalidSignature(e.to_string()))?; - let sighash_type = TapSighashType::from_consensus_u8(encoded_signature[64]) - .map_err(|_| Error::InvalidSighashType)?; - - if sighash_type != TapSighashType::All { - return Err(Error::InvalidSighashType); - } - - let mut sighash_cache = SighashCache::new(to_sign.unsigned_tx); - - let sighash = sighash_cache - .taproot_key_spend_signature_hash( - 0, - &sighash::Prevouts::All(&[TxOut { - value: Amount::from_sat(0), - script_pubkey: to_spend.output[0].clone().script_pubkey, - }]), - sighash_type, - ) - .map_err(|_| Error::SighashError)?; - - let msg = - &Message::from_digest_slice(sighash.as_ref()).map_err(|_| Error::InvalidMessage)?; - - Ok(secp.verify_schnorr(&signature, msg, &pub_key).is_ok()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::Signer; - - // TEST VECTORS FROM - // https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki#user-content-Test_vectors - // https://github.com/Peach2Peach/bip322-js/tree/main/test - - const PRIVATE_KEY: &str = "L3VFeEujGtevx9w18HD1fhRbCH67Az2dpCymeRE1SoPK6XQtaN2k"; - const PRIVATE_KEY_TESTNET: &str = "cTrF79uahxMC7bQGWh2931vepWPWqS8KtF8EkqgWwv3KMGZNJ2yP"; - - const SEGWIT_ADDRESS: &str = "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l"; - const SEGWIT_TESTNET_ADDRESS: &str = "tb1q9vza2e8x573nczrlzms0wvx3gsqjx7vaxwd45v"; - const TAPROOT_ADDRESS: &str = "bc1ppv609nr0vr25u07u95waq5lucwfm6tde4nydujnu8npg4q75mr5sxq8lt3"; - const TAPROOT_TESTNET_ADDRESS: &str = - "tb1ppv609nr0vr25u07u95waq5lucwfm6tde4nydujnu8npg4q75mr5s3g3s37"; - const LEGACY_ADDRESS: &str = "14vV3aCHBeStb5bkenkNHbe2YAFinYdXgc"; - const LEGACY_ADDRESS_TESTNET: &str = "mjSSLdHFzft9NC5NNMik7WrMQ9rRhMhNpT"; - - const HELLO_WORLD_MESSAGE: &str = "Hello World"; - - const NESTED_SEGWIT_PRIVATE_KEY: &str = "KwTbAxmBXjoZM3bzbXixEr9nxLhyYSM4vp2swet58i19bw9sqk5z"; - const NESTED_SEGWIT_TESTNET_PRIVATE_KEY: &str = - "cMpadsm2xoVpWV5FywY5cAeraa1PCtSkzrBM45Ladpf9rgDu6cMz"; - const NESTED_SEGWIT_ADDRESS: &str = "3HSVzEhCFuH9Z3wvoWTexy7BMVVp3PjS6f"; - const NESTED_SEGWIT_TESTNET_ADDRESS: &str = "2N8zi3ydDsMnVkqaUUe5Xav6SZqhyqEduap"; - - #[test] - fn sign_and_verify_legacy_signature() { - let legacy_sign = Signer::new( - PRIVATE_KEY.to_string(), - HELLO_WORLD_MESSAGE.to_string(), - LEGACY_ADDRESS.to_string(), - SignatureFormat::Legacy, - ); - let sign_message = legacy_sign.sign().unwrap(); - - let legacy_sign_testnet = Signer::new( - PRIVATE_KEY_TESTNET.to_string(), - HELLO_WORLD_MESSAGE.to_string(), - LEGACY_ADDRESS_TESTNET.to_string(), - SignatureFormat::Legacy, - ); - let sign_message_testnet = legacy_sign_testnet.sign().unwrap(); - - let legacy_verify = Verifier::new( - LEGACY_ADDRESS.to_string(), - sign_message, - HELLO_WORLD_MESSAGE.to_string(), - SignatureFormat::Legacy, - Some(PRIVATE_KEY.to_string()), - ); - - let verify_message = legacy_verify.verify().unwrap(); - - let legacy_verify_testnet = Verifier::new( - LEGACY_ADDRESS.to_string(), - sign_message_testnet, - HELLO_WORLD_MESSAGE.to_string(), - SignatureFormat::Legacy, - Some(PRIVATE_KEY_TESTNET.to_string()), - ); - - let verify_message_testnet = legacy_verify_testnet.verify().unwrap(); - - assert!(verify_message); - assert!(verify_message_testnet); - } - - #[test] - fn sign_and_verify_legacy_signature_with_wrong_message() { - let legacy_sign = Signer::new( - PRIVATE_KEY.to_string(), - HELLO_WORLD_MESSAGE.to_string(), - LEGACY_ADDRESS.to_string(), - SignatureFormat::Legacy, - ); - let sign_message = legacy_sign.sign().unwrap(); - - let legacy_verify = Verifier::new( - LEGACY_ADDRESS.to_string(), - sign_message, - "".to_string(), - SignatureFormat::Legacy, - Some(PRIVATE_KEY.to_string()), - ); - - let verify_message = legacy_verify.verify().unwrap(); - - assert!(!verify_message); - } - - #[test] - fn test_sign_and_verify_nested_segwit_address() { - let nested_segwit_simple_sign = Signer::new( - NESTED_SEGWIT_PRIVATE_KEY.to_string(), - HELLO_WORLD_MESSAGE.to_string(), - NESTED_SEGWIT_ADDRESS.to_string(), - SignatureFormat::Simple, - ); - - let sign_message = nested_segwit_simple_sign.sign().unwrap(); - assert_eq!( - sign_message.clone(), - "AkgwRQIhAMd2wZSY3x0V9Kr/NClochoTXcgDaGl3OObOR17yx3QQAiBVWxqNSS+CKen7bmJTG6YfJjsggQ4Fa2RHKgBKrdQQ+gEhAxa5UDdQCHSQHfKQv14ybcYm1C9y6b12xAuukWzSnS+w" - ); - - let nested_segwit_full_sign_testnet = Signer::new( - NESTED_SEGWIT_TESTNET_PRIVATE_KEY.to_string(), - HELLO_WORLD_MESSAGE.to_string(), - NESTED_SEGWIT_TESTNET_ADDRESS.to_string(), - SignatureFormat::Full, - ); - - let sign_message_testnet = nested_segwit_full_sign_testnet.sign().unwrap(); - assert_eq!( - sign_message_testnet.clone(), - "AAAAAAABAVuR8vsJiiYj9+vO+8l7Ol3wt3Frz7SVyVSxn0ehOUb+AAAAAAAAAAAAAQAAAAAAAAAAAWoCSDBFAiEAx3bBlJjfHRX0qv80KWhyGhNdyANoaXc45s5HXvLHdBACIFVbGo1JL4Ip6ftuYlMbph8mOyCBDgVrZEcqAEqt1BD6ASEDFrlQN1AIdJAd8pC/XjJtxibUL3LpvXbEC66RbNKdL7AAAAAA" - ); - - let nested_segwit_full_verify = Verifier::new( - NESTED_SEGWIT_ADDRESS.to_string(), - sign_message, - HELLO_WORLD_MESSAGE.to_string(), - SignatureFormat::Simple, - None, - ); - - let verify_message = nested_segwit_full_verify.verify().unwrap(); - - let nested_segwit_full_verify_testnet = Verifier::new( - NESTED_SEGWIT_TESTNET_ADDRESS.to_string(), - sign_message_testnet, - HELLO_WORLD_MESSAGE.to_string(), - SignatureFormat::Full, - None, - ); - - let verify_message_testnet = nested_segwit_full_verify_testnet.verify().unwrap(); - - assert!(verify_message); - assert!(verify_message_testnet); - } - - #[test] - fn test_sign_and_verify_segwit_address() { - let full_sign = Signer::new( - PRIVATE_KEY.to_string(), - HELLO_WORLD_MESSAGE.to_string(), - SEGWIT_ADDRESS.to_string(), - SignatureFormat::Full, - ); - let sign_message = full_sign.sign().unwrap(); - - let simple_sign = Signer::new( - PRIVATE_KEY_TESTNET.to_string(), - HELLO_WORLD_MESSAGE.to_string(), - SEGWIT_TESTNET_ADDRESS.to_string(), - SignatureFormat::Simple, - ); - let sign_message_testnet = simple_sign.sign().unwrap(); - - let full_verify = Verifier::new( - SEGWIT_ADDRESS.to_string(), - sign_message, - HELLO_WORLD_MESSAGE.to_string(), - SignatureFormat::Full, - None, - ); - - let verify_message = full_verify.verify().unwrap(); - - let simple_verify = Verifier::new( - SEGWIT_TESTNET_ADDRESS.to_string(), - sign_message_testnet, - HELLO_WORLD_MESSAGE.to_string(), - SignatureFormat::Simple, - None, - ); - - let verify_message_testnet = simple_verify.verify().unwrap(); - - assert!(verify_message); - assert!(verify_message_testnet); - } - - #[test] - fn test_sign_and_verify_taproot_address() { - let full_sign = Signer::new( - PRIVATE_KEY.to_string(), - HELLO_WORLD_MESSAGE.to_string(), - TAPROOT_ADDRESS.to_string(), - SignatureFormat::Full, - ); - let sign_message = full_sign.sign().unwrap(); - - let simple_sign = Signer::new( - PRIVATE_KEY.to_string(), - HELLO_WORLD_MESSAGE.to_string(), - TAPROOT_TESTNET_ADDRESS.to_string(), - SignatureFormat::Simple, - ); - - let sign_message_testnet = simple_sign.sign().unwrap(); - - let full_verify = Verifier::new( - TAPROOT_ADDRESS.to_string(), - sign_message, - HELLO_WORLD_MESSAGE.to_string(), - SignatureFormat::Full, - None, - ); - - let verify_message = full_verify.verify().unwrap(); - - let simple_verify = Verifier::new( - TAPROOT_TESTNET_ADDRESS.to_string(), - sign_message_testnet, - HELLO_WORLD_MESSAGE.to_string(), - SignatureFormat::Simple, - None, - ); - - let verify_message_testnet = simple_verify.verify().unwrap(); - - assert!(verify_message); - assert!(verify_message_testnet); - } - - #[test] - fn test_simple_segwit_verification() { - let simple_verify = Verifier::new( - SEGWIT_ADDRESS.to_string(), - "AkcwRAIgZRfIY3p7/DoVTty6YZbWS71bc5Vct9p9Fia83eRmw2QCICK/ENGfwLtptFluMGs2KsqoNSk89pO7F29zJLUx9a/sASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI=".to_string(), - HELLO_WORLD_MESSAGE.to_string(), - SignatureFormat::Simple, - None, - ); - assert!(simple_verify.verify().unwrap()); - - let simple_verify_2 = Verifier::new( - SEGWIT_ADDRESS.to_string(), - "AkgwRQIhAOzyynlqt93lOKJr+wmmxIens//zPzl9tqIOua93wO6MAiBi5n5EyAcPScOjf1lAqIUIQtr3zKNeavYabHyR8eGhowEhAsfxIAMZZEKUPYWI4BruhAQjzFT8FSFSajuFwrDL1Yhy".to_string(), - HELLO_WORLD_MESSAGE.to_string(), - SignatureFormat::Simple, - None, - ); - assert!(simple_verify_2.verify().unwrap()); - - let simple_verify_empty_message = Verifier::new( - SEGWIT_ADDRESS.to_string(), - "AkgwRQIhAPkJ1Q4oYS0htvyuSFHLxRQpFAY56b70UvE7Dxazen0ZAiAtZfFz1S6T6I23MWI2lK/pcNTWncuyL8UL+oMdydVgzAEhAsfxIAMZZEKUPYWI4BruhAQjzFT8FSFSajuFwrDL1Yhy".to_string(), - "".to_string(), - SignatureFormat::Simple, - None, - ); - assert!(simple_verify_empty_message.verify().unwrap()); - - let simple_verify_empty_message_2 = Verifier::new( - SEGWIT_ADDRESS.to_string(), - "AkcwRAIgM2gBAQqvZX15ZiysmKmQpDrG83avLIT492QBzLnQIxYCIBaTpOaD20qRlEylyxFSeEA2ba9YOixpX8z46TSDtS40ASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI=".to_string(), - "".to_string(), - SignatureFormat::Simple, - None, - ); - assert!(simple_verify_empty_message_2.verify().unwrap()); - } -} diff --git a/src/verify.rs b/src/verify.rs new file mode 100644 index 0000000..eddaabc --- /dev/null +++ b/src/verify.rs @@ -0,0 +1,530 @@ +//! BIP-322 signature verification implementation. +//! +//! This module handles verification of all BIP-322 signature formats including +//! legacy, simple, full, and proof-of-funds variants. + +use crate::{ + Bip322VerificationResult, Error, SecpCtx, SignatureFormat, to_sign, to_spend, validate_witness, +}; +use alloc::{ + string::{String, ToString}, + vec::Vec, +}; +use bdk_wallet::Wallet; +use bitcoin::{ + Address, Amount, EcdsaSighashType, OutPoint, Psbt, PubkeyHash, PublicKey, ScriptBuf, + TapSighashType, Transaction, TxOut, Witness, WitnessVersion, XOnlyPublicKey, + base64::{Engine, engine::general_purpose}, + consensus::Decodable, + hashes::Hash, + opcodes::all::OP_RETURN, + secp256k1::{Message, ecdsa::Signature, schnorr}, + sighash::{self, SighashCache}, +}; + +/// Verifies a PSBT (unsigned) proof for proof-of-funds. +pub fn verify_psbt_proof( + psbt: &Psbt, + script_pubkey: ScriptBuf, +) -> Result { + // Calculate total amount from inputs matching the address + let total_amount: Amount = psbt + .inputs + .iter() + .enumerate() + .skip(1) + .filter_map(|(i, psbt_input)| { + let tx_input = &psbt.unsigned_tx.input.get(i)?; + + // Get UTXO value from witness_utxo or non_witness_utxo + psbt_input.witness_utxo.as_ref().or_else(|| { + psbt_input + .non_witness_utxo + .as_ref() + .and_then(|tx| tx.output.get(tx_input.previous_output.vout as usize)) + }) + }) + .filter(|utxo| utxo.script_pubkey == script_pubkey) + .map(|utxo| utxo.value) + .sum(); + + Ok(if total_amount > Amount::ZERO { + Bip322VerificationResult { + valid: true, + proven_amount: Some(total_amount), + } + } else { + Bip322VerificationResult { + valid: false, + proven_amount: None, + } + }) +} + +/// Verifies a signed (finalized) BIP-322 proof. +pub fn verify_signed_proof( + wallet: &mut Wallet, + message: &str, + sig_type: SignatureFormat, + address: &Address, + script_pubkey: &ScriptBuf, + tx: &String, +) -> Result { + let to_spend = to_spend(script_pubkey, message); + let mut to_sign = to_sign(&to_spend)?; + + // Decode the base64 signature + let signature_bytes = general_purpose::STANDARD + .decode(tx) + .map_err(|_| Error::InvalidFormat("Invalid base64 encoding".to_string()))?; + + let mut cursor = bitcoin::io::Cursor::new(&signature_bytes); + + match sig_type { + SignatureFormat::Legacy => { + let verification_result = verify_legacy(&signature_bytes, message)?; + + Ok(Bip322VerificationResult { + valid: verification_result, + proven_amount: None, + }) + } + SignatureFormat::Simple => { + let witness = Witness::consensus_decode_from_finite_reader(&mut cursor)?; + validate_witness(&witness, script_pubkey)?; + + to_sign.input[0].witness = witness; + + let verification_result = + verify_message(wallet, address, &to_sign, to_spend, script_pubkey, sig_type)?; + + Ok(Bip322VerificationResult { + valid: verification_result, + proven_amount: None, + }) + } + SignatureFormat::Full => { + let tx = Transaction::consensus_decode_from_finite_reader(&mut cursor)?; + let verification_result = + verify_message(wallet, address, &tx, to_spend, script_pubkey, sig_type)?; + + Ok(Bip322VerificationResult { + valid: verification_result, + proven_amount: None, + }) + } + SignatureFormat::FullWithProofOfFunds => { + let tx = Transaction::consensus_decode_from_finite_reader(&mut cursor)?; + // Validate transaction has proof-of-funds inputs + if tx.input.len() < 2 { + return Err(Error::InvalidFormat( + "FullWithProofOfFunds requires at least 2 inputs".to_string(), + )); + } + + // Verify all additional inputs belong to the same address + for input in tx.input.iter().skip(1) { + let utxo = wallet + .get_utxo(input.previous_output) + .ok_or(Error::UtxoNotFound(input.previous_output))?; + + if utxo.txout.script_pubkey != *script_pubkey { + return Err(Error::InvalidFormat( + "Additional input doesn't belong to the same address".to_string(), + )); + } + } + + let verification_result = + verify_message(wallet, address, &tx, to_spend, script_pubkey, sig_type)?; + + let total_amount: Amount = tx + .input + .iter() + .skip(1) + .filter_map(|tx_in| { + let utxo = wallet.get_utxo(tx_in.previous_output)?; + if utxo.txout.script_pubkey == *script_pubkey { + Some(utxo.txout.value) + } else { + None + } + }) + .sum(); + + Ok(Bip322VerificationResult { + valid: verification_result, + proven_amount: if total_amount > Amount::ZERO { + Some(total_amount) + } else { + None + }, + }) + } + } +} + +/// Verifies a BIP-322 message signature for the given address using the specified format. +fn verify_message( + wallet: &mut Wallet, + address: &Address, + to_sign: &Transaction, + to_spend: Transaction, + script_pubkey: &ScriptBuf, + sig_type: SignatureFormat, +) -> Result { + // Verify to_sign spends to_spend + let to_spend_outpoint = OutPoint { + txid: to_spend.compute_txid(), + vout: 0, + }; + if to_spend_outpoint != to_sign.input[0].previous_output { + return Err(Error::InvalidSignature( + "to_sign must spend to_spend output".to_string(), + )); + } + // Verify output is OP_RETURN + if to_sign.output[0].script_pubkey != ScriptBuf::from_bytes(vec![OP_RETURN.to_u8()]) { + return Err(Error::InvalidSignature( + "to_sign output must be OP_RETURN".to_string(), + )); + } + + let prevouts = TxOut { + value: Amount::from_sat(0), + script_pubkey: to_spend.output[0].clone().script_pubkey, + }; + + if script_pubkey.is_p2wpkh() { + let wp = address.witness_program().ok_or(Error::NotSegwitAddress)?; + if wp.version() != WitnessVersion::V0 { + return Err(Error::UnsupportedSegwitVersion("v0".to_string())); + } + verify_p2wpkh(to_sign, &prevouts, 0)? + } else if script_pubkey.is_p2wsh() { + let wp = address.witness_program().ok_or(Error::NotSegwitAddress)?; + if wp.version() != WitnessVersion::V0 { + return Err(Error::UnsupportedSegwitVersion("v0".to_string())); + } + verify_p2wsh_single_script(to_sign, &prevouts, address, 0)? + } else if script_pubkey.is_p2tr() { + let wp = address.witness_program().ok_or(Error::NotSegwitAddress)?; + if wp.version() != WitnessVersion::V1 { + return Err(Error::UnsupportedSegwitVersion("v1".to_string())); + } + + verify_p2tr(to_sign, &prevouts, 0, wallet, &to_spend)? + } else { + return Ok(false); + }; + + // For proof-of-funds, verify all additional inputs + if sig_type == SignatureFormat::FullWithProofOfFunds { + return verify_proof_of_funds(wallet, to_sign, script_pubkey, &to_spend, address); + } + + Ok(true) +} + +/// Verifies all proof-of-funds inputs beyond the first. +fn verify_proof_of_funds( + wallet: &mut Wallet, + to_sign: &Transaction, + script_pubkey: &ScriptBuf, + to_spend: &Transaction, + address: &Address, +) -> Result { + if to_sign.input.len() < 2 { + return Err(Error::InvalidFormat( + "FullWithProofOfFunds requires at least 2 inputs".to_string(), + )); + } + + // Verify each additional input (starting from index 1) + for (i, tx_input) in to_sign.input.iter().enumerate().skip(1) { + // Get the UTXO being spent + let utxo = wallet + .get_utxo(tx_input.previous_output) + .ok_or(Error::UtxoNotFound(tx_input.previous_output))?; + + // Verify it belongs to the same address + if utxo.txout.script_pubkey != *script_pubkey { + return Ok(false); + } + + // Verify the signature for this input + if script_pubkey.is_p2wpkh() { + verify_p2wpkh(to_sign, &utxo.txout, i)? + } else if script_pubkey.is_p2tr() { + verify_p2tr(to_sign, &utxo.txout, i, wallet, to_spend)? + } else if script_pubkey.is_p2wsh() { + verify_p2wsh_single_script(to_sign, &utxo.txout, address, i)? + } else { + return Err(Error::InvalidFormat( + "Unsupported script type for proof of funds".to_string(), + )); + }; + } + + Ok(true) +} + +/// Verifies Legacy format Bitcoin Core message signature. +fn verify_legacy(signature_bytes: &[u8], message: &str) -> Result { + let secp = SecpCtx::new(); + + // Validate signature length + if signature_bytes.len() != 106 { + return Err(Error::InvalidFormat(format!( + "Invalid scriptSig length: {} (expected 106)", + signature_bytes.len() + ))); + } + + // Parse scriptSig: [sig_len][sig_data][sighash][pubkey_len][pubkey_data] + let sig_len = signature_bytes[0] as usize; + if sig_len != 71 { + return Err(Error::InvalidFormat(format!( + "Invalid signature length marker: {} (expected 71)", + sig_len + ))); + } + + let sig_with_sighash = &signature_bytes[1..=71]; + let sig_without_sighash = &sig_with_sighash[..70]; + let sighash_byte = sig_with_sighash[70]; + + // Validate pubkey length marker + let pubkey_len = signature_bytes[72] as usize; + if pubkey_len != 33 { + return Err(Error::InvalidFormat(format!( + "Invalid pubkey length: {} (expected 33)", + pubkey_len + ))); + } + + let pubkey = &signature_bytes[73..]; + + // Validate sighash type + let sighash_type = EcdsaSighashType::from_consensus(sighash_byte as u32); + if sighash_type != EcdsaSighashType::All { + return Err(Error::InvalidSighashType); + } + + // Parse public key and derive script_pubkey + let pub_key = + PublicKey::from_slice(pubkey).map_err(|e| Error::InvalidPublicKey(e.to_string()))?; + + let pubkey_hash = PubkeyHash::hash(pubkey); + let script_pubkey = ScriptBuf::new_p2pkh(&pubkey_hash); + + // Create the to_spend transaction using BIP322 method + let to_spend = to_spend(&script_pubkey, message); + let to_sign = to_sign(&to_spend)?; + + // Calculate the legacy sighash that was signed + let sighash_cache = SighashCache::new(&to_sign); + let sighash = sighash_cache + .legacy_signature_hash(0, &script_pubkey, sighash_type.to_u32()) + .map_err(|_| Error::SighashError)?; + + let msg = Message::from_digest_slice(sighash.as_ref()).map_err(|_| Error::InvalidMessage)?; + + // Parse and verify signature + let signature = Signature::from_der(sig_without_sighash) + .map_err(|e| Error::InvalidSignature(e.to_string()))?; + + Ok(secp.verify_ecdsa(&msg, &signature, &pub_key.inner).is_ok()) +} + +/// Verifies P2WPKH (Pay-to-Witness-Public-Key-Hash) signature. +fn verify_p2wpkh( + to_sign: &Transaction, + prevout: &TxOut, + input_index: usize, +) -> Result { + let secp = SecpCtx::new(); + let witness = to_sign.input[input_index].witness.clone(); + + if witness.len() != 2 { + return Err(Error::InvalidWitness( + "P2WPKH requires exactly 2 witness elements".to_string(), + )); + } + + // Extract witness elements + let encoded_signature = &witness.to_vec()[0]; + let witness_pub_key = &witness.to_vec()[1]; + let signature_length = encoded_signature.len(); + + if encoded_signature.is_empty() { + return Ok(false); + } + + // Parse public key + let pub_key = PublicKey::from_slice(witness_pub_key) + .map_err(|e| Error::InvalidPublicKey(e.to_string()))?; + + // Parse signature (DER + sighash byte) + let signature = Signature::from_der(&encoded_signature.as_slice()[..signature_length - 1]) + .map_err(|e| Error::InvalidSignature(e.to_string()))?; + let sighash_type = + EcdsaSighashType::from_consensus(encoded_signature[signature_length - 1] as u32); + + if sighash_type != EcdsaSighashType::All { + return Err(Error::InvalidSighashType); + } + + // Compute sighash + let mut sighash_cache = SighashCache::new(to_sign); + let wpubkey_hash = &pub_key + .wpubkey_hash() + .map_err(|e| Error::InvalidPublicKey(e.to_string()))?; + let script_pubkey = ScriptBuf::new_p2wpkh(wpubkey_hash); + + let sighash = sighash_cache + .p2wpkh_signature_hash(input_index, &script_pubkey, prevout.value, sighash_type) + .map_err(|_| Error::SighashError)?; + + let msg = &Message::from_digest_slice(sighash.as_ref()).map_err(|_| Error::InvalidMessage)?; + + Ok(secp.verify_ecdsa(msg, &signature, &pub_key.inner).is_ok()) +} + +/// Verifies P2WSH (Pay-to-Witness-Script-Hash) signature for single-sig scripts. +fn verify_p2wsh_single_script( + to_sign: &Transaction, + prevout: &TxOut, + address: &Address, + input_index: usize, +) -> Result { + let secp = SecpCtx::new(); + let script_pubkey = address.script_pubkey(); + let witness = to_sign.input[input_index].witness.clone(); + + // Validate witness has minimum required elements + if witness.len() < 2 { + return Err(Error::InvalidWitness( + "P2WSH requires at least 2 witness elements".to_string(), + )); + } + + let witness_script_bytes = witness + .nth(witness.len() - 1) + .ok_or(Error::InvalidWitness("No witness script found".to_string()))?; + let witness_script = ScriptBuf::from_bytes(witness_script_bytes.to_vec()); + + // Get signature + let signature_bytes = witness + .nth(0) + .ok_or(Error::InvalidWitness("No signature".to_string()))?; + + let signature_length = signature_bytes.len(); + if signature_bytes.is_empty() { + return Ok(false); + } + + // Verify witness script hash matches address + let script_hash = witness_script.wscript_hash(); + let expected_script_pubkey = ScriptBuf::new_p2wsh(&script_hash); + + if script_pubkey != expected_script_pubkey { + return Err(Error::InvalidSignature( + "Witness script hash doesn't match address".to_string(), + )); + } + + // Parse signature + let signature = Signature::from_der(&signature_bytes[..signature_length - 1]) + .map_err(|e| Error::InvalidSignature(e.to_string()))?; + let sighash_type = + EcdsaSighashType::from_consensus(signature_bytes[signature_length - 1] as u32); + + if sighash_type != EcdsaSighashType::All { + return Err(Error::InvalidSighashType); + } + + // Extract public key from witness script + let pub_key = PublicKey::from_slice(&witness_script.as_bytes()[1..34]) + .map_err(|e| Error::InvalidPublicKey(e.to_string()))?; + + // Compute sighash + let mut sighash_cache = SighashCache::new(to_sign); + let sighash = sighash_cache + .p2wsh_signature_hash(input_index, &witness_script, prevout.value, sighash_type) + .map_err(|_| Error::SighashError)?; + + let msg = &Message::from_digest_slice(sighash.as_ref()).map_err(|_| Error::InvalidMessage)?; + + Ok(secp.verify_ecdsa(msg, &signature, &pub_key.inner).is_ok()) +} + +/// Verifies P2TR (Pay-to-Taproot) signature for key path spend. +fn verify_p2tr( + to_sign: &Transaction, + prevout: &TxOut, + input_index: usize, + wallet: &mut Wallet, + to_spend: &Transaction, +) -> Result { + let secp = SecpCtx::new(); + let script_bytes = prevout.script_pubkey.as_bytes(); + + // Extract x-only public key from script + let pub_key = XOnlyPublicKey::from_slice(&script_bytes[2..]) + .map_err(|e| Error::InvalidPublicKey(e.to_string()))?; + + // Validate witness structure + let witness = to_sign.input[input_index].witness.clone(); + if witness.len() != 1 { + return Err(Error::InvalidWitness( + "P2TR key path requires exactly 1 witness element".to_string(), + )); + } + + let encoded_signature = &witness.to_vec()[0]; + if encoded_signature.len() != 65 { + return Ok(false); + } + + // Parse Schnorr signature + let signature = schnorr::Signature::from_slice(&encoded_signature.as_slice()[..64]) + .map_err(|e| Error::InvalidSignature(e.to_string()))?; + let sighash_type = TapSighashType::from_consensus_u8(encoded_signature[64]) + .map_err(|_| Error::InvalidSighashType)?; + + if sighash_type != TapSighashType::All { + return Err(Error::InvalidSighashType); + } + + // Build prevouts array for sighash computation + let mut prevouts_vec = Vec::new(); + let to_spend_outpoint = OutPoint { + txid: to_spend.compute_txid(), + vout: 0, + }; + for (i, txin) in to_sign.input.iter().enumerate() { + if i == input_index { + prevouts_vec.push(prevout.clone()); + } else if txin.previous_output == to_spend_outpoint { + prevouts_vec.push(to_spend.output[0].clone()); + } else { + let utxo = wallet + .get_utxo(txin.previous_output) + .ok_or(Error::UtxoNotFound(txin.previous_output))?; + prevouts_vec.push(utxo.txout); + } + } + + let prevouts = sighash::Prevouts::All(&prevouts_vec); + + // Compute sighash + let mut sighash_cache = SighashCache::new(to_sign); + let sighash = sighash_cache + .taproot_key_spend_signature_hash(input_index, &prevouts, sighash_type) + .map_err(|_| Error::SighashError)?; + + let msg = &Message::from_digest_slice(sighash.as_ref()).map_err(|_| Error::InvalidMessage)?; + + Ok(secp.verify_schnorr(&signature, msg, &pub_key).is_ok()) +}