Introduce CI action to enforce encodable fuzzing
What changed, and why it matters
This commit adds a new continuous integration (CI) check and a helper script that make sure every type that can be encoded in the current version of the library is also tested by an existing differential fuzz test against an older version of the library. It does not change any library code, user-facing behavior, or fix a bug; it only strengthens automated testing.
No security action required. This is a defensive CI hardening change. Reviewers may want to verify the EXCLUSIONS list is well-justified and that the script parsing is robust enough for future refactors.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The change introduces a GitHub Actions job named Encodable-coverage and a shell script contrib/check-encodable-coverage.sh. The script runs cargo doc, parses rustdoc’s generated trait implementor JavaScript to list all types implementing encoding::Encodable, and compares that list against the types already exercised in fuzz/fuzz_targets/bitcoin/compare_consensus_encoding.rs. If any new Encodable type is missing from the fuzz target and not in the documented EXCLUSIONS list, the CI job fails. This is a testing/quality-assurance improvement, not a code change.
Changed components
.github/workflows/rust.ymlcontrib/check-encodable-coverage.shInspect captured patch +128 / −0
diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml
index 2337d2f0..263d648e 100644
--- a/.github/workflows/rust.yml
+++ b/.github/workflows/rust.yml
@@ -239,6 +239,21 @@ jobs:
- name: "Check error type re-exports"
run: contrib/check-error-reexports.sh
+ Encodable-coverage:
+ name: Check Encodable fuzz coverage - stable toolchain
+ runs-on: ubuntu-24.04
+ permissions:
+ contents: read
+ steps:
+ - name: "Checkout repo"
+ uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
+ with:
+ persist-credentials: false
+ - name: "Select toolchain"
+ uses: dtolnay/rust-toolchain@5d458579430fc14a04a08a1e7d3694f545e91ce6 # stable
+ - name: "Check Encodable coverage"
+ run: ./contrib/check-encodable-coverage.sh
+
DiffMutants:
name: Check cargo mutants in diff - stable toolchain
runs-on: ubuntu-24.04
diff --git a/contrib/check-encodable-coverage.sh b/contrib/check-encodable-coverage.sh
new file mode 100755
index 00000000..aafec533
--- /dev/null
+++ b/contrib/check-encodable-coverage.sh
@@ -0,0 +1,113 @@
+#!/usr/bin/env bash
+#
+# Check that all types implementing encoding::Encodable are covered in the fuzz
+# test that compares encoding between old and new bitcoin crates.
+
+set -euo pipefail
+
+REPO_DIR=$(git rev-parse --show-toplevel)
+FUZZ_FILE="$REPO_DIR/fuzz/fuzz_targets/bitcoin/compare_consensus_encoding.rs"
+TRAIT_IMPL_JS="$REPO_DIR/target/doc/trait.impl/bitcoin_consensus_encoding/encode/trait.Encodable.js"
+
+# Known exclusions (types that don't exist in old_bitcoin 0.32 or are generic).
+# Add types here that have new Encodable but no old_bitcoin equivalent.
+# - CommandString has very different decoding functionality in new bitcoin.
+# - HeadersMessage has no type in 0.32 bitcoin. Vec<(Header, u8)> is not Decodable.
+# - InventoryPayload has no type in 0.32 bitcoin. Vec<Inventory> fails special case consideration.
+# - FeeFilter is a FeeRate newtype. FeeRate has no old Encodable/Decodable and is just a u64 le encoding in FeeFilter.
+EXCLUSIONS="CommandString HeadersMessage InventoryPayload FeeFilter NetworkMessage Script Validation V2NetworkMessage V1MessageHeader"
+
+main() {
+ check_required_commands
+
+ generate_docs
+ check_trait_impl_file
+
+ local new_types fuzz_types missing
+ new_types=$(extract_new_types)
+ fuzz_types=$(extract_fuzz_types)
+ missing=$(find_missing_types "$new_types" "$fuzz_types")
+
+ if [ -n "$missing" ]; then
+ echo "The following types implement encoding::Encodable but are not in the fuzz test:" >&2
+ for type in $missing; do
+ echo " - $type" >&2
+ done
+ err "Either add them to compare_consensus_encoding.rs or add to EXCLUSIONS in this script"
+ fi
+
+ echo "All encoding::Encodable types are covered (or excluded)"
+}
+
+generate_docs() {
+ echo "Generating docs to discover Encodable implementors..."
+ cargo doc --workspace --no-deps --quiet
+}
+
+check_trait_impl_file() {
+ if [ ! -f "$TRAIT_IMPL_JS" ]; then
+ err "Could not find trait implementors file at $TRAIT_IMPL_JS"
+ fi
+}
+
+# Extract type names from the rustdoc implementors JS file.
+# Split on '>' then find lines starting with TypeName< pattern, excluding "Encodable" itself.
+extract_new_types() {
+ tr '>' '\n' < "$TRAIT_IMPL_JS" \
+ | grep -oE '^[A-Z][a-zA-Z0-9_]+<' \
+ | sed 's/<$//' \
+ | grep -v '^Encodable$' \
+ | sort -u
+}
+
+# Extract types from the fuzz test file.
+extract_fuzz_types() {
+ grep -E 'compare_encoding!' "$FUZZ_FILE" \
+ | grep -v '//' \
+ | sed -E 's/.*compare_encoding!\s*\(\s*data\s*,\s*//' \
+ | sed -E 's/\s*\);.*//' \
+ | sed -E 's/,.*$//' \
+ | sed -E 's/.*:://' \
+ | grep -E '^[A-Z]' \
+ | sort -u
+}
+
+# Find types that are in new_types but not in fuzz_types or exclusions.
+find_missing_types() {
+ local new_types=$1
+ local fuzz_types=$2
+ local missing=""
+
+ for type in $new_types; do
+ if ! echo "$fuzz_types" | grep -qw "$type"; then
+ if ! echo "$EXCLUSIONS" | grep -qw "$type"; then
+ missing="$missing $type"
+ fi
+ fi
+ done
+
+ echo "$missing"
+}
+
+check_required_commands() {
+ need_cmd grep
+ need_cmd sed
+ need_cmd tr
+}
+
+err() {
+ echo "ERROR: $1" >&2
+ exit 1
+}
+
+need_cmd() {
+ if ! command -v "$1" > /dev/null 2>&1; then
+ err "need '$1' (command not found)"
+ fi
+}
+
+#
+# Main script
+#
+main "$@"
+exit 0
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.