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.
///
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(_))
+ ));
+}
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/*)";