From 6b1f587808254367fe8022c468789df181daf4b6 Mon Sep 17 00:00:00 2001 From: Vihiga Tyonum Date: Thu, 10 Sep 2026 23:10:32 +0100 Subject: [PATCH 1/2] test(rpc): add end-to-end perf test for getrawtx and cmp.txid - Add an end-to-end performance test for get_raw_transaction against compute_txid Result performed against a size of 500 unconfirmed mempool txs indicate that compute_txid() validation introduces a negligible performance impact, consuming just 0.946% of the time required to get_raw_transaction. --- crates/bitcoind_rpc/tests/perf_check.rs | 56 +++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 crates/bitcoind_rpc/tests/perf_check.rs diff --git a/crates/bitcoind_rpc/tests/perf_check.rs b/crates/bitcoind_rpc/tests/perf_check.rs new file mode 100644 index 0000000000..b01c3a4cf0 --- /dev/null +++ b/crates/bitcoind_rpc/tests/perf_check.rs @@ -0,0 +1,56 @@ +use bdk_bitcoind_rpc::bitcoincore_rpc::{self, RpcApi}; +use bdk_testenv::{anyhow, TestEnv}; +use bitcoin::{hashes::Hash, Address, Amount, ScriptBuf, WScriptHash}; +use std::time::Instant; + +/// Measures the marginal cost of the `compute_txid()` verification against the +/// `get_raw_transaction` fetch it accompanies, using a real regtest node. +#[allow(clippy::print_stdout)] +#[test] +fn measure_txid_check_cost() -> anyhow::Result<()> { + let env = TestEnv::new()?; + let client = bitcoincore_rpc::Client::new( + &env.bitcoind.rpc_url(), + bitcoincore_rpc::Auth::CookieFile(env.bitcoind.params.cookie_file.clone()), + )?; + env.mine_blocks(500, None)?; + + let addr = Address::from_script( + &ScriptBuf::new_p2wsh(&WScriptHash::all_zeros()), + bitcoin::Network::Regtest, + )?; + let mut txids = Vec::new(); + for _ in 0..500 { + match env.send(&addr, Amount::from_sat(1_000)) { + Ok(txid) => txids.push(txid), + Err(_) => break, + } + } + let n = txids.len(); + assert!(n > 0); + + // Fetch phase: exactly what mempool_at pays per new tx (RPC + hex + deserialize). + let t = Instant::now(); + let mut txs = Vec::with_capacity(n); + for txid in &txids { + txs.push(client.get_raw_transaction(txid, None)?); + } + let rpc = t.elapsed(); + + // The verification we added. + let t = Instant::now(); + let mut mismatches = 0usize; + for (txid, tx) in txids.iter().zip(&txs) { + if tx.compute_txid() != *txid { + mismatches += 1; + } + } + let check = t.elapsed(); + + println!("\n==== txid-check perf (n={n} mempool txs, localhost regtest) ===="); + println!("get_raw_transaction : {rpc:?} ({:.1} us/tx)", rpc.as_secs_f64() * 1e6 / n as f64); + println!("txid check : {check:?} ({:.3} us/tx)", check.as_secs_f64() * 1e6 / n as f64); + println!("check as % of fetch : {:.3}% (mismatches={mismatches})", + check.as_secs_f64() / rpc.as_secs_f64() * 100.0); + Ok(()) +} From a081086638e6e5003c4804069fc33a8052be2042 Mon Sep 17 00:00:00 2001 From: Vihiga Tyonum Date: Thu, 10 Sep 2026 23:33:41 +0100 Subject: [PATCH 2/2] test: add cpu test for serialization against compute.txid MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - add criterion benchmarks for compute_txid and deserialize across three transaction sizes. The result indicate that both operations scale linearly with the size of the transaction, but are both fast even for a large transaction. The txid check costs a stable ~0.6-2.7 µs/tx (size-dependent; ~0.8 µs typical), reproducible across runs. It's roughly 2x a bare deserialize, but both are sub-µs — and it's <1% of the full get_raw_transaction fetch, which is dominated by the RPC round-trip. --- crates/bitcoind_rpc/Cargo.toml | 5 ++ crates/bitcoind_rpc/benches/txid_check.rs | 60 +++++++++++++++++++++++ crates/bitcoind_rpc/tests/perf_check.rs | 18 +++++-- 3 files changed, 78 insertions(+), 5 deletions(-) create mode 100644 crates/bitcoind_rpc/benches/txid_check.rs diff --git a/crates/bitcoind_rpc/Cargo.toml b/crates/bitcoind_rpc/Cargo.toml index 572d1f5230..47260b9dae 100644 --- a/crates/bitcoind_rpc/Cargo.toml +++ b/crates/bitcoind_rpc/Cargo.toml @@ -24,6 +24,7 @@ bdk_core = { path = "../core", version = "0.6.1", default-features = false } bdk_bitcoind_rpc = { path = "." } bdk_testenv = { path = "../testenv" } bdk_chain = { path = "../chain" } +criterion = { version = "0.7" } [features] default = ["std"] @@ -33,3 +34,7 @@ serde = ["bitcoin/serde", "bdk_core/serde"] [[example]] name = "filter_iter" required-features = ["std"] + +[[bench]] +name = "txid_check" +harness = false diff --git a/crates/bitcoind_rpc/benches/txid_check.rs b/crates/bitcoind_rpc/benches/txid_check.rs new file mode 100644 index 0000000000..37b66bea4d --- /dev/null +++ b/crates/bitcoind_rpc/benches/txid_check.rs @@ -0,0 +1,60 @@ +use bitcoin::consensus::encode; +use bitcoin::hashes::Hash; +use bitcoin::{ + absolute::LockTime, transaction::Version, Amount, OutPoint, ScriptBuf, Sequence, Transaction, + TxIn, TxOut, Txid, Witness, +}; +use criterion::{criterion_group, criterion_main, Criterion}; +use std::hint::black_box; + +fn make_tx(n_in: usize, n_out: usize) -> Transaction { + let mut w = Witness::new(); + w.push(vec![0u8; 72]); + w.push(vec![0u8; 33]); + Transaction { + version: Version::TWO, + lock_time: LockTime::ZERO, + input: (0..n_in) + .map(|k| TxIn { + previous_output: OutPoint { + txid: Txid::from_byte_array([k as u8 + 1; 32]), + vout: 0, + }, + script_sig: ScriptBuf::new(), + sequence: Sequence::MAX, + witness: w.clone(), + }) + .collect(), + output: (0..n_out) + .map(|k| TxOut { + value: Amount::from_sat(1000 + k as u64), + script_pubkey: ScriptBuf::from(vec![0u8; 22]), + }) + .collect(), + } +} + +fn bench(c: &mut Criterion) { + for (label, i, o) in [ + ("small_1in_2out", 1, 2), + ("typical_2in_2out", 2, 2), + ("large_10in_10out", 10, 10), + ] { + let tx = make_tx(i, o); + let ser = encode::serialize(&tx); + let mut g = c.benchmark_group(label); + g.bench_function("compute_txid", |b| { + b.iter(|| black_box(black_box(&tx).compute_txid())) + }); + g.bench_function("deserialize", |b| { + b.iter(|| { + let t: Transaction = encode::deserialize(black_box(&ser)).unwrap(); + black_box(t); + }) + }); + g.finish(); + } +} + +criterion_group!(benches, bench); +criterion_main!(benches); diff --git a/crates/bitcoind_rpc/tests/perf_check.rs b/crates/bitcoind_rpc/tests/perf_check.rs index b01c3a4cf0..d06145f46f 100644 --- a/crates/bitcoind_rpc/tests/perf_check.rs +++ b/crates/bitcoind_rpc/tests/perf_check.rs @@ -23,7 +23,7 @@ fn measure_txid_check_cost() -> anyhow::Result<()> { for _ in 0..500 { match env.send(&addr, Amount::from_sat(1_000)) { Ok(txid) => txids.push(txid), - Err(_) => break, + Err(_) => break, } } let n = txids.len(); @@ -48,9 +48,17 @@ fn measure_txid_check_cost() -> anyhow::Result<()> { let check = t.elapsed(); println!("\n==== txid-check perf (n={n} mempool txs, localhost regtest) ===="); - println!("get_raw_transaction : {rpc:?} ({:.1} us/tx)", rpc.as_secs_f64() * 1e6 / n as f64); - println!("txid check : {check:?} ({:.3} us/tx)", check.as_secs_f64() * 1e6 / n as f64); - println!("check as % of fetch : {:.3}% (mismatches={mismatches})", - check.as_secs_f64() / rpc.as_secs_f64() * 100.0); + println!( + "get_raw_transaction : {rpc:?} ({:.1} us/tx)", + rpc.as_secs_f64() * 1e6 / n as f64 + ); + println!( + "txid check : {check:?} ({:.3} us/tx)", + check.as_secs_f64() * 1e6 / n as f64 + ); + println!( + "check as % of fetch : {:.3}% (mismatches={mismatches})", + check.as_secs_f64() / rpc.as_secs_f64() * 100.0 + ); Ok(()) }