Merge rust-bitcoin/rust-bitcoin#6736: benches: fix encoding use, add CI check, and standardize on criterion
What changed, and why it matters
This change is purely a cleanup of benchmark code. It moves the last remaining old-style benchmark into a shared benchmark package, switches the project to use the standard Criterion benchmarking library, and adds a CI check so benchmarks stay buildable. No user-facing code, cryptographic logic, or network behavior is changed.
No security action needed. This is a routine refactoring of benchmark infrastructure.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The merge commit migrates remaining #[bench] benchmarks in base58/src/lib.rs to the separate benches/ Criterion workspace, updates block/transaction/witness benchmarks to use the encoding crate helpers (decode_from_slice, encode_to_vec, encode_to_writer, etc.), removes the cfg(bench) conditional and feature(test) usage from the main library, and adds a stable-toolchain CI step that checks the benches package compiles. The diff shows only benchmark and build-configuration changes; no runtime library code is modified.
Changed components
benches/base58/base58ck.rsbenches/bitcoin/block.rsbenches/bitcoin/transaction.rsbenches/bitcoin/witness.rsbenches/Cargo.tomlbenches/Cargo.lock.github/workflows/rust.ymlCONTRIBUTING.mdCargo.tomlbase58/src/lib.rsInspect captured patch +70 / −67
### .github/workflows/rust.yml
@@ -86,10 +86,9 @@ jobs:
with:
persist-credentials: false
- uses: ./.github/actions/setup-rbmt
- - name: "Run bench"
- env:
- RUSTFLAGS: --cfg=bench
- run: cargo rbmt run --toolchain nightly -- bench
+ - name: "Check criterion benches"
+ working-directory: benches
+ run: cargo rbmt run --toolchain stable --lockfile existing -- bench -- --test
Prerelease:
runs-on: ubuntu-24.04
### CONTRIBUTING.md
@@ -322,8 +322,9 @@ Run as for any other Rust project `cargo test --all-features`.
### Benchmarks
-We use a custom Rust compiler configuration conditional to guard the benchmark code. To run the
-benchmarks use: `RUSTFLAGS='--cfg=bench' cargo +nightly bench`.
+Benchmarks use [criterion](https://github.com/criterion-rs/criterion.rs) and live in the separate
+`benches/` package (its own workspace, excluded from the root workspace). See
+[`benches/README.md`](./benches/README.md) for details.
### Mutation tests
### Cargo.toml
@@ -9,7 +9,7 @@ rbmt.toolchains.nightly = "nightly-2026-07-02"
rbmt.toolchains.stable = "1.97.1"
[workspace.lints.rust]
-unexpected_cfgs = { level = "deny", check-cfg = ['cfg(bench)', 'cfg(chacha20_poly1305_fuzz)', 'cfg(fuzzing)', 'cfg(hashes_fuzz)', 'cfg(kani)'] }
+unexpected_cfgs = { level = "deny", check-cfg = ['cfg(chacha20_poly1305_fuzz)', 'cfg(fuzzing)', 'cfg(hashes_fuzz)', 'cfg(kani)'] }
[workspace.lints.clippy]
# Exclude lints we don't think are valuable.
### base58/src/lib.rs
@@ -37,24 +37,18 @@
//! ```
#![no_std]
-// Experimental features we need.
-#![cfg_attr(bench, feature(test))]
// Coding conventions.
#![warn(missing_docs)]
#![warn(deprecated_in_future)]
#![doc(test(attr(warn(unused))))]
-// Instead of littering the codebase for non-fuzzing and bench code just globally allow.
+// Instead of littering the codebase for non-fuzzing code just globally allow.
#![cfg_attr(fuzzing, allow(dead_code, unused_imports))]
-#![cfg_attr(bench, allow(dead_code, unused_imports))]
// Exclude lints we don't think are valuable.
#![allow(clippy::incompatible_msrv)] // Has FPs and we're testing it which is more reliable anyway.
#[cfg(feature = "alloc")]
extern crate alloc;
-#[cfg(bench)]
-extern crate test;
-
#[cfg(feature = "std")]
extern crate std;
@@ -680,28 +674,3 @@ mod tests {
);
}
}
-
-#[cfg(bench)]
-mod benches {
- use test::{black_box, Bencher};
-
- #[bench]
- pub fn bench_encode_check_50(bh: &mut Bencher) {
- let data: alloc::vec::Vec<_> = (0u8..50).collect();
-
- bh.iter(|| {
- let r = super::Base58CkString::encode_unbounded(&data);
- black_box(r.as_str());
- });
- }
-
- #[bench]
- pub fn bench_encode_check_xpub(bh: &mut Bencher) {
- let data: alloc::vec::Vec<_> = (0u8..78).collect(); // length of xpub
-
- bh.iter(|| {
- let r = super::Base58CkString::encode_unbounded(&data);
- black_box(r.as_str());
- });
- }
-}
### benches/Cargo.lock
@@ -97,6 +97,7 @@ dependencies = [
name = "bitcoin-benches"
version = "0.1.0"
dependencies = [
+ "base58ck",
"bitcoin",
"bitcoin-consensus-encoding",
"bitcoin_hashes",
### benches/Cargo.toml
@@ -6,6 +6,7 @@ description = "Criterion benchmarks for rust-bitcoin"
edition = "2021"
[dependencies]
+base58ck = { path = "../base58" }
bitcoin = { path = "../bitcoin", default-features = false, features = ["std"] }
bitcoin_hashes = { path = "../hashes" }
chacha20-poly1305 = { path = "../chacha20_poly1305" }
@@ -17,6 +18,14 @@ hex = { package = "hex-conservative", version = "1.1.0" }
[lints.clippy]
use_self = "warn"
+[workspace.metadata.rbmt.toolchains]
+stable = "1.97.1"
+
+[[bench]]
+name = "base58ck"
+path = "base58/base58ck.rs"
+harness = false
+
[[bench]]
name = "block"
path = "bitcoin/block.rs"
### benches/base58/base58ck.rs
@@ -0,0 +1,31 @@
+// SPDX-License-Identifier: CC0-1.0
+
+use std::hint::black_box;
+
+use base58ck::Base58CkString;
+use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
+
+fn bench_encode_check(c: &mut Criterion) {
+ let mut g = c.benchmark_group("base58ck");
+
+ g.bench_function(BenchmarkId::new("encode_check", "50_bytes"), |b| {
+ let data: Vec<u8> = (0u8..50).collect();
+ b.iter(|| {
+ let r = Base58CkString::encode_unbounded(black_box(&data));
+ black_box(r.as_str());
+ });
+ });
+
+ g.bench_function(BenchmarkId::new("encode_check", "xpub_78_bytes"), |b| {
+ let data: Vec<u8> = (0u8..78).collect(); // length of xpub
+ b.iter(|| {
+ let r = Base58CkString::encode_unbounded(black_box(&data));
+ black_box(r.as_str());
+ });
+ });
+
+ g.finish();
+}
+
+criterion_group!(benches, bench_encode_check);
+criterion_main!(benches);
### benches/bitcoin/block.rs
@@ -5,12 +5,10 @@ use std::hint::black_box;
use bitcoin::block::Header;
use bitcoin::blockdata::block::Block;
-use bitcoin::consensus::{deserialize, serialize, Decodable, Encodable};
-use bitcoin::io::sink;
use bitcoin::script::{ScriptPubKeyBuf, ScriptSigBuf};
use bitcoin::transaction::{OutPoint, Transaction, TxIn, TxOut, Version};
use bitcoin::{Amount, BlockTime, CompactTarget, Sequence, TxMerkleNode, Witness};
-use encoding::decode_from_slice;
+use encoding::{decode_from_read, decode_from_slice, encode_to_vec, encode_to_writer};
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
@@ -54,13 +52,13 @@ fn build_test_block(num_tx: usize) -> Vec<u8> {
nonce: 0,
};
- serialize(&Block::new_unchecked(header, txs))
+ encode_to_vec(&Block::new_unchecked(header, txs))
}
fn bench_block(c: &mut Criterion) {
let raw_block = include_bytes!("../../bitcoin/tests/data/mainnet_block_000000000000000000000c835b2adcaedc20fdf6ee440009c249452c726dafae.raw");
assert_eq!(raw_block.len(), 1_381_836);
- let block: Block = deserialize(&raw_block[..]).unwrap();
+ let block: Block = decode_from_slice(&raw_block[..]).unwrap();
let mut g = c.benchmark_group("block");
g.throughput(Throughput::Bytes(raw_block.len() as u64));
@@ -69,31 +67,29 @@ fn bench_block(c: &mut Criterion) {
g.bench_function(BenchmarkId::new("stream_reader", "big"), |b| {
let big_block = black_box(raw_block.as_ref());
b.iter(|| {
- let mut reader = big_block;
- let blk = Block::consensus_decode(&mut reader).unwrap();
+ let blk: Block = decode_from_read(big_block).unwrap();
black_box(blk);
});
});
g.bench_function(BenchmarkId::new("serialize", "big"), |b| {
let mut data = Vec::with_capacity(raw_block.len());
b.iter(|| {
- let result = block.consensus_encode(&mut data);
- black_box(&result);
+ encode_to_writer(&block, &mut data).unwrap();
+ black_box(&data);
data.clear();
});
});
g.bench_function(BenchmarkId::new("serialize_logic", "big"), |b| {
b.iter(|| {
- let size = block.consensus_encode(&mut sink());
- let _ = black_box(size);
+ encode_to_writer(&block, std::io::sink()).unwrap();
});
});
g.bench_function(BenchmarkId::new("deserialize", "big"), |b| {
b.iter(|| {
- let blk: Block = deserialize(&raw_block[..]).unwrap();
+ let blk: Block = decode_from_slice(&raw_block[..]).unwrap();
black_box(blk);
});
});
### benches/bitcoin/transaction.rs
@@ -4,8 +4,7 @@ use std::hint::black_box;
use bitcoin::blockdata::transaction::Transaction;
use bitcoin::blockdata::transaction::TransactionExt as _; // for total_size()
-use bitcoin::consensus::{encode, Encodable};
-use bitcoin::io::sink;
+use encoding::{decode_from_hex, decode_from_slice, encode_to_writer};
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
const SOME_TX: &str = "0100000001a15d57094aa7a21a28cb20b59aab8fc7d1149a3bdbcddba9c622e4f5f6a99ece010000006c493046022100f93bb0e7d8db7bd46e40132d1f8242026e045f03a0efe71bbb8e3f475e970d790221009337cd7f1f929f00cc6ff01f03729b069a7c21b59b1736ddfee5db5946c5da8c0121033b9b137ee87d5a812d6f506efdd37f0affa7ffc310711c06c7f3e097c9447c52ffffffff0100e1f505000000001976a9140389035a9225b3839e2bbf32d826a1e222031fd888ac00000000";
@@ -14,41 +13,40 @@ fn bench_tx(c: &mut Criterion) {
let mut g = c.benchmark_group("transaction");
g.bench_function(BenchmarkId::new("size", "some"), |b| {
- let mut tx: Transaction = encode::deserialize_hex(SOME_TX).unwrap();
+ let mut tx: Transaction = decode_from_hex(SOME_TX).unwrap();
b.iter(|| {
black_box(black_box(&mut tx).total_size());
});
});
g.bench_function(BenchmarkId::new("serialize", "some"), |b| {
- let tx: Transaction = encode::deserialize_hex(SOME_TX).unwrap();
+ let tx: Transaction = decode_from_hex(SOME_TX).unwrap();
let mut data = Vec::with_capacity(SOME_TX.len() / 2);
b.iter(|| {
- let result = tx.consensus_encode(&mut data);
- black_box(&result);
+ encode_to_writer(&tx, &mut data).unwrap();
+ black_box(&data);
data.clear();
});
});
g.bench_function(BenchmarkId::new("serialize_logic", "some"), |b| {
- let tx: Transaction = encode::deserialize_hex(SOME_TX).unwrap();
+ let tx: Transaction = decode_from_hex(SOME_TX).unwrap();
b.iter(|| {
- let size = tx.consensus_encode(&mut sink());
- let _ = black_box(size);
+ encode_to_writer(&tx, std::io::sink()).unwrap();
});
});
g.bench_function(BenchmarkId::new("deserialize", "raw_bytes"), |b| {
let raw_tx = hex::hex!(SOME_TX);
b.iter(|| {
- let tx: Transaction = encode::deserialize(&raw_tx).unwrap();
+ let tx: Transaction = decode_from_slice(&raw_tx).unwrap();
black_box(tx);
});
});
g.bench_function(BenchmarkId::new("deserialize_hex", "string"), |b| {
b.iter(|| {
- let tx: Transaction = encode::deserialize_hex(SOME_TX).unwrap();
+ let tx: Transaction = decode_from_hex(SOME_TX).unwrap();
black_box(tx);
});
});
### benches/bitcoin/witness.rs
@@ -3,9 +3,8 @@
use std::hint::black_box;
use bitcoin::blockdata::witness::{Witness, WitnessDecoder};
-use bitcoin::consensus::encode;
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
-use encoding::{decode_from_slice, Decoder as _};
+use encoding::{decode_from_slice, encode_to_vec, Decoder as _};
fn bench_witness(c: &mut Criterion) {
let mut g = c.benchmark_group("witness");
@@ -29,7 +28,7 @@ fn bench_witness(c: &mut Criterion) {
// Many small elements (100 and 1000).
for count in [100, 1000] {
let witness = Witness::from_slice(&vec![vec![0u8; 4]; count]);
- let bytes = encode::serialize(&witness);
+ let bytes = encode_to_vec(&witness);
g.bench_with_input(BenchmarkId::new("many_elements", count), &bytes, |b, bytes| {
b.iter(|| black_box(decode_from_slice::<Witness>(bytes).unwrap()));
});
@@ -38,15 +37,15 @@ fn bench_witness(c: &mut Criterion) {
// Single element of different sizes.
for size in [64, 256, 1024, 2048, 4096] {
let witness = Witness::from_slice(&[vec![0u8; size]]);
- let bytes = encode::serialize(&witness);
+ let bytes = encode_to_vec(&witness);
g.bench_with_input(BenchmarkId::new("one_element", size), &bytes, |b, bytes| {
b.iter(|| black_box(decode_from_slice::<Witness>(bytes).unwrap()));
});
}
// 64 KB element fed in different chunk sizes.
let witness = Witness::from_slice(&[vec![0u8; 65536]]);
- let bytes = encode::serialize(&witness);
+ let bytes = encode_to_vec(&witness);
for chunk in [1, 64, 256, 1024, 4096] {
g.bench_with_input(BenchmarkId::new("chunk_64kb", chunk), &bytes, |b, bytes| {
b.iter(|| black_box(decode_chunked(bytes, chunk)));Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.