Skip to content

feat(wasm-utxo): add sighash policy primitives to BitGoPsbt - #416

Merged
OttoAllmendinger merged 2 commits into
masterfrom
WCN-1994-sighash-policy-primitives
Sep 30, 2026
Merged

OttoAllmendinger merged 2 commits into
masterfrom
WCN-1994-sighash-policy-primitives

Conversation

@ralph-bitgo

@ralph-bitgo ralph-bitgo Bot commented Sep 29, 2026

Copy link
Copy Markdown

What

  • Adds get_input_sighash_types() to the BitGoPsbt wasm class (and the TypeScript wrapper as getInputSighashTypes()): returns each input's declared BIP-174 PSBT_IN_SIGHASH_TYPE, undefined/null when absent (signer default applies).

  • Adds assert_sighash_all_policy() (TS: assertSighashAllPolicy()): asserts the policy required when signing externally supplied PSBTs. For every input it rejects

    • a declared sighash type that does not commit to the entire transaction, and
    • any signature already present (ECDSA partial signatures, Taproot key-path/script-path signatures) under such a type.

    Accepted types are SIGHASH_ALL (0x01), SIGHASH_ALL|SIGHASH_FORKID (0x41) on BCH-family coins, and SIGHASH_DEFAULT (0x00)/SIGHASH_ALL on Taproot inputs; an absent type uses the signer default. MuSig2 partial signatures carry no sighash byte and are governed by the declared type checked above.

  • Rust unit tests cover the accepted/rejected sets per network (incl. BCH FORKID and Taproot DEFAULT) and the policy assert on declared types, injected signatures, and real signed wallet PSBTs; mocha tests in test/fixedScript/sighashPolicy.ts cover the wasm/TS surface end-to-end, including real signing runs for p2wsh/BCH-p2sh/Taproot and the output-swap invalidation property.

Why

The signer honors the per-input PSBT_IN_SIGHASH_TYPE embedded in a PSBT. A foreign PSBT can request SIGHASH_NONE/SINGLE/ANYONECANPAY, producing a signature that does not bind the signer to the transaction outputs — AnchorWatch demonstrated an output-swap drain of recovered funds this way (SIG-001 / WCN-1994). Consumers that ingest foreign PSBTs need one shared, network-aware primitive; per-app reimplementations miss the BCH FORKID and Taproot rules (a hardcoded 0x01-only check rejects every BCH PSBT).

Test plan

  • cargo test --lib — 620 passed (5 new tests)
  • cargo clippy --all-targets --all-features -- -D warnings, cargo fmt --check — clean
  • tsc --noEmit -p tsconfig.test.json — clean
  • Full mocha suite (test/**/*.ts) — 1572 passing (14 new tests)
  • CI (test:wasm-pack chrome/node legs run in CI; not runnable in this sandbox)

Stack

This PR is part 1 of 3 in the WCN-1994 stack. Review and merge in order:

  1. This PR — wasm-utxo primitives (publishes as @bitgo/wasm-utxo ≥ 5.6.0)
  2. BitGoJS @bitgo/abstract-utxo — signExternalPsbt / assertExternalPsbtSighashPolicy built on these primitives (publishes as ≥ 13.3.0)
  3. wallet-recovery-wizard PR #753 — WRW "Sign Unsigned PSBT" flow switches to the library helpers

Ticket: WCN-1994

Add get_input_sighash_types() and assert_sighash_all_policy() to the
BitGoPsbt wasm class and its TypeScript wrapper.

assert_sighash_all_policy enforces the policy required when signing
externally supplied PSBTs: every input's declared sighash type and every
signature already present in the PSBT must commit to the entire
transaction (SIGHASH_ALL 0x01, SIGHASH_ALL|FORKID 0x41 on BCH-family
coins, SIGHASH_DEFAULT 0x00/SIGHASH_ALL on Taproot inputs). An absent
sighash type is accepted and uses the signer default.

get_input_sighash_types exposes each input's declared BIP-174 sighash
type so callers can display it or apply their own policies.

Why: signers honor the per-input PSBT_IN_SIGHASH_TYPE field, and
SIGHASH_NONE/SINGLE/ANYONECANPAY signatures do not bind the signer to
the transaction outputs. Callers that ingest foreign PSBTs (wallet
recovery tools) need one shared, network-aware primitive instead of
per-app reimplementations that miss the BCH FORKID and Taproot rules.

Ticket: WCN-1994
Session-Id: 5c05e047-31d0-43fa-91ab-f1c595b37850
Task-Id: f7cdbdd7-f9f0-4164-a69b-0c7e58e609a8
Add mocha coverage for getInputSighashTypes and
assertSighashAllPolicy: declared NONE/SINGLE/ANYONECANPAY and combined
modes are rejected before signing, absent types use the signer default,
BCH-family coins require SIGHASH_ALL|FORKID, Taproot inputs accept
SIGHASH_DEFAULT, and real signing runs (p2wsh, BCH p2sh, Taproot) keep
the policy satisfied while binding signatures to the outputs (an output
swap invalidates every signature).

Ticket: WCN-1994
Session-Id: 5c05e047-31d0-43fa-91ab-f1c595b37850
Task-Id: f7cdbdd7-f9f0-4164-a69b-0c7e58e609a8
@linear-code

linear-code Bot commented Sep 29, 2026

Copy link
Copy Markdown

WCN-1994

@OttoAllmendinger
OttoAllmendinger merged commit 17abd2c into master Sep 30, 2026
14 checks passed
@OttoAllmendinger
OttoAllmendinger deleted the WCN-1994-sighash-policy-primitives branch September 30, 2026 15:12
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.

3 participants