Skip to content

fix(utils): use max-enum-value+1 as checkHasKey boundary; reject empty keys - #22

Draft
bitgo-ai-agent-dev[bot] wants to merge 2 commits into
fix/WCN-1934-addOutput-unknownKeyVals-routing-pt1from
fix/WCN-1934-addOutput-unknownKeyVals-routing-pt2
Draft

fix(utils): use max-enum-value+1 as checkHasKey boundary; reject empty keys#22
bitgo-ai-agent-dev[bot] wants to merge 2 commits into
fix/WCN-1934-addOutput-unknownKeyVals-routing-pt1from
fix/WCN-1934-addOutput-unknownKeyVals-routing-pt2

Conversation

@bitgo-ai-agent-dev

Copy link
Copy Markdown

What

  • ts_src/lib/utils.tsgetEnumLength: rewrite to return
    max(numeric enum values) + 1 instead of counting named members.
    For OutputTypes this changes the threshold from 6 → 8 (covering
    TAP_TREE = 0x06 and TAP_BIP32_DERIVATION = 0x07); for InputTypes
    from 16 → 25 (covering all taproot input types 0x130x18).
  • ts_src/lib/utils.tscheckHasKey: add explicit guard rejecting
    zero-length key Buffers before the type-byte comparison.
  • ts_src/tests/addInputOutput.ts — add 7 new regression tests:
    • Key byte 0x08 (above OutputTypes max) is accepted as unknown.
    • Key bytes 0x06 / 0x07 (TAP_TREE / TAP_BIP32_DERIVATION) are now rejected.
    • Zero-length key is rejected on all three paths (output, input, global).

Why

Two correctness bugs in checkHasKey / getEnumLength were exposed when
the pt1 routing fix made the output path reachable:

Bug 1 — wrong threshold for non-contiguous enums (WCN-1934):
getEnumLength counted named enum members rather than returning
max-value + 1. OutputTypes has 6 named members but a highest type byte
of 0x07 (TAP_BIP32_DERIVATION), so TAP_TREE (0x06) and
TAP_BIP32_DERIVATION (0x07) bypassed the "use the typed method" guard
(since 6 < 6 and 7 < 6 are both false). A caller could silently store
these known fields in unknownKeyVals alongside their typed counterparts,
producing a PSBT with duplicate-key serialization that conformant parsers
reject. The same flaw affected the input path: all six taproot input types
(0x130x18) had values above the old threshold of 16 and also escaped.

Bug 2 — zero-length key causes silent round-trip data loss:
key[0] is undefined for a zero-length Buffer; undefined < N is false
in JavaScript, so the guard passed silently. The zero-length key was then
stored and serialized as the PSBT end-of-map separator byte (0x00),
causing silent data loss on fromBuffer re-parse with no error thrown.

Test plan

  • npm run unit — 394 tests pass (7 new regression tests added)
  • npm run format:ci — no formatting issues

Stack

This PR is part 2 of 2 in a stack. Review and merge in order:

  1. fix(psbt): route addOutput unknownKeyVals to output map #21 — primary routing fix + regression tests (base: master)
  2. #TBD — checkHasKey/getEnumLength correctness fixes (base: pt1) ← you are here

Ticket: WCN-1934

BitGo Agent added 2 commits August 7, 2026 19:54
addOutput() was calling addUnknownKeyValToInput(outputIndex, keyVal)
instead of addUnknownKeyValToOutput(outputIndex, keyVal).  This caused
two confirmed failure modes:

1. Silent corruption when outputIndex < inputs.length: the keyval was
   attached to inputs[outputIndex].unknownKeyVals and survived
   serialize/parse round-trips in the wrong (input) map.  Because the
   duplicate-check also ran against InputTypes (16 entries) instead of
   OutputTypes (6 entries), output unknown-key type bytes 6–15 were
   additionally misrejected.

2. Crash ("No input #N") when outputIndex >= inputs.length — the
   common 1-input/2-output case — making PSBT construction impossible
   whenever unknownKeyVals accompany the second output.

Both modes affect musig2/MPC coordination data carried as output
proprietary fields: the signing ceremony crashes or corrupts the PSBT,
leaving funds stuck in the shared wallet.

Fix: change line 159 to call addUnknownKeyValToOutput, matching the
upstream bip174 v2.1.1 fix.  Add regression tests covering correct
placement, round-trip survival, and the outputs>inputs case.

Ticket: WCN-1934
Session-Id: 9abd2e08-b701-4f8d-9355-06124e17bf0c
Task-Id: edef13cc-a93b-4c12-85ac-d51bcc1dfaaa
…y keys

Two correctness bugs in checkHasKey / getEnumLength exposed by the
addOutput routing fix (WCN-1934 primary fix):

1. getEnumLength counted named enum members rather than returning
   max-numeric-value + 1. For non-contiguous enums this diverges:
   OutputTypes has 6 named members but its highest type byte is 7
   (TAP_BIP32_DERIVATION = 0x07). InputTypes has 16 named members but
   its highest type byte is 24 (TAP_MERKLE_ROOT = 0x18 = 24). The old
   count-based threshold let TAP_TREE (0x06) and TAP_BIP32_DERIVATION
   (0x07) bypass the "use the typed method" guard on the output path,
   and let all six taproot input types (0x13-0x18) bypass the same guard
   on the input path. A caller could silently store these known fields in
   unknownKeyVals alongside their typed counterparts, producing a PSBT
   with duplicate-key serialization that conformant parsers reject. Fix:
   derive the threshold as max(numeric enum values) + 1.

2. A zero-length key Buffer passed checkHasKey unchecked: key[0] is
   undefined, and undefined < N is false in JavaScript, so the guard did
   not throw. The zero-length key was stored in unknownKeyVals, then
   serialized as the PSBT end-of-map separator byte (0x00), causing
   silent data loss on round-trip without any error at write time.
   Fix: explicit key.length === 0 guard at the top of checkHasKey.

Adds 7 regression tests covering:
- key byte 0x08 (above OutputTypes max) is accepted as unknown
- key bytes 0x06 / 0x07 (TAP_TREE / TAP_BIP32_DERIVATION) are rejected
- zero-length key is rejected on output, input, and global paths

Ticket: WCN-1934
Session-Id: 9abd2e08-b701-4f8d-9355-06124e17bf0c
Task-Id: edef13cc-a93b-4c12-85ac-d51bcc1dfaaa
@linear-code

linear-code Bot commented Aug 7, 2026

Copy link
Copy Markdown

WCN-1934

@ralph-bitgo
ralph-bitgo Bot force-pushed the fix/WCN-1934-addOutput-unknownKeyVals-routing-pt2 branch from e938561 to ba0680a Compare August 7, 2026 20:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant