Skip to content

refactor(abstract-utxo): decode descriptor PSBTs directly to wasm Psbt#8824

Merged
OttoAllmendinger merged 3 commits into
masterfrom
otto/t1-3400-fix-descriptor-sign
May 21, 2026
Merged

refactor(abstract-utxo): decode descriptor PSBTs directly to wasm Psbt#8824
OttoAllmendinger merged 3 commits into
masterfrom
otto/t1-3400-fix-descriptor-sign

Conversation

@OttoAllmendinger
Copy link
Copy Markdown
Contributor

@OttoAllmendinger OttoAllmendinger commented May 21, 2026

Summary

Fixes T1-3400 — descriptor wallet signing failed with descriptor wallets require PSBT format transactions even when handed a valid PSBT. The underlying issue: descriptor flows were doing a wasteful byte round-trip through the wrong PSBT class.

prebuild bytes  →  fixedScriptWallet.BitGoPsbt  →  bytes  →  descriptorWallet.Psbt

decodeTransactionFromPrebuild with decodeWith='wasm-utxo' (the default for all coins after 1702a08) returns the fixed-script BitGoPsbt. Descriptor signing/parsing/verifying/explaining then had to toWasmPsbt(tx) back to the descriptor Psbt before calling the descriptor APIs. Beyond the wasted work, an isUtxoLibPsbt-only guard on the descriptor sign branch silently broke every descriptor signing flow once wasm-utxo became the default backend — customers on CoreDAO BTC staking hit this in production (request 439fbc81-433e-417f-8c69-b9c005a8df79).

Approach

Replace the guard relaxation that was originally proposed with a deeper fix: decode prebuild bytes straight into the wasm-utxo descriptor Psbt, skipping BitGoPsbt entirely.

New helper decodeDescriptorPsbt:

export function decodeDescriptorPsbt(prebuild: PrebuildLike): WasmPsbt {
  const s = prebuild.txHexPsbt ?? prebuild.txHex ?? prebuild.txBase64;
  if (!s) throw new Error('missing required txHex or txBase64 property');
  const bytes = stringToBufferTryFormats(s, ['hex', 'base64']);
  if (!hasPsbtMagic(bytes)) {
    throw new Error('descriptor wallets require PSBT format transactions');
  }
  return WasmPsbt.deserialize(bytes);
}

Used in all four descriptor call sites:

Site Was Now
signTransaction.ts descriptor branch decodeTransactionFromPrebuildBitGoPsbt → guard → toWasmPsbtPsbt decodeDescriptorPsbt(prebuild)Psbt
descriptor/parse.ts same round-trip same direct decode
descriptor/verifyTransaction.ts same round-trip same direct decode
abstractUtxoCoin.explainTransaction + explainTx branched after decode, then toWasmPsbt(tx) branches by wallet type before decode

As a result, BitGoPsbt is never produced for descriptor flows, and toWasmPsbt's job is back to handling fixed-script callers only.

Commits

  1. test(abstract-utxo): reproduce BitGoPsbt rejection in descriptor sign — failing regression test, asserts the T1-3400 guard message is not thrown.
  2. refactor(abstract-utxo): decode descriptor PSBTs directly to wasm Psbt — the actual fix.
  3. test(abstract-utxo): add E2E descriptor signTransaction test (T1-3401) — drives coin.signTransaction end-to-end for a descriptor wallet via nockWalletKeys, asserts the returned txHex deserializes as a PSBT with valid signatures on each input. This is the test that would have caught T1-3400 — the SDK had no top-level signTransaction coverage against any descriptor wallet before this.

The regression test (commit 1) verifies the bug; the refactor commit (commit 2) makes it pass; the E2E test (commit 3) locks in the full signing pipeline.

Test plan

  • npx tsc --noEmit clean
  • yarn lint clean
  • yarn unit-test — 1926 passing, 0 failing
  • Regression confirmed: checked out commit 1 (test only) and ran both signTransactionGuard.ts and signTransactionE2E.ts against the pre-refactor source — both fail with Error: descriptor wallets require PSBT format transactions at signTransaction.ts:63:13, exactly the customer-facing error.

Linear

@linear-code
Copy link
Copy Markdown

linear-code Bot commented May 21, 2026

T1-3400

T1-3401

@OttoAllmendinger OttoAllmendinger force-pushed the otto/t1-3400-fix-descriptor-sign branch from 260ca71 to de1cffc Compare May 21, 2026 10:12
Descriptor flows were going through a wasteful byte round-trip:

  prebuild bytes -> fixedScriptWallet.BitGoPsbt -> bytes -> descriptorWallet.Psbt

decodeTransactionFromPrebuild + decodeWith='wasm-utxo' returns the
fixed-script BitGoPsbt, which descriptor signing/parsing/verifying/
explaining then had to convert via toWasmPsbt before reaching the
descriptor APIs. Beyond the wasted work, it produced T1-3400 — a
guard that only accepted utxo-lib PSBTs rejected the BitGoPsbt the
default backend produced.

Add a decodeDescriptorPsbt helper (transaction/decode.ts) that parses
prebuild bytes straight into the wasm-utxo descriptor Psbt. Use it in
all four descriptor sites:

  - signTransaction.ts descriptor branch
  - descriptor/parse.ts
  - descriptor/verifyTransaction.ts
  - abstractUtxoCoin.explainTransaction (+ explainTx descriptor branch)

This makes the BitGoPsbt path unreachable for descriptor wallets,
so the T1-3400 'descriptor wallets require PSBT format transactions'
guard relaxation is no longer needed.

Closes: T1-3400
No test in the monorepo exercised the top-level signTransaction
against a descriptor wallet, leaving the decode-and-route logic in
the SDK entry point unprotected. T1-3400 lived in that gap for the
full lifetime of the wasm-utxo backend default.

Add an end-to-end test that drives a real PSBT through
coin.signTransaction with decodeWith='wasm-utxo', mocks the keychain
fetch via nock, and asserts the returned txHex deserializes as a
signed PSBT with valid user-key signatures on each input.

Refs: T1-3401
@OttoAllmendinger OttoAllmendinger force-pushed the otto/t1-3400-fix-descriptor-sign branch from de1cffc to 6766023 Compare May 21, 2026 10:39
@OttoAllmendinger OttoAllmendinger changed the title fix(abstract-utxo): accept BitGoPsbt in descriptor sign guard refactor(abstract-utxo): decode descriptor PSBTs directly to wasm Psbt May 21, 2026
@OttoAllmendinger OttoAllmendinger marked this pull request as ready for review May 21, 2026 10:59
@OttoAllmendinger OttoAllmendinger requested a review from a team as a code owner May 21, 2026 10:59
@OttoAllmendinger OttoAllmendinger merged commit c447baf into master May 21, 2026
22 checks passed
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.

2 participants