From 48f171e69a54273aeab0e88568f4c3a674397d01 Mon Sep 17 00:00:00 2001 From: valued mammal Date: Sat, 28 Sep 2024 11:42:12 -0400 Subject: [PATCH 1/3] feat(chain): Add method `verify_tx` for TxGraph * Add feature `bitcoinconsensus` to Cargo.toml --- crates/chain/Cargo.toml | 1 + crates/chain/src/tx_graph.rs | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/crates/chain/Cargo.toml b/crates/chain/Cargo.toml index 95bcaf9f6..98b3b4e84 100644 --- a/crates/chain/Cargo.toml +++ b/crates/chain/Cargo.toml @@ -35,3 +35,4 @@ std = ["bitcoin/std", "miniscript?/std", "bdk_core/std"] serde = ["dep:serde", "bitcoin/serde", "miniscript?/serde", "bdk_core/serde"] hashbrown = ["bdk_core/hashbrown"] rusqlite = ["std", "dep:rusqlite", "serde", "serde_json"] +bitcoinconsensus = ["bitcoin/bitcoinconsensus"] diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index 9a32ccdfc..e604f5c34 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -416,8 +416,44 @@ impl TxGraph { .range(start..=end) .map(|(outpoint, spends)| (outpoint.vout, spends)) } + + /// Verify the given transaction is able to spend its inputs. + /// + /// This method uses [`rust-bitcoinconsensus`][0] to verify a transaction, guaranteeing + /// that if the method succeeds the transaction meets consensus criteria as defined in + /// Bitcoin's `libbitcoinconsensus`. + /// + /// # Errors + /// + /// If the previous output isn't found for one or more `tx` inputs. + /// + /// If Bitcoin Script verification fails. + /// + /// [0]: https://docs.rs/bitcoinconsensus/latest/bitcoinconsensus/ + #[cfg(feature = "bitcoinconsensus")] + #[cfg_attr(docsrs, doc(cfg(feature = "bitcoinconsensus")))] + pub fn verify_tx(&self, tx: &Transaction) -> Result<(), VerifyTxError> { + tx.verify(|op: &OutPoint| -> Option { self.get_txout(*op).cloned() }) + .map_err(VerifyTxError) + } } +/// Error returned by [`TxGraph::verify_tx`]. +#[cfg(feature = "bitcoinconsensus")] +#[derive(Debug)] +pub struct VerifyTxError(pub bitcoin::transaction::TxVerifyError); + +#[cfg(feature = "bitcoinconsensus")] +impl fmt::Display for VerifyTxError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +#[cfg(feature = "std")] +#[cfg(feature = "bitcoinconsensus")] +impl std::error::Error for VerifyTxError {} + impl TxGraph { /// Creates an iterator that filters and maps ancestor transactions. /// From e52055bb7a1f5828d20f0f534f1b7182b11d931e Mon Sep 17 00:00:00 2001 From: valued mammal Date: Sat, 28 Sep 2024 12:54:45 -0400 Subject: [PATCH 2/3] test(tx_graph): add test_verify_tx Check we can verify a transaction when `TxGraph` has knowledge of the coins being spent. Also check that verification fails either due to missing prevouts or a malformed input. --- crates/chain/tests/common/mod.rs | 8 +++++ crates/chain/tests/test_tx_graph.rs | 54 +++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/crates/chain/tests/common/mod.rs b/crates/chain/tests/common/mod.rs index 3fad37f93..c39d52d61 100644 --- a/crates/chain/tests/common/mod.rs +++ b/crates/chain/tests/common/mod.rs @@ -14,6 +14,14 @@ macro_rules! block_id { }}; } +/// Returns `Vec` from a hex `&str` +#[allow(unused_macros)] +macro_rules! hex { + ($hex:literal) => { + as bitcoin::hex::FromHex>::from_hex($hex) + }; +} + #[allow(unused_macros)] macro_rules! h { ($index:literal) => {{ diff --git a/crates/chain/tests/test_tx_graph.rs b/crates/chain/tests/test_tx_graph.rs index a49c9e5f5..9f479942c 100644 --- a/crates/chain/tests/test_tx_graph.rs +++ b/crates/chain/tests/test_tx_graph.rs @@ -1263,3 +1263,57 @@ fn tx_graph_update_conversion() { ); } } + +#[test] +#[allow(unused)] +#[cfg(feature = "bitcoinconsensus")] +fn test_verify_tx() { + use bdk_chain::tx_graph::VerifyTxError; + use bitcoin::consensus; + use bitcoin::transaction::TxVerifyError; + + // spent tx + // txid: 95da344585fcf2e5f7d6cbf2c3df2dcce84f9196f7a7bb901a43275cd6eb7c3f + let spent: Transaction = consensus::deserialize(hex!("020000000101192dea5e66d444380e106f8e53acb171703f00d43fb6b3ae88ca5644bdb7e1000000006b48304502210098328d026ce138411f957966c1cf7f7597ccbb170f5d5655ee3e9f47b18f6999022017c3526fc9147830e1340e04934476a3d1521af5b4de4e98baf49ec4c072079e01210276f847f77ec8dd66d78affd3c318a0ed26d89dab33fa143333c207402fcec352feffffff023d0ac203000000001976a9144bfbaf6afb76cc5771bc6404810d1cc041a6933988aca4b956050000000017a91494d5543c74a3ee98e0cf8e8caef5dc813a0f34b48768cb0700") + .unwrap() + .as_slice()) + .unwrap(); + let spent_prevout: OutPoint = + "e1b7bd4456ca88aeb3b63fd4003f7071b1ac538e6f100e3844d4665eea2d1901:0" + .parse() + .unwrap(); + // spending tx + // txid: aca326a724eda9a461c10a876534ecd5ae7b27f10f26c3862fb996f80ea2d45d + let spending: Transaction = consensus::deserialize(hex!("02000000013f7cebd65c27431a90bba7f796914fe8cc2ddfc3f2cbd6f7e5f2fc854534da95000000006b483045022100de1ac3bcdfb0332207c4a91f3832bd2c2915840165f876ab47c5f8996b971c3602201c6c053d750fadde599e6f5c4e1963df0f01fc0d97815e8157e3d59fe09ca30d012103699b464d1d8bc9e47d4fb1cdaa89a1c5783d68363c4dbc4b524ed3d857148617feffffff02836d3c01000000001976a914fc25d6d5c94003bf5b0c7b640a248e2c637fcfb088ac7ada8202000000001976a914fbed3d9b11183209a57999d54d59f67c019e756c88ac6acb0700") + .unwrap() + .as_slice()) + .unwrap(); + let spending_prevout: OutPoint = + "95da344585fcf2e5f7d6cbf2c3df2dcce84f9196f7a7bb901a43275cd6eb7c3f:0" + .parse() + .unwrap(); + + // First insert the spending tx. neither verify because we don't have prevouts + let mut graph = TxGraph::::default(); + let _ = graph.insert_tx(spending.clone()); + assert!(matches!( + graph.verify_tx(&spending).unwrap_err(), + VerifyTxError(TxVerifyError::UnknownSpentOutput(outpoint)) + if outpoint == spending_prevout + )); + assert!(matches!( + graph.verify_tx(&spent).unwrap_err(), + VerifyTxError(TxVerifyError::UnknownSpentOutput(outpoint)) + if outpoint == spent_prevout + )); + // Now insert the spent parent. spending tx verifies + let _ = graph.insert_tx(spent); + graph.verify_tx(&spending).unwrap(); + // Verification fails for malformed input + let mut tx = spending.clone(); + tx.input[0].script_sig = ScriptBuf::from_bytes(vec![0x00; 3]); + assert!(matches!( + graph.verify_tx(&tx).unwrap_err(), + VerifyTxError(TxVerifyError::ScriptVerification(_)) + )); +} From 4bd918a48182e2a5c4beb42f91b8bc4802e30092 Mon Sep 17 00:00:00 2001 From: valued mammal Date: Sat, 28 Sep 2024 13:22:22 -0400 Subject: [PATCH 3/3] feat(wallet): expose `TxGraph::verify_tx` for Wallet * Add feature `bitcoinconsensus` to Cargo.toml --- crates/wallet/Cargo.toml | 1 + crates/wallet/src/wallet/mod.rs | 41 ++++++++++++++++++++++++++++++++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/crates/wallet/Cargo.toml b/crates/wallet/Cargo.toml index ebec3a842..cab26145d 100644 --- a/crates/wallet/Cargo.toml +++ b/crates/wallet/Cargo.toml @@ -35,6 +35,7 @@ all-keys = ["keys-bip39"] keys-bip39 = ["bip39"] rusqlite = ["bdk_chain/rusqlite"] file_store = ["bdk_file_store"] +bitcoinconsensus = ["bdk_chain/bitcoinconsensus"] [dev-dependencies] lazy_static = "1.4" diff --git a/crates/wallet/src/wallet/mod.rs b/crates/wallet/src/wallet/mod.rs index 1a25e7d7d..aeb199a85 100644 --- a/crates/wallet/src/wallet/mod.rs +++ b/crates/wallet/src/wallet/mod.rs @@ -2487,6 +2487,45 @@ impl Wallet { keychain } } + + /// Verify the given transaction is able to spend its inputs. + /// + /// This method uses [`rust-bitcoinconsensus`][0] to verify a transaction, guaranteeing + /// that if the method succeeds the transaction meets consensus criteria as defined in + /// Bitcoin's `libbitcoinconsensus`. + /// + /// # Example + /// + /// ```rust,no_run + /// # use bitcoin::Amount; + /// # use bdk_wallet::{KeychainKind, SignOptions}; + /// # let mut wallet = bdk_wallet::doctest_wallet!(); + /// # let address = wallet.reveal_next_address(KeychainKind::External); + /// let mut builder = wallet.build_tx(); + /// builder.add_recipient(address.script_pubkey(), Amount::from_sat(100_000)); + /// let mut psbt = builder.finish().unwrap(); + /// let _ = wallet.sign(&mut psbt, SignOptions::default()).unwrap(); + /// let tx = psbt.extract_tx().unwrap(); + /// assert!(wallet.verify_tx(&tx).is_ok()); + /// ``` + /// + /// Note that validation by the Bitcoin network can ultimately fail in other ways, for + /// example if a timelock wasn't met. Also verifying that a transaction can spend its + /// inputs doesn't guarantee it will be accepted to mempools or propagated by nodes on + /// the peer-to-peer network. + /// + /// # Errors + /// + /// If the previous output isn't found for one or more `tx` inputs. + /// + /// If Bitcoin Script verification fails. + /// + /// [0]: https://docs.rs/bitcoinconsensus/latest/bitcoinconsensus/ + #[cfg(feature = "bitcoinconsensus")] + #[cfg_attr(docsrs, doc(cfg(feature = "bitcoinconsensus")))] + pub fn verify_tx(&self, tx: &Transaction) -> Result<(), chain::tx_graph::VerifyTxError> { + self.tx_graph().verify_tx(tx) + } } /// Methods to construct sync/full-scan requests for spk-based chain sources. @@ -2619,7 +2658,7 @@ macro_rules! floating_rate { /// Macro for getting a wallet for use in a doctest macro_rules! doctest_wallet { () => {{ - use $crate::bitcoin::{BlockHash, Transaction, absolute, TxOut, Network, hashes::Hash}; + use $crate::bitcoin::{transaction, BlockHash, Transaction, absolute, TxOut, Network, hashes::Hash}; use $crate::chain::{ConfirmationBlockTime, BlockId, TxGraph, tx_graph}; use $crate::{Update, KeychainKind, Wallet}; let descriptor = "tr([73c5da0a/86'/0'/0']tprv8fMn4hSKPRC1oaCPqxDb1JWtgkpeiQvZhsr8W2xuy3GEMkzoArcAWTfJxYb6Wj8XNNDWEjfYKK4wGQXh3ZUXhDF2NcnsALpWTeSwarJt7Vc/0/*)";