Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(chain): Add verify_tx for TxGraph #1339

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/chain/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
36 changes: 36 additions & 0 deletions crates/chain/src/tx_graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -416,8 +416,44 @@ impl<A> TxGraph<A> {
.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<TxOut> { 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<A: Clone + Ord> TxGraph<A> {
/// Creates an iterator that filters and maps ancestor transactions.
///
Expand Down
8 changes: 8 additions & 0 deletions crates/chain/tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ macro_rules! block_id {
}};
}

/// Returns `Vec<u8>` from a hex `&str`
#[allow(unused_macros)]
macro_rules! hex {
($hex:literal) => {
<Vec<u8> as bitcoin::hex::FromHex>::from_hex($hex)
};
}

#[allow(unused_macros)]
macro_rules! h {
($index:literal) => {{
Expand Down
54 changes: 54 additions & 0 deletions crates/chain/tests/test_tx_graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<BlockId>::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(_))
));
}
1 change: 1 addition & 0 deletions crates/wallet/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
41 changes: 40 additions & 1 deletion crates/wallet/src/wallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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/*)";
Expand Down
Loading