Introduce fuzz targets to test roundtrip for all encodable types
What changed, and why it matters
This commit only adds new fuzz-testing scripts and helper code. It does not change any production library code, fix a bug, or alter behavior visible to users. It is a testing/infrastructure addition meant to catch encoding bugs in the future, not a security patch for an existing issue.
No security action required; treat as normal test-infrastructure commit. Continue existing fuzzing and code-review practices.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The diff adds fuzz/generate-encoding-roundtrip.sh to programmatically create per-type roundtrip fuzz targets, updates fuzz/generate-files.sh to source that script and declare a fuzz library, and creates fuzz/src/lib.rs with check_roundtrip and check_script_roundtrip helpers. No consensus-encoding implementation code is modified.
Changed components
fuzz/generate-encoding-roundtrip.shfuzz/generate-files.shfuzz/src/lib.rsInspect captured patch +222 / −0
diff --git a/fuzz/generate-encoding-roundtrip.sh b/fuzz/generate-encoding-roundtrip.sh
new file mode 100755
index 00000000..14d7190c
--- /dev/null
+++ b/fuzz/generate-encoding-roundtrip.sh
@@ -0,0 +1,179 @@
+#!/usr/bin/env bash
+
+# Generates one fuzz target file per Encodable/Decodable type under
+# fuzz_targets/bitcoin/encoding_roundtrip/.
+#
+# After running this script, re-run fuzz/generate-files.sh to update Cargo.toml
+# and the fuzz CI workflow.
+
+set -euo pipefail
+
+REPO_DIR=$(git rev-parse --show-toplevel)
+TARGET_DIR="$REPO_DIR/fuzz/fuzz_targets/bitcoin/encoding_roundtrip"
+
+mkdir -p "$TARGET_DIR"
+
+# Types tested with check_roundtrip (standard Encodable + Decodable).
+ROUNDTRIP_TYPES=(
+ "bitcoin::Amount"
+ "bitcoin::Block"
+ "bitcoin::BlockHash"
+ "bitcoin::BlockHeight"
+ "bitcoin::BlockTime"
+ "bitcoin::CompactTarget"
+ "bitcoin::OutPoint"
+ "bitcoin::Sequence"
+ "bitcoin::Transaction"
+ "bitcoin::TxIn"
+ "bitcoin::TxMerkleNode"
+ "bitcoin::TxOut"
+ "bitcoin::Witness"
+ "bitcoin::WitnessMerkleNode"
+ "bitcoin::absolute::LockTime"
+ "bitcoin::block::Header"
+ "bitcoin::block::Version"
+ "bitcoin::transaction::Version"
+ "p2p::Magic"
+ "p2p::ProtocolVersion"
+ "p2p::ServiceFlags"
+ "p2p::address::AddrV1Message"
+ "p2p::address::AddrV2"
+ "p2p::address::AddrV2Message"
+ "p2p::address::Address"
+ "p2p::bip152::BlockTransactions"
+ "p2p::bip152::BlockTransactionsRequest"
+ "p2p::bip152::HeaderAndShortIds"
+ "p2p::bip152::PrefilledTransaction"
+ "p2p::bip152::ShortId"
+ "p2p::merkle_tree::MerkleBlock"
+ "p2p::merkle_tree::PartialMerkleTree"
+ "p2p::message::AddrPayload"
+ "p2p::message::AddrV2Payload"
+ "p2p::message::CommandString"
+ "p2p::message::FeeFilter"
+ "p2p::message::HeadersMessage"
+ "p2p::message::InventoryPayload"
+ "p2p::message::NetworkHeader"
+ "p2p::message::Ping"
+ "p2p::message::Pong"
+ "p2p::message::V1MessageHeader"
+ "p2p::message::V1NetworkMessage"
+ "p2p::message_blockdata::BlockLocator"
+ "p2p::message_blockdata::GetBlocksMessage"
+ "p2p::message_blockdata::GetHeadersMessage"
+ "p2p::message_blockdata::Inventory"
+ "p2p::message_bloom::BloomFlags"
+ "p2p::message_bloom::FilterAdd"
+ "p2p::message_bloom::FilterLoad"
+ "p2p::message_compact_blocks::SendCmpct"
+ "p2p::message_filter::CFCheckpt"
+ "p2p::message_filter::CFHeaders"
+ "p2p::message_filter::CFilter"
+ "p2p::message_filter::FilterHash"
+ "p2p::message_filter::FilterHeader"
+ "p2p::message_filter::GetCFCheckpt"
+ "p2p::message_filter::GetCFHeaders"
+ "p2p::message_filter::GetCFilters"
+ "p2p::message_network::Alert"
+ "p2p::message_network::Reject"
+ "p2p::message_network::RejectReason"
+ "p2p::message_network::UserAgent"
+ "p2p::message_network::VersionMessage"
+)
+
+# Types tested with check_script_roundtrip (Buf types that Deref to their Encodable target).
+SCRIPT_ROUNDTRIP_TYPES=(
+ "bitcoin::RedeemScriptBuf"
+ "bitcoin::ScriptPubKeyBuf"
+ "bitcoin::ScriptSigBuf"
+ "bitcoin::TapScriptBuf"
+ "bitcoin::WitnessScriptBuf"
+)
+
+# Convert a Rust type path to a snake_case filename stem.
+#
+# bitcoin::Amount -> amount
+# bitcoin::block::Header -> block_header
+# bitcoin::transaction::Version -> transaction_version
+# p2p::Magic -> p2p_magic
+# p2p::bip152::HeaderAndShortIds -> p2p_bip152_header_and_short_ids
+type_to_stem() {
+ local type="$1"
+ local result
+
+ # Strip the `bitcoin::` crate prefix; leave `p2p::` intact so p2p types
+ # stay distinguishable from bitcoin types with the same short name.
+ if [[ "$type" == bitcoin::* ]]; then
+ result="${type#bitcoin::}"
+ else
+ result="$type"
+ fi
+
+ # Replace `::` with `_`.
+ result="${result//::/_}"
+
+ # CamelCase -> snake_case: insert `_` before each uppercase letter, then
+ # lowercase everything, then collapse any runs of `__` caused by the
+ # inserted underscores landing next to existing ones.
+ result=$(echo "$result" \
+ | sed 's/\([A-Z]\)/_\1/g' \
+ | tr '[:upper:]' '[:lower:]' \
+ | sed 's/__*/_/g' \
+ | sed 's/^_//')
+
+ echo "$result"
+}
+
+generate_roundtrip() {
+ local type="$1"
+ local stem filepath
+ stem="$(type_to_stem "$type")"
+ filepath="$TARGET_DIR/$stem.rs"
+
+ cat > "$filepath" <<RUST
+#![cfg_attr(fuzzing, no_main)]
+#![cfg_attr(not(fuzzing), allow(unused))]
+
+use bitcoin_fuzz::check_roundtrip;
+use libfuzzer_sys::fuzz_target;
+
+#[cfg(not(fuzzing))]
+fn main() {}
+
+fuzz_target!(|data: &[u8]| {
+ check_roundtrip::<$type>(data);
+});
+RUST
+}
+
+generate_script_roundtrip() {
+ local type="$1"
+ local stem filepath
+ stem="$(type_to_stem "$type")"
+ filepath="$TARGET_DIR/$stem.rs"
+
+ cat > "$filepath" <<RUST
+#![cfg_attr(fuzzing, no_main)]
+#![cfg_attr(not(fuzzing), allow(unused))]
+
+use bitcoin_fuzz::check_script_roundtrip;
+use libfuzzer_sys::fuzz_target;
+
+#[cfg(not(fuzzing))]
+fn main() {}
+
+fuzz_target!(|data: &[u8]| {
+ check_script_roundtrip::<$type>(data);
+});
+RUST
+}
+
+for type in "${ROUNDTRIP_TYPES[@]}"; do
+ generate_roundtrip "$type"
+done
+
+for type in "${SCRIPT_ROUNDTRIP_TYPES[@]}"; do
+ generate_script_roundtrip "$type"
+done
+
+echo "Generated $(( ${#ROUNDTRIP_TYPES[@]} + ${#SCRIPT_ROUNDTRIP_TYPES[@]} )) targets in $TARGET_DIR"
diff --git a/fuzz/generate-files.sh b/fuzz/generate-files.sh
index 1d0957b3..71b49e8f 100755
--- a/fuzz/generate-files.sh
+++ b/fuzz/generate-files.sh
@@ -7,6 +7,7 @@ REPO_DIR=$(git rev-parse --show-toplevel)
# can't find the file because of the ENV var
# shellcheck source=/dev/null
source "$REPO_DIR/fuzz/fuzz-util.sh"
+source "$REPO_DIR/fuzz/generate-encoding-roundtrip.sh"
# 1. Generate fuzz/Cargo.toml
cat > "$REPO_DIR/fuzz/Cargo.toml" <<EOF
@@ -42,6 +43,10 @@ unexpected_cfgs = { level = "deny", check-cfg = ['cfg(fuzzing)'] }
redundant_clone = "warn"
use_self = "warn"
+[lib]
+name = "bitcoin_fuzz"
+path = "src/lib.rs"
+
[package.metadata.rbmt.lint]
allowed_duplicates = [
"hex-conservative",
diff --git a/fuzz/src/lib.rs b/fuzz/src/lib.rs
new file mode 100644
index 00000000..3fe76625
--- /dev/null
+++ b/fuzz/src/lib.rs
@@ -0,0 +1,38 @@
+//! Shared utilities for fuzz targets.
+
+use std::fmt;
+
+use bitcoin_consensus_encoding::{decode_from_slice, encode_to_vec, Decode, Decoder, Encode};
+
+/// Checks roundtrip decode -> encode for a type.
+///
+/// Verifies that for all byte slices that decode successfully, the decoded value
+/// re-encodes to a slice that decodes back to the same value.
+pub fn check_roundtrip<T>(data: &[u8])
+where
+ T: Encode + Decode + PartialEq + fmt::Debug,
+ <<T as Decode>::Decoder as Decoder>::Error: fmt::Debug,
+{
+ if let Ok(base_decoded) = decode_from_slice::<T>(data) {
+ let encoded = encode_to_vec(&base_decoded);
+ let decoded = decode_from_slice::<T>(&encoded).unwrap();
+ assert_eq!(base_decoded, decoded);
+ }
+}
+
+/// Checks roundtrip decode -> encode for a script type that derefs to its encoding target.
+///
+/// Script `Buf` types (e.g. `ScriptPubKeyBuf`) implement `Encode` via `Deref` to their
+/// unsized counterpart (e.g. `ScriptPubKey`), so encoding must go through the deref.
+pub fn check_script_roundtrip<T>(data: &[u8])
+where
+ T: Decode + PartialEq + fmt::Debug + std::ops::Deref,
+ <T as std::ops::Deref>::Target: Encode,
+ <<T as Decode>::Decoder as Decoder>::Error: fmt::Debug,
+{
+ if let Ok(base_decoded) = decode_from_slice::<T>(data) {
+ let encoded = encode_to_vec(&*base_decoded);
+ let decoded = decode_from_slice::<T>(&encoded).unwrap();
+ assert_eq!(base_decoded, decoded);
+ }
+}
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.