Add fuzz target comparing consensus encoding to bitcoin 0.32
What changed, and why it matters
This commit adds a new automated test (a fuzz target) that compares how the current development version of the rust-bitcoin library encodes Bitcoin data against the previous stable release (0.32). It does not change any production code, fix a bug, or introduce a vulnerability. It only adds testing infrastructure and dependency lockfile updates needed to run that test.
No security action required. Treat as a normal test/CI addition. Reviewers may want to verify the whitelisted divergences are intentional and documented, but that is a code-quality matter, not a vulnerability.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The change introduces fuzz/fuzz_targets/bitcoin/compare_consensus_encoding.rs, wires it into fuzz/Cargo.toml and the cron-daily-fuzz.yml CI matrix, and updates Cargo-minimal.lock and Cargo-recent.lock to pull in bitcoin 0.32.8 as old_bitcoin for differential fuzzing. The fuzz target deserializes arbitrary bytes with both the old (0.32) and new (consensus_encoding) decoders and asserts that re-serialization outputs match, while explicitly whitelisting known intentional divergences (stricter amount ranges, non-ASCII command strings, length-prefix caps, zero-output transactions, removed TorV2 variant, and Inventory::Error encoding). No runtime or consensus-critical code is modified.
Changed components
fuzz testing harnessCargo lockfilesCI fuzzing workflowInspect captured patch +536 / −63
diff --git a/.github/workflows/cron-daily-fuzz.yml b/.github/workflows/cron-daily-fuzz.yml
index a3c3751a..9870e982 100644
--- a/.github/workflows/cron-daily-fuzz.yml
+++ b/.github/workflows/cron-daily-fuzz.yml
@@ -24,6 +24,7 @@ jobs:
bitcoin_arbitrary_script,
bitcoin_arbitrary_transaction,
bitcoin_arbitrary_witness,
+ bitcoin_compare_consensus_encoding,
bitcoin_deserialize_block,
bitcoin_deserialize_prefilled_transaction,
bitcoin_deserialize_psbt,
diff --git a/Cargo-minimal.lock b/Cargo-minimal.lock
index 9eba61eb..82a81330 100644
--- a/Cargo-minimal.lock
+++ b/Cargo-minimal.lock
@@ -14,12 +14,22 @@ version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6"
+[[package]]
+name = "base58ck"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c8d66485a3a2ea485c1913c4572ce0256067a5377ac8c75c4960e1cda98605f"
+dependencies = [
+ "bitcoin-internals 0.3.0",
+ "bitcoin_hashes 0.14.0",
+]
+
[[package]]
name = "base58ck"
version = "0.4.0"
dependencies = [
- "bitcoin-internals",
- "bitcoin_hashes",
+ "bitcoin-internals 0.5.0",
+ "bitcoin_hashes 0.20.0",
"hex-conservative 0.3.2",
]
@@ -45,28 +55,45 @@ dependencies = [
"serde",
]
+[[package]]
+name = "bitcoin"
+version = "0.32.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e499f9fc0407f50fe98af744ab44fa67d409f76b6772e1689ec8485eb0c0f66"
+dependencies = [
+ "base58ck 0.1.0",
+ "bech32",
+ "bitcoin-internals 0.3.0",
+ "bitcoin-io 0.1.1",
+ "bitcoin-units 0.1.0",
+ "bitcoin_hashes 0.14.0",
+ "hex-conservative 0.2.2",
+ "hex_lit",
+ "secp256k1 0.29.0",
+]
+
[[package]]
name = "bitcoin"
version = "0.33.0-beta"
dependencies = [
"arbitrary",
- "base58ck",
+ "base58ck 0.4.0",
"base64",
"bech32",
"bincode",
"bitcoin-consensus-encoding",
"bitcoin-crypto",
- "bitcoin-internals",
- "bitcoin-io",
+ "bitcoin-internals 0.5.0",
+ "bitcoin-io 0.5.0",
"bitcoin-network-kind",
"bitcoin-primitives",
"bitcoin-taproot-primitives",
- "bitcoin-units",
- "bitcoin_hashes",
+ "bitcoin-units 0.3.0",
+ "bitcoin_hashes 0.20.0",
"bitcoinconsensus",
"hex-conservative 0.3.2",
"hex-conservative 1.0.0",
- "secp256k1",
+ "secp256k1 0.32.0-beta.2",
"serde",
"serde_json",
"serde_test",
@@ -84,7 +111,7 @@ version = "0.0.0"
name = "bitcoin-consensus-encoding"
version = "0.2.0"
dependencies = [
- "bitcoin-internals",
+ "bitcoin-internals 0.5.0",
"hex-conservative 0.3.2",
]
@@ -93,13 +120,13 @@ name = "bitcoin-crypto"
version = "0.1.0"
dependencies = [
"arbitrary",
- "base58ck",
- "bitcoin-internals",
+ "base58ck 0.4.0",
+ "bitcoin-internals 0.5.0",
"bitcoin-network-kind",
- "bitcoin_hashes",
+ "bitcoin_hashes 0.20.0",
"hex-conservative 0.3.2",
"hex-conservative 1.0.0",
- "secp256k1",
+ "secp256k1 0.32.0-beta.2",
"serde",
"serde_test",
]
@@ -109,7 +136,8 @@ name = "bitcoin-fuzz"
version = "0.0.1"
dependencies = [
"arbitrary",
- "bitcoin",
+ "bitcoin 0.32.8",
+ "bitcoin 0.33.0-beta",
"bitcoin-consensus-encoding",
"bitcoin-p2p-messages",
"libfuzzer-sys",
@@ -118,6 +146,12 @@ dependencies = [
"standard_test",
]
+[[package]]
+name = "bitcoin-internals"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30bdbe14aa07b06e6cfeffc529a1f099e5fbe249524f8125358604df99a4bed2"
+
[[package]]
name = "bitcoin-internals"
version = "0.5.0"
@@ -128,13 +162,19 @@ dependencies = [
"serde_json",
]
+[[package]]
+name = "bitcoin-io"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17e5b76b88667412087beea1882980ad843b660490bbf6cce0a6cfc999c5b989"
+
[[package]]
name = "bitcoin-io"
version = "0.5.0"
dependencies = [
"bitcoin-consensus-encoding",
- "bitcoin-internals",
- "bitcoin_hashes",
+ "bitcoin-internals 0.5.0",
+ "bitcoin_hashes 0.20.0",
]
[[package]]
@@ -146,7 +186,7 @@ name = "bitcoin-network-kind"
version = "0.1.0"
dependencies = [
"arbitrary",
- "bitcoin-internals",
+ "bitcoin-internals 0.5.0",
"serde",
"serde_json",
"serde_test",
@@ -157,14 +197,14 @@ name = "bitcoin-p2p-messages"
version = "0.1.0"
dependencies = [
"arbitrary",
- "bitcoin",
+ "bitcoin 0.33.0-beta",
"bitcoin-consensus-encoding",
- "bitcoin-internals",
- "bitcoin-io",
+ "bitcoin-internals 0.5.0",
+ "bitcoin-io 0.5.0",
"bitcoin-network-kind",
"bitcoin-primitives",
- "bitcoin-units",
- "bitcoin_hashes",
+ "bitcoin-units 0.3.0",
+ "bitcoin_hashes 0.20.0",
"hex-conservative 0.3.2",
"hex-conservative 1.0.0",
"serde",
@@ -177,27 +217,42 @@ dependencies = [
"arbitrary",
"bincode",
"bitcoin-consensus-encoding",
- "bitcoin-internals",
- "bitcoin-units",
- "bitcoin_hashes",
+ "bitcoin-internals 0.5.0",
+ "bitcoin-units 0.3.0",
+ "bitcoin_hashes 0.20.0",
"hex-conservative 0.3.2",
"hex-conservative 1.0.0",
"serde",
"serde_json",
]
+[[package]]
+name = "bitcoin-private"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73290177011694f38ec25e165d0387ab7ea749a4b81cd4c80dae5988229f7a57"
+
[[package]]
name = "bitcoin-taproot-primitives"
version = "0.1.0"
dependencies = [
"arbitrary",
"bitcoin-crypto",
- "bitcoin-internals",
- "bitcoin_hashes",
- "secp256k1",
+ "bitcoin-internals 0.5.0",
+ "bitcoin_hashes 0.20.0",
+ "secp256k1 0.32.0-beta.2",
"serde",
]
+[[package]]
+name = "bitcoin-units"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d437fd727271c866d6fd5e71eb2c886437d4c97f80d89246be3189b1da4e58b"
+dependencies = [
+ "bitcoin-internals 0.3.0",
+]
+
[[package]]
name = "bitcoin-units"
version = "0.3.0"
@@ -205,19 +260,38 @@ dependencies = [
"arbitrary",
"bincode",
"bitcoin-consensus-encoding",
- "bitcoin-internals",
+ "bitcoin-internals 0.5.0",
"serde",
"serde_json",
"serde_test",
]
+[[package]]
+name = "bitcoin_hashes"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d7066118b13d4b20b23645932dfb3a81ce7e29f95726c2036fa33cd7b092501"
+dependencies = [
+ "bitcoin-private",
+]
+
+[[package]]
+name = "bitcoin_hashes"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb18c03d0db0247e147a21a6faafd5a7eb851c743db062de72018b6b7e8e4d16"
+dependencies = [
+ "bitcoin-io 0.1.1",
+ "hex-conservative 0.2.2",
+]
+
[[package]]
name = "bitcoin_hashes"
version = "0.20.0"
dependencies = [
"arbitrary",
"bitcoin-consensus-encoding",
- "bitcoin-internals",
+ "bitcoin-internals 0.5.0",
"cpufeatures",
"hex-conservative 0.3.2",
"hex-conservative 1.0.0",
@@ -286,6 +360,15 @@ dependencies = [
"windows-targets",
]
+[[package]]
+name = "hex-conservative"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f"
+dependencies = [
+ "arrayvec",
+]
+
[[package]]
name = "hex-conservative"
version = "0.3.2"
@@ -301,6 +384,12 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ee770c000993d17c185713463d5ebfbd1af9afae4c17cc295640104383bfbf0"
+[[package]]
+name = "hex_lit"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd"
+
[[package]]
name = "itoa"
version = "0.4.3"
@@ -384,6 +473,16 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c92464b447c0ee8c4fb3824ecc8383b81717b9f1e74ba2e72540aef7b9f82997"
+[[package]]
+name = "secp256k1"
+version = "0.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e0cc0f1cf93f4969faf3ea1c7d8a9faed25918d96affa959720823dfe86d4f3"
+dependencies = [
+ "bitcoin_hashes 0.12.0",
+ "secp256k1-sys 0.10.0",
+]
+
[[package]]
name = "secp256k1"
version = "0.32.0-beta.2"
@@ -392,10 +491,19 @@ checksum = "3c5fdc7d6e800869d3fd60ff857c479bf0a83ea7bf44b389e64461e844204994"
dependencies = [
"arbitrary",
"rand",
- "secp256k1-sys",
+ "secp256k1-sys 0.12.0",
"serde",
]
+[[package]]
+name = "secp256k1-sys"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1433bd67156263443f14d603720b082dd3121779323fce20cba2aa07b874bc1b"
+dependencies = [
+ "cc",
+]
+
[[package]]
name = "secp256k1-sys"
version = "0.12.0"
diff --git a/Cargo-recent.lock b/Cargo-recent.lock
index 8b9d2212..d9703507 100644
--- a/Cargo-recent.lock
+++ b/Cargo-recent.lock
@@ -14,12 +14,22 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
+[[package]]
+name = "base58ck"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c8d66485a3a2ea485c1913c4572ce0256067a5377ac8c75c4960e1cda98605f"
+dependencies = [
+ "bitcoin-internals 0.3.0",
+ "bitcoin_hashes 0.14.1",
+]
+
[[package]]
name = "base58ck"
version = "0.4.0"
dependencies = [
- "bitcoin-internals",
- "bitcoin_hashes",
+ "bitcoin-internals 0.5.0",
+ "bitcoin_hashes 0.20.0",
"hex-conservative 0.3.2",
]
@@ -44,28 +54,45 @@ dependencies = [
"serde",
]
+[[package]]
+name = "bitcoin"
+version = "0.32.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e499f9fc0407f50fe98af744ab44fa67d409f76b6772e1689ec8485eb0c0f66"
+dependencies = [
+ "base58ck 0.1.0",
+ "bech32",
+ "bitcoin-internals 0.3.0",
+ "bitcoin-io 0.1.4",
+ "bitcoin-units 0.1.3",
+ "bitcoin_hashes 0.14.1",
+ "hex-conservative 0.2.2",
+ "hex_lit",
+ "secp256k1 0.29.1",
+]
+
[[package]]
name = "bitcoin"
version = "0.33.0-beta"
dependencies = [
"arbitrary",
- "base58ck",
+ "base58ck 0.4.0",
"base64",
"bech32",
"bincode",
"bitcoin-consensus-encoding",
"bitcoin-crypto",
- "bitcoin-internals",
- "bitcoin-io",
+ "bitcoin-internals 0.5.0",
+ "bitcoin-io 0.5.0",
"bitcoin-network-kind",
"bitcoin-primitives",
"bitcoin-taproot-primitives",
- "bitcoin-units",
- "bitcoin_hashes",
+ "bitcoin-units 0.3.0",
+ "bitcoin_hashes 0.20.0",
"bitcoinconsensus",
"hex-conservative 0.3.2",
"hex-conservative 1.0.0",
- "secp256k1",
+ "secp256k1 0.32.0-beta.2",
"serde",
"serde_json",
"serde_test",
@@ -83,7 +110,7 @@ version = "0.0.0"
name = "bitcoin-consensus-encoding"
version = "0.2.0"
dependencies = [
- "bitcoin-internals",
+ "bitcoin-internals 0.5.0",
"hex-conservative 0.3.2",
]
@@ -92,13 +119,13 @@ name = "bitcoin-crypto"
version = "0.1.0"
dependencies = [
"arbitrary",
- "base58ck",
- "bitcoin-internals",
+ "base58ck 0.4.0",
+ "bitcoin-internals 0.5.0",
"bitcoin-network-kind",
- "bitcoin_hashes",
+ "bitcoin_hashes 0.20.0",
"hex-conservative 0.3.2",
"hex-conservative 1.0.0",
- "secp256k1",
+ "secp256k1 0.32.0-beta.2",
"serde",
"serde_test",
]
@@ -108,7 +135,8 @@ name = "bitcoin-fuzz"
version = "0.0.1"
dependencies = [
"arbitrary",
- "bitcoin",
+ "bitcoin 0.32.8",
+ "bitcoin 0.33.0-beta",
"bitcoin-consensus-encoding",
"bitcoin-p2p-messages",
"libfuzzer-sys",
@@ -117,6 +145,12 @@ dependencies = [
"standard_test",
]
+[[package]]
+name = "bitcoin-internals"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30bdbe14aa07b06e6cfeffc529a1f099e5fbe249524f8125358604df99a4bed2"
+
[[package]]
name = "bitcoin-internals"
version = "0.5.0"
@@ -127,13 +161,19 @@ dependencies = [
"serde_json",
]
+[[package]]
+name = "bitcoin-io"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dee39a0ee5b4095224a0cfc6bf4cc1baf0f9624b96b367e53b66d974e51d953"
+
[[package]]
name = "bitcoin-io"
version = "0.5.0"
dependencies = [
"bitcoin-consensus-encoding",
- "bitcoin-internals",
- "bitcoin_hashes",
+ "bitcoin-internals 0.5.0",
+ "bitcoin_hashes 0.20.0",
]
[[package]]
@@ -145,7 +185,7 @@ name = "bitcoin-network-kind"
version = "0.1.0"
dependencies = [
"arbitrary",
- "bitcoin-internals",
+ "bitcoin-internals 0.5.0",
"serde",
"serde_json",
"serde_test",
@@ -156,14 +196,14 @@ name = "bitcoin-p2p-messages"
version = "0.1.0"
dependencies = [
"arbitrary",
- "bitcoin",
+ "bitcoin 0.33.0-beta",
"bitcoin-consensus-encoding",
- "bitcoin-internals",
- "bitcoin-io",
+ "bitcoin-internals 0.5.0",
+ "bitcoin-io 0.5.0",
"bitcoin-network-kind",
"bitcoin-primitives",
- "bitcoin-units",
- "bitcoin_hashes",
+ "bitcoin-units 0.3.0",
+ "bitcoin_hashes 0.20.0",
"hex-conservative 0.3.2",
"hex-conservative 1.0.0",
"serde",
@@ -176,9 +216,9 @@ dependencies = [
"arbitrary",
"bincode",
"bitcoin-consensus-encoding",
- "bitcoin-internals",
- "bitcoin-units",
- "bitcoin_hashes",
+ "bitcoin-internals 0.5.0",
+ "bitcoin-units 0.3.0",
+ "bitcoin_hashes 0.20.0",
"hex-conservative 0.3.2",
"hex-conservative 1.0.0",
"serde",
@@ -191,12 +231,21 @@ version = "0.1.0"
dependencies = [
"arbitrary",
"bitcoin-crypto",
- "bitcoin-internals",
- "bitcoin_hashes",
- "secp256k1",
+ "bitcoin-internals 0.5.0",
+ "bitcoin_hashes 0.20.0",
+ "secp256k1 0.32.0-beta.2",
"serde",
]
+[[package]]
+name = "bitcoin-units"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "346568ebaab2918487cea76dd55dae13c27bb618cdb737c952e69eb2017c4118"
+dependencies = [
+ "bitcoin-internals 0.3.0",
+]
+
[[package]]
name = "bitcoin-units"
version = "0.3.0"
@@ -204,19 +253,29 @@ dependencies = [
"arbitrary",
"bincode",
"bitcoin-consensus-encoding",
- "bitcoin-internals",
+ "bitcoin-internals 0.5.0",
"serde",
"serde_json",
"serde_test",
]
+[[package]]
+name = "bitcoin_hashes"
+version = "0.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26ec84b80c482df901772e931a9a681e26a1b9ee2302edeff23cb30328745c8b"
+dependencies = [
+ "bitcoin-io 0.1.4",
+ "hex-conservative 0.2.2",
+]
+
[[package]]
name = "bitcoin_hashes"
version = "0.20.0"
dependencies = [
"arbitrary",
"bitcoin-consensus-encoding",
- "bitcoin-internals",
+ "bitcoin-internals 0.5.0",
"cpufeatures",
"hex-conservative 0.3.2",
"hex-conservative 1.0.0",
@@ -284,6 +343,15 @@ dependencies = [
"wasip2",
]
+[[package]]
+name = "hex-conservative"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f"
+dependencies = [
+ "arrayvec",
+]
+
[[package]]
name = "hex-conservative"
version = "0.3.2"
@@ -299,6 +367,12 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ee770c000993d17c185713463d5ebfbd1af9afae4c17cc295640104383bfbf0"
+[[package]]
+name = "hex_lit"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd"
+
[[package]]
name = "itoa"
version = "1.0.11"
@@ -404,6 +478,16 @@ version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
+[[package]]
+name = "secp256k1"
+version = "0.29.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113"
+dependencies = [
+ "bitcoin_hashes 0.14.1",
+ "secp256k1-sys 0.10.1",
+]
+
[[package]]
name = "secp256k1"
version = "0.32.0-beta.2"
@@ -412,10 +496,19 @@ checksum = "3c5fdc7d6e800869d3fd60ff857c479bf0a83ea7bf44b389e64461e844204994"
dependencies = [
"arbitrary",
"rand",
- "secp256k1-sys",
+ "secp256k1-sys 0.12.0",
"serde",
]
+[[package]]
+name = "secp256k1-sys"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9"
+dependencies = [
+ "cc",
+]
+
[[package]]
name = "secp256k1-sys"
version = "0.12.0"
diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml
index 395ce976..3a14b30d 100644
--- a/fuzz/Cargo.toml
+++ b/fuzz/Cargo.toml
@@ -10,7 +10,8 @@ publish = false
cargo-fuzz = true
[dependencies]
-bitcoin = { path = "../bitcoin", features = [ "serde", "arbitrary" ] }
+bitcoin = { path = "../bitcoin", version = "0.33.0-beta", features = [ "serde", "arbitrary" ] }
+old_bitcoin = { version = "0.32.8", package = "bitcoin" }
bitcoin_consensus_encoding = { path = "../consensus_encoding", package = "bitcoin-consensus-encoding" }
p2p = { path = "../p2p", package = "bitcoin-p2p-messages", features = ["arbitrary"] }
@@ -30,6 +31,15 @@ use_self = "warn"
[package.metadata.rbmt.lint]
allowed_duplicates = [
"hex-conservative",
+ "bitcoin_hashes",
+ "bitcoin-io",
+ "hex-conservative",
+ "base58ck",
+ "bitcoin",
+ "bitcoin-internals",
+ "bitcoin-units",
+ "secp256k1",
+ "secp256k1-sys",
]
[[bin]]
@@ -67,6 +77,13 @@ test = false
doc = false
bench = false
+[[bin]]
+name = "bitcoin_compare_consensus_encoding"
+path = "fuzz_targets/bitcoin/compare_consensus_encoding.rs"
+test = false
+doc = false
+bench = false
+
[[bin]]
name = "bitcoin_deserialize_block"
path = "fuzz_targets/bitcoin/deserialize_block.rs"
diff --git a/fuzz/fuzz_targets/bitcoin/compare_consensus_encoding.rs b/fuzz/fuzz_targets/bitcoin/compare_consensus_encoding.rs
new file mode 100644
index 00000000..6e2b4c03
--- /dev/null
+++ b/fuzz/fuzz_targets/bitcoin/compare_consensus_encoding.rs
@@ -0,0 +1,244 @@
+#![cfg_attr(fuzzing, no_main)]
+#![cfg_attr(not(fuzzing), allow(unused))]
+
+//! Fuzz target comparing consensus encoding between bitcoin 0.32 and master.
+//!
+//! This fuzz target compares the consensus encoding produced by `bitcoin_consensus_encoding::encode_to_vec`
+//! in master branch with `bitcoin::consensus::encode::serialize` from bitcoin 0.32 for all shared types.
+
+use libfuzzer_sys::fuzz_target;
+
+use bitcoin_consensus_encoding::{Decoder, decode_from_slice, encode_to_vec};
+
+#[cfg(not(fuzzing))]
+fn main() {}
+
+/// Walk the `std::error::Error` source chain looking for a known decoder divergence.
+///
+/// Returns `true` if the error chain contains any of the following known cases where
+/// the new decoder is stricter than the old bitcoin 0.32 decoder:
+///
+/// - `OutOfRangeError`: The new `AmountDecoder` validates the decoded value against
+/// `Amount::MAX`; the old decoder accepted any `u64`. Affects all types that encode
+/// an `Amount` anywhere in their structure (`TxOut`, `Transaction`, `Block`, …).
+///
+/// - `CommandStringDecoderError::NotAscii`: The new `CommandString` decoder rejects
+/// non-ASCII bytes; the old decoder accepted them silently.
+///
+/// - `LengthPrefixExceedsMaxError`: The new decoders cap collection lengths at
+/// `0x2_000_000`; the old decoders only rejected values above `u64::MAX`.
+///
+/// - `TransactionDecoderError` with "no outputs": The new `TransactionDecoder` rejects
+/// transactions with zero outputs; the old decoder accepted them.
+fn is_known_decoder_divergence(err: &(dyn std::error::Error + 'static)) -> bool {
+ use bitcoin_consensus_encoding::LengthPrefixExceedsMaxError;
+ use bitcoin::blockdata::transaction::TransactionDecoderError;
+ use p2p::message::error::CommandStringDecoderError;
+
+ let mut current: Option<&(dyn std::error::Error + 'static)> = Some(err);
+ while let Some(e) = current {
+ if e.downcast_ref::<bitcoin::amount::OutOfRangeError>().is_some() {
+ return true;
+ }
+ if matches!(e.downcast_ref::<CommandStringDecoderError>(), Some(CommandStringDecoderError::NotAscii)) {
+ return true;
+ }
+ if e.downcast_ref::<LengthPrefixExceedsMaxError>().is_some() {
+ return true;
+ }
+ if e.downcast_ref::<TransactionDecoderError>().is_some_and(|e| e.to_string() == "transaction has no outputs") {
+ return true;
+ }
+ current = e.source();
+ }
+ false
+}
+
+/// Helper macro to compare encoding between old and new implementations for a type.
+///
+/// Takes raw bytes, deserialises using the old bitcoin crate, then encodes with both
+/// implementations and compares the results.
+macro_rules! compare_encoding {
+ // Simple path for top-level types
+ ($data:expr, $ty:ident) => {
+ compare_encoding!($data, bitcoin::$ty, old_bitcoin::$ty);
+ };
+
+ // Types in submodules need this because we can't easily concatenate crate prefixes.
+ ($data:expr, $new_ty:ty, $old_ty:ty) => {{
+ // Try to deserialise using both bitcoin crates. Skip if it can't be deserialised
+ let old_result: Result<$old_ty, _> = old_bitcoin::consensus::encode::deserialize($data);
+ let new_result: Result<$new_ty, _> = decode_from_slice($data);
+
+ match (old_result, new_result) {
+ (Ok(old_obj), Ok(new_obj)) => {
+ // Encode with both the old and consensus_encoding implementations
+ let old_encoded = old_bitcoin::consensus::encode::serialize(&old_obj);
+ let new_encoded = encode_to_vec(&new_obj);
+ assert_eq!(old_encoded, new_encoded);
+ },
+ (Ok(old_obj), Err(ref err)) => {
+ if !is_known_decoder_divergence(err) {
+ panic!("Decoded with old decoder only: {:?}, {:?} {:?}", $data, old_obj, err);
+ }
+ },
+ (Err(err), Ok(new_obj)) => {
+ panic!("Decoded with new decoder only: {:?}, {:?} {:?}", $data, new_obj, err);
+ },
+ (_, _) => {}
+ }
+ }};
+}
+
+/// Reads a compact-size integer from the front of `data`, advancing `data` past it.
+fn read_compact_size(data: &mut &[u8]) -> Option<u64> {
+ let mut decoder = bitcoin::encoding::CompactSizeU64Decoder::new();
+ decoder.push_bytes(data).ok()?;
+ decoder.end().ok()
+}
+
+/// Returns `true` if `data`, interpreted as an `AddrV2Message`, contains a TorV2 (network_id 0x03) address.
+///
+/// `AddrV2Message` is encoded as: `time(u32) || services(compact-size u64) || AddrV2`.
+/// The AddrV2 network_id follows the variable-length services field, so a fixed offset check is wrong.
+fn addrv2_message_has_torv2(data: &[u8]) -> bool {
+ (|| -> Option<bool> {
+ let mut rest = data.get(4..)?; // skip time (u32 LE, 4 bytes)
+ read_compact_size(&mut rest)?; // skip services (compact-size u64)
+ Some(*rest.first()? == 0x03) // check AddrV2 network_id byte
+ })()
+ .unwrap_or(false)
+}
+
+/// Returns `true` if `data`, interpreted as an `AddrV2Payload` (`Vec<AddrV2Message>`),
+/// contains any TorV2 (network_id 0x03) address.
+fn addrv2_payload_has_torv2(data: &[u8]) -> bool {
+ (|| -> Option<bool> {
+ let mut rest = data;
+ let count = read_compact_size(&mut rest)?;
+ for _ in 0..count {
+ let message: p2p::address::AddrV2Message
+ = bitcoin::encoding::decode_from_slice_unbounded(&mut rest).ok()?;
+ if let p2p::address::AddrV2::Unknown(addr_type, _) = message.addr {
+ if addr_type == 0x03 {
+ return Some(true);
+ }
+ }
+ }
+ Some(false)
+ })()
+ .unwrap_or(false)
+}
+
+#[rustfmt::skip] // rustfmt butchers all of these with inconsistent newlines.
+fn do_test(data: &[u8]) {
+ compare_encoding!(data, Block);
+ compare_encoding!(data, Transaction);
+ compare_encoding!(data, TxIn);
+ compare_encoding!(data, TxOut);
+ compare_encoding!(data, OutPoint);
+ compare_encoding!(data, Witness);
+ compare_encoding!(data, Sequence);
+ compare_encoding!(data, Amount);
+ compare_encoding!(data, CompactTarget);
+ compare_encoding!(data, BlockHash);
+ compare_encoding!(data, TxMerkleNode);
+ compare_encoding!(data, WitnessMerkleNode);
+
+ compare_encoding!(data, bitcoin::block::Header, old_bitcoin::block::Header);
+ compare_encoding!(data, bitcoin::absolute::LockTime, old_bitcoin::absolute::LockTime);
+ compare_encoding!(data, bitcoin::block::Version, old_bitcoin::block::Version);
+ compare_encoding!(data, bitcoin::transaction::Version, old_bitcoin::transaction::Version);
+
+ // P2P types
+ compare_encoding!(data, p2p::ServiceFlags, old_bitcoin::p2p::ServiceFlags);
+ compare_encoding!(data, p2p::Magic, old_bitcoin::p2p::Magic);
+ compare_encoding!(data, p2p::address::Address, old_bitcoin::p2p::address::Address);
+ compare_encoding!(data, p2p::bip152::BlockTransactions, old_bitcoin::bip152::BlockTransactions);
+ compare_encoding!(data, p2p::bip152::BlockTransactionsRequest, old_bitcoin::bip152::BlockTransactionsRequest);
+ compare_encoding!(data, p2p::bip152::HeaderAndShortIds, old_bitcoin::bip152::HeaderAndShortIds);
+ compare_encoding!(data, p2p::bip152::PrefilledTransaction, old_bitcoin::bip152::PrefilledTransaction);
+ compare_encoding!(data, p2p::bip152::ShortId, old_bitcoin::bip152::ShortId);
+ compare_encoding!(data, p2p::merkle_tree::MerkleBlock, old_bitcoin::MerkleBlock);
+ compare_encoding!(data, p2p::merkle_tree::PartialMerkleTree, old_bitcoin::merkle_tree::PartialMerkleTree);
+ compare_encoding!(data, p2p::message_blockdata::GetBlocksMessage, old_bitcoin::p2p::message_blockdata::GetBlocksMessage);
+ compare_encoding!(data, p2p::message_blockdata::GetHeadersMessage, old_bitcoin::p2p::message_blockdata::GetHeadersMessage);
+ compare_encoding!(data, p2p::message_bloom::FilterAdd, old_bitcoin::p2p::message_bloom::FilterAdd);
+ compare_encoding!(data, p2p::message_bloom::FilterLoad, old_bitcoin::p2p::message_bloom::FilterLoad);
+ compare_encoding!(data, p2p::message_bloom::BloomFlags, old_bitcoin::p2p::message_bloom::BloomFlags);
+ compare_encoding!(data, p2p::message_compact_blocks::SendCmpct, old_bitcoin::p2p::message_compact_blocks::SendCmpct);
+ compare_encoding!(data, p2p::message_filter::CFHeaders, old_bitcoin::p2p::message_filter::CFHeaders);
+ compare_encoding!(data, p2p::message_filter::CFilter, old_bitcoin::p2p::message_filter::CFilter);
+ compare_encoding!(data, p2p::message_filter::CFCheckpt, old_bitcoin::p2p::message_filter::CFCheckpt);
+ compare_encoding!(data, p2p::message_filter::GetCFCheckpt, old_bitcoin::p2p::message_filter::GetCFCheckpt);
+ compare_encoding!(data, p2p::message_filter::GetCFHeaders, old_bitcoin::p2p::message_filter::GetCFHeaders);
+ compare_encoding!(data, p2p::message_filter::GetCFilters, old_bitcoin::p2p::message_filter::GetCFilters);
+ compare_encoding!(data, p2p::message_filter::FilterHash, old_bitcoin::bip158::FilterHash);
+ compare_encoding!(data, p2p::message_filter::FilterHeader, old_bitcoin::bip158::FilterHeader);
+ compare_encoding!(data, p2p::message_network::Reject, old_bitcoin::p2p::message_network::Reject);
+ compare_encoding!(data, p2p::message_network::RejectReason, old_bitcoin::p2p::message_network::RejectReason);
+ compare_encoding!(data, p2p::message_network::VersionMessage, old_bitcoin::p2p::message_network::VersionMessage);
+
+ // Types that only exist in new bitcoin, but can encode the same as some known type
+ compare_encoding!(data, p2p::ProtocolVersion, u32);
+ compare_encoding!(data, p2p::address::AddrV1Message, (u32, old_bitcoin::p2p::Address));
+ compare_encoding!(data, p2p::message::AddrPayload, Vec<(u32, old_bitcoin::p2p::Address)>);
+ compare_encoding!(data, p2p::message::NetworkHeader, (old_bitcoin::block::Header, u8));
+ compare_encoding!(data, p2p::message::Ping, u64);
+ compare_encoding!(data, p2p::message::Pong, u64);
+ compare_encoding!(data, p2p::message::V1NetworkMessage, old_bitcoin::p2p::message::RawNetworkMessage);
+ compare_encoding!(data, p2p::message_blockdata::BlockLocator, Vec<old_bitcoin::BlockHash>);
+ compare_encoding!(data, p2p::message_network::Alert, Vec<u8>);
+ compare_encoding!(data, p2p::message_network::UserAgent, String);
+ compare_encoding!(data, bitcoin::BlockHeight, u32);
+ compare_encoding!(data, bitcoin::BlockTime, u32);
+
+ // TorV2 (network_id 0x03) was removed from AddrV2 in bitcoin 0.33+. Bitcoin 0.32 decodes TorV2
+ // as a distinct variant whose encoding differs from the new crate's AddrV2::Unknown(3, ...).
+ // Skip inputs that would be decoded as TorV2 to avoid a false encoding mismatch.
+ if data.first() != Some(&0x03) {
+ compare_encoding!(data, p2p::address::AddrV2, old_bitcoin::p2p::address::AddrV2);
+ }
+ if !addrv2_message_has_torv2(data) {
+ compare_encoding!(data, p2p::address::AddrV2Message, old_bitcoin::p2p::address::AddrV2Message);
+ }
+ if !addrv2_payload_has_torv2(data) {
+ compare_encoding!(data, p2p::message::AddrV2Payload, Vec<old_bitcoin::p2p::address::AddrV2Message>);
+ }
+ // Inventory::Error (type_id=0) encodes differently between old/new bitcoin: old omits the
+ // 32-byte hash field, new includes it. Skip inputs that would decode as the Error variant.
+ if data.get(..4) != Some(&[0u8; 4]) {
+ compare_encoding!(data, p2p::message_blockdata::Inventory, old_bitcoin::p2p::message_blockdata::Inventory);
+ }
+}
+
+fuzz_target!(|data| {
+ do_test(data);
+});
+
+#[cfg(all(test, fuzzing))]
+mod tests {
+ fn extend_vec_from_hex(hex: &str, out: &mut Vec<u8>) {
+ let mut b = 0;
+ for (idx, c) in hex.as_bytes().iter().enumerate() {
+ b <<= 4;
+ match *c {
+ b'A'..=b'F' => b |= c - b'A' + 10,
+ b'a'..=b'f' => b |= c - b'a' + 10,
+ b'0'..=b'9' => b |= c - b'0',
+ _ => panic!("Bad hex"),
+ }
+ if (idx & 1) == 1 {
+ out.push(b);
+ b = 0;
+ }
+ }
+ }
+
+ #[test]
+ fn duplicate_crash() {
+ let mut a = Vec::new();
+ extend_vec_from_hex("00003cb1133bb113", &mut a);
+ super::do_test(&a);
+ }
+}
diff --git a/fuzz/generate-files.sh b/fuzz/generate-files.sh
index 1fe312e9..880a7822 100755
--- a/fuzz/generate-files.sh
+++ b/fuzz/generate-files.sh
@@ -23,6 +23,7 @@ cargo-fuzz = true
[dependencies]
bitcoin = { path = "../bitcoin", features = [ "serde", "arbitrary" ] }
+old_bitcoin = { version = "0.32.8", package = "bitcoin" }
bitcoin_consensus_encoding = { path = "../consensus_encoding", package = "bitcoin-consensus-encoding" }
p2p = { path = "../p2p", package = "bitcoin-p2p-messages", features = ["arbitrary"] }
@@ -42,6 +43,15 @@ use_self = "warn"
[package.metadata.rbmt.lint]
allowed_duplicates = [
"hex-conservative",
+ "bitcoin_hashes",
+ "bitcoin-io",
+ "hex-conservative",
+ "base58ck",
+ "bitcoin",
+ "bitcoin-internals",
+ "bitcoin-units",
+ "secp256k1",
+ "secp256k1-sys",
]
EOF
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.