While reading bip-0346/ref-impl/src/main.rs I found that the two-byte form of an individual
in/output index can never work.
In parse_inout_selection, individual selection mode, an index byte whose highest bit is set means
the index spans two bytes. The composition is:
(((first & (1 << 7)) as usize) << 8) + next_byte as usize
Inside that branch first & 0x80 is always 0x80 — that is exactly the condition that got us
there. So the mask keeps the flag bit and discards the seven value bits, and every two-byte index
evaluates to 0x8000 + next_byte, i.e. 32768 or more.
Concretely, on the existing test transaction (4 inputs), selecting absolute input index 1:
- single-byte encoding, index byte
0x01→Ok(0x9973075dd63b8af73e73c9881eff129aa2051e1ce9d342d052f874f9bdd75b16) - two-byte encoding of the same index, bytes
0x80 0x01→Err("selected index out of bounds")
Both spell the same selection, so they must produce the same TxHash. In absolute mode the bug makes
every two-byte index out of bounds, so valid selectors are rejected and the whole encoding is dead.
In relative mode it is quieter and worse: read_i15 masks 0x7fff, which erases the 0x8000 that
was just added, so an offset of 256 or more collapses onto its low 8 bits and commits to a
different input than the one the selector names, with no error.
The BIP text settles what the code should do — "If that byte's highest bit is set to 1, the
remaining 7 bits, together with the next byte's 8 bits represent the absolute index to select"
(and the same wording for the relative index) — and the Rationale states the encoding allows
"absolute indices up to ~32k" and relative indices "from -16382 to +16383". Those ranges need 15
value bits; as written, no index below 32768 is reachable through the two-byte form. The leading
mode, twenty lines above, already composes its two-byte count the intended way, with
selection & TXFS_INOUT_SELECTION_MASK.
So the fix is to mask the flag bit off instead of keeping only it.
I added a test asserting that both encodings of the same absolute index yield the same TxHash; it
fails before the change and passes after. Nothing else moves: generate_vectors only exercises
single-byte indices (as its own TODO notes, testing index size needs more than 32 in/outputs), so
the patched branch was unreachable, and running the binary regenerates txhash_vectors.json
byte for byte identical (md5 56b8499b66d20582ceba6a18544a0145 before and after).
Found by a defect-hunting pipeline I build and run (Dev-next-gen), using Claude Code with Anthropic's Claude Opus 5.
Nothing this pipeline produces is submitted without human approval. The entire pull request — the code change, the tests and this description — was reviewed by a human before the pipeline was allowed to submit it.