Skip to content

Block-level malleability coverage: merkle duplicate, 64-byte tx, witness commitment #4

Description

@melvincarvalho

Companion to #3, at a different layer. #3 covers the script-verification path (interpreter.js, script.js, secp256k1.js). This covers block-level malleability, which sits above it in blocks.js / merkle construction and is not reached by that review.

Cross-referenced against Bitcoin Core HEAD (2026-07-24) and libbitcoin, both read for what to test rather than for code. Every vector below is sourceable from Core (MIT) or generable locally — nothing here requires AGPL-licensed material.

1. Merkle duplicate-hash malleability (CVE-2012-2459)

Odd node counts duplicate the last hash, so for some leaf counts an attacker can append duplicate transactions and reproduce the same merkle root.

Two distinct predicates are needed — "is this tree shape vulnerable" versus "has this block actually been mutated":

Leaves Shape Malleable? Malleated?
2 no no
3 yes
4 no
5 yes
6 yes
2 two distinct / two same no
4 two duplicated no
6 one duplicated yes
8 two duplicated yes

Plus an overflow case.

2. 64-byte transaction malleability

A transaction serialising to exactly 64 bytes is indistinguishable from two concatenated 32-byte merkle nodes and can be reinterpreted as an internal node.

  • one 64-byte tx → vulnerable
  • one 65-byte tx → not
  • two 64-byte txs → vulnerable
  • 64 + 65 → not
  • 64 + 65 + 64 → not
  • 64 + 64 + 64 → vulnerable

A scoping decision is required here, and the two reference implementations differ. Core applies the check only to blocks lacking a valid coinbase, reasoning such blocks are already invalid so it is not a consensus change; it deliberately neglects the 64-byte-coinbase case as requiring ≥224 bits of work to exploit. libbitcoin tests the property unconditionally. Either is defensible — worth choosing deliberately and asserting the choice in a test rather than inheriting it.

Reference: "Weaknesses in Bitcoin's Merkle Root Construction" (Daftuar, 2019), §3.1.

Vectors are generable, not copyable. Core documents the construction as pseudocode: loop generating random tx1, tx2; form tx3 = deserialize_tx(txid(tx1) || txid(tx2)); stop when serialized_size_without_witness(tx3) == 64. Implementing that gives us our own vectors.

3. Witness commitment malleability

Distinct from the witness script rules in #3 (which concern per-input structure). This is the block-level commitment:

  • coinbase witness stack must be exactly one item of exactly 32 bytes — else bad-witness-nonce-size
  • witness merkle commitment must match — else bad-witness-merkle-match

Note Core argues witness-tree mutation is impossible to trigger once the transaction tree forbids it, and omits an inline check there; older forks still compute it. Worth being explicit about which invariant we rely on.

Lower priority

  • compact / nBits: zero → zero; exponent < 3 left-shifts, == 3 no shift, > 3 right-shifts losing bytes above order 3; the leading-bit-set path permits a shift of 33 the unset path cannot produce; negatives always expand to zero; round-trip across half-byte and full-byte steps both ways. Core covers this in arith_uint256_tests.cpp with explicit fNegative/fOverflow flags.
  • Headers: bits exceeding maximum target; hash above vs below target; the 2-hour futuristic-timestamp boundary both sides (MAX_FUTURE_BLOCK_TIME).
  • Merkle branch: single-tx tree, position out of range, three-tx trees (values and sibling ordering), witness variant, round-trip.

Deliberately excluded

General caution: libbitcoin is a multi-chain toolkit, so parts of its test surface are altcoin-specific (ltc_scrypt_proof_of_work, ltc_time_warp_patch, and scrypt proof-of-work header cases). Verify against Core before treating any of it as a Bitcoin rule.

Sourcing

Core's script_tests.json / tx_valid / tx_invalid are MIT and also carry the CLTV/CSV timelock vectors currently uncovered — the same first step #3 recommends. Prefer them over any AGPL source, which would foreclose relicensing this project later.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions