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.
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 inblocks.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":
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.
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; formtx3 = deserialize_tx(txid(tx1) || txid(tx2)); stop whenserialized_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:
bad-witness-nonce-sizebad-witness-merkle-matchNote 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
arith_uint256_tests.cppwith explicitfNegative/fOverflowflags.MAX_FUTURE_BLOCK_TIME).Deliberately excluded
ltc_retarget_overflow_patch— a Litecoin flag,falseon every Bitcoin network, which is why Core has no counterpart. Not a Bitcoin rule; do not implement.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_invalidare 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.