Skip to content

feat(psbt): add taproot support for HD signing methods#2331

Open
dripsmvcp wants to merge 1 commit intobitcoinjs:masterfrom
dripsmvcp:feat/taproot-hd-signing
Open

feat(psbt): add taproot support for HD signing methods#2331
dripsmvcp wants to merge 1 commit intobitcoinjs:masterfrom
dripsmvcp:feat/taproot-hd-signing

Conversation

@dripsmvcp
Copy link
Copy Markdown

Summary

signInputHD / signAllInputsHD (and their async variants) silently fail on taproot inputs because they only look at bip32Derivation, not tapBip32Derivation. This patch teaches the HD signing flow to recognize taproot inputs and derive the correct signers.

What changed

  • getSignersFromHD now detects taproot inputs via isTaprootInput() and delegates to a new getTaprootSignersFromHD helper
  • Key-path spend (leafHashes is empty): derives the child key from tapBip32Derivation, then tweaks it with tapTweakHash(internalKey, tapMerkleRoot) — the tweaked signer produces a valid tapKeySig
  • Script-path spend (leafHashes is non-empty): derives the child key untweaked and hands it off to the existing _signTaprootInput machinery, which matches the pubkey against tapLeafScript entries
  • sighashTypes parameter on all four HD methods changed from = [SIGHASH_ALL] to optional — this removes a conflict where taproot's SIGHASH_DEFAULT (0x00) was being rejected by a hardcoded SIGHASH_ALL filter. Inner methods (_signInput / _signTaprootInput) already default correctly per input type
  • HDSigner / HDSignerAsync interfaces extended with optional signSchnorr? and tweak? — no breaking change for existing implementations

Why this approach

PR #2329 tackled the same problem. This takes a slightly different approach:

  1. Only modifies ts_src/psbt.ts — compiled src/cjs/ and src/esm/ output is generated by the build, not hand-edited
  2. Cleaner filtering — .filter() instead of .map().filter() for fingerprint matching
  3. Avoids redundant checkForInput call — passes the already-resolved input directly to getTaprootSignersFromHD
  4. Uses Uint8Array consistently in tests (v7-compatible)

Test plan

  • Key-path signing with signInputHD
  • Key-path signing with merkle root present (script tree exists, but signing via key path)
  • Script-path signing with signInputHD
  • Mixed taproot + legacy (P2WPKH) inputs with signAllInputsHD
  • Error: missing tapBip32Derivation
  • Error: fingerprint mismatch
  • Error: pubkey / derivation path mismatch
  • All 2667 existing tests still pass

Closes #2132

@dripsmvcp
Copy link
Copy Markdown
Author

@jasonandjay @junderw Would you check this PR?

Taproot inputs using tapBip32Derivation now work with signInputHD,
signInputHDAsync, signAllInputsHD, and signAllInputsHDAsync.

- Detect taproot inputs in getSignersFromHD and delegate to new
  getTaprootSignersFromHD helper
- Key-path spend (empty leafHashes): derive child key and apply
  tap tweak using tapMerkleRoot from the PSBT input
- Script-path spend (non-empty leafHashes): derive child key
  untweaked, existing _signTaprootInput handles the rest
- Change sighashTypes default from [SIGHASH_ALL] to optional on
  HD methods, matching signAllInputs — inner methods pick the
  correct default per input type (SIGHASH_DEFAULT for taproot,
  SIGHASH_ALL for legacy/segwit)
- Extend HDSigner / HDSignerAsync interfaces with optional
  signSchnorr and tweak methods

Closes bitcoinjs#2132
@dripsmvcp dripsmvcp force-pushed the feat/taproot-hd-signing branch from 51455f3 to 02f7fcb Compare April 5, 2026 18:33
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.

Feature Request: Taproot support for signAllInputsHD

1 participant