Skip to content

fix(wasm-utxo): authenticate legacy PSBT prevout UTXO data - #413

Closed
ralph-bitgo[bot] wants to merge 1 commit into
masterfrom
WCN-1907-authenticate-legacy-prevouts
Closed

ralph-bitgo[bot] wants to merge 1 commit into
masterfrom
WCN-1907-authenticate-legacy-prevouts

Conversation

@ralph-bitgo

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

Copy link
Copy Markdown

What

  • get_output_script_and_value_for_network (new, in
    psbt_wallet_input.rs) now authenticates PSBT input UTXO data:
    non_witness_utxo.compute_txid() must equal the prevout txid, and
    witness_utxo must match the referenced non_witness_utxo output
    when both are present. Dash and Zcash follow network-specific txid
    rules and are validated separately.
  • Legacy P2SH and p2shP2pk inputs now require the full previous
    transaction on networks whose sighash does not commit the input
    amount (BTC/LTC/DOGE/DASH/Pearl, per
    Network::requires_prev_tx_for_legacy_input), enforced in
    add_wallet_input_to_psbt,
    add_replay_protection_input_to_psbt, add_input_at_index,
    BitGoPsbt::deserialize, hydration (HydrationUnspent gains an
    optional prevTx field, wired through the JS/WASM boundary) and the
    descriptor input builder (add_input_with_descriptor).
  • The same validation runs before signing, parsing and fee-based
    extraction: sign, sign_with_privkey, sign_all_with_xpriv,
    sign_single_input_with_xpriv, musig2 context creation,
    parse_transaction_with_wallet_keys, and the extract_tx* family,
    so the absurd-fee guard and fee display consume authenticated values.
  • Dash PSBT deserialization verifies the preserved raw prevout bytes
    against the prevout txid (compute_dash_txid).
  • Nested-segwit P2SH inputs stay usable with witness-only UTXOs (via
    redeem_script/final_script_sig shape checks); forged metadata that
    merely resembles nested segwit does not bypass the requirement.
  • Tests: Rust regressions in psbt_wallet_input.rs and dash_psbt.rs;
    new TypeScript suite test/fixedScript/legacyUtxoAuthentication.ts;
    AcidTest and the fixed-script suites now build synthetic previous
    transactions whose computed txids match their outpoints.

Why

WCN-1907:
the legacy sighash commits only the scriptCode, not the input amount,
so a fabricated non_witness_utxo from a compromised UTXO feed could
inflate the displayed input value, pass the absurd-fee guard, and
produce valid signatures that burn the difference to miner fees.
BIP174 requires signers to verify the prevout txid; the repo's own CLI
already did (cli/src/psbt/add_input.rs), but no library signing path
did. Value-committing networks (Zcash ZIP-243, BCH-family FORKID,
segwit, taproot) are unaffected and keep witness-only inputs.

Test plan

  • Regenerate the checked-in AcidTest-generated PSBT fixtures
    (test/fixtures/fixed-script/, cli/test/fixtures/...,
    packages/webui/src/fixtures/...) — their synthetic prevouts
    predate this change and now fail txid authentication.
  • cd packages/wasm-utxo && cargo test (new
    prevout_authentication_tests, test_dash_psbt_*, and existing
    suites).
  • npm test in packages/wasm-utxo (new
    legacyUtxoAuthentication.ts + updated fixedScript suites).

Note: this sandbox had no Rust/Node toolchain, so the diff is
unverified: it compiles unproven and fixtures are not yet regenerated.
Draft is pushed for toolchain-backed CI and follow-up fixture
regeneration.

Ticket: WCN-1907

Legacy P2SH inputs on Bitcoin, Litecoin, Dogecoin, Dash and Pearl do
not commit the input amount in their sighash, so a fabricated
non_witness_utxo (or a lying witness_utxo) supplied by a compromised
UTXO feed could inflate the displayed input value, pass the absurd-fee
guard and hide a large miner fee behind fully valid signatures.

- Check non_witness_utxo.compute_txid() against the prevout txid and
  cross-check witness_utxo against the referenced prevout output in
  get_output_script_and_value_for_network (Dash raw bytes and Zcash
  transparent txids follow network-specific rules and are validated
  separately).
- Require the full previous transaction for legacy P2SH and p2shP2pk
  inputs on non-value-committing networks in add_wallet_input_to_psbt,
  add_replay_protection_input_to_psbt, add_input_at_index,
  BitGoPsbt::deserialize, hydration (HydrationUnspent gains an
  optional prevTx field) and the descriptor input builder.
- Enforce the same validation before signing, parsing and fee-based
  extraction: sign, sign_with_privkey, sign_all_with_xpriv, musig2
  context creation, parse_transaction_with_wallet_keys and the
  extract_tx* family.
- Dash PSBT deserialization now verifies the preserved raw prevout
  bytes against the prevout txid.
- Update AcidTest and fixed-script tests to build synthetic previous
  transactions whose computed txids match their outpoints, and add
  Rust and TypeScript regressions for txid mismatch, witness/non-
  witness disagreement, forged nested-segwit metadata and missing
  prevTx rejection.

Ticket: WCN-1907
Session-Id: f3c6e0a8-c064-4f47-8c83-f52c665fe185
Task-Id: 078b961f-836b-4c49-a8ce-c08e0aacfc38
Requested-By: David Kaplan <davidkaplan@bitgo.com>
@linear-code

linear-code Bot commented Sep 29, 2026

Copy link
Copy Markdown

WCN-1907

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.

4 participants