Skip to content

fix(tbtc): gate native BuildTaprootTx on the transaction being all-Taproot#4117

Merged
mswilkison merged 2 commits into
feat/frost-schnorr-migration-scaffoldfrom
fix/native-buildtx-gate-taproot
Jun 27, 2026
Merged

fix(tbtc): gate native BuildTaprootTx on the transaction being all-Taproot#4117
mswilkison merged 2 commits into
feat/frost-schnorr-migration-scaffoldfrom
fix/native-buildtx-gate-taproot

Conversation

@mswilkison

Copy link
Copy Markdown
Contributor

Summary

Follow-up to #3866 (Go FROST/ROAST coordinator). Gates the native (Rust cgo) BuildTaprootTx parity/substitution path so it runs only for Taproot transactions.

walletTransactionExecutor.signTransaction ran the native BuildTaprootTx path unconditionally, at the very top, for every transaction a wallet signs — including legacy ECDSA redemptions / sweeps / moving-funds — before any scheme/Taproot check. The native builder is a Taproot builder, so running it for a legacy (non-Taproot) transaction is meaningless, and a hard error from it (other than ErrNativeCryptographyUnavailable) would fail the signing of that legacy transaction. In the default build the native builder is a no-op (("", nil)), so this only bit the frost_native+cgo build — but it's a latent regression on the legacy signing path.

Fix

Gate the native-build/substitution path on unsignedTx.HasOnlyTaprootKeyPathInputs() — the same predicate that already governs FROST signing later in the same function ("cannot apply FROST signatures to non-taproot transaction inputs"). The native Taproot builder now runs only for all-Taproot transactions; legacy transactions skip it. The path is extracted into maybeSubstituteNativeBuildTaprootTx.

Gate signal chosen per a Codex second opinion: the tx shape (HasOnlyTaprootKeyPathInputs), not the wallet scheme — "the native builder's applicability is about the unsigned tx it is asked to build."

Tests

The four legacy-P2PKH substitution-through-signTransaction integration tests asserted the now-removed behaviour (substitution for a legacy tx). They are replaced with:

  • SkipsNativeBuildForLegacyTransaction — a legacy (P2PKH) tx: the native build is not invoked even with substitution enabled, and the tx signs via the Go path. (Verified to fail if the gate is removed.)
  • SubstitutesNativeBuildForTaprootTransaction — an all-Taproot tx: the native build is invoked, and a matching native tx is substituted (ReplaceUnsignedTransaction + the substitution info log) and signed with a Taproot witness.

The two native-build error-propagation tests are retargeted to a Taproot tx (the only path on which the native build now runs), via a shared buildTaprootKeyPathUnsignedTxForTest helper. The substitution logic remains covered directly by the TestEvaluateNativeUnsignedTransactionForSigning_* tests.

gofmt + go vet clean; full untagged pkg/tbtc suite passes; builds clean under -tags 'frost_native frost_tbtc_signer cgo'.

Found during review of #3866.

🤖 Generated with Claude Code

…proot

signTransaction unconditionally ran the native (Rust cgo) BuildTaprootTx
parity/substitution path at the top of the function -- for EVERY transaction,
including legacy ECDSA redemptions/sweeps/moving-funds. The native builder is a
Taproot builder, so running it for a legacy (non-Taproot) transaction is
meaningless, and a hard error from it (other than ErrNativeCryptographyUnavailable)
would fail the signing of that legacy transaction. In the default build the
native builder is a no-op, so this only bit the frost_native+cgo build, but it is
a latent regression on the legacy signing path.

Gate the native-build/substitution path on unsignedTx.HasOnlyTaprootKeyPathInputs()
-- the same predicate that already governs FROST signing later in the function --
so the native Taproot builder runs only for all-Taproot transactions. Extract the
path into maybeSubstituteNativeBuildTaprootTx.

The substitution LOGIC (observational logging, divergence rejection, matching-IO
acceptance) remains covered directly by the
TestEvaluateNativeUnsignedTransactionForSigning_* tests. Replace the four
legacy-P2PKH substitution-through-signTransaction integration tests (which
asserted the now-removed behaviour) with two gate tests --
SkipsNativeBuildForLegacyTransaction and InvokesNativeBuildForTaprootTransaction --
and retarget the two native-build error-propagation tests to a Taproot
transaction (the only path on which the native build now runs).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: cea6cafa-a8ce-43b3-9b75-e0529f8a736f

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/native-buildtx-gate-taproot

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@mswilkison mswilkison merged commit 1d4e2fb into feat/frost-schnorr-migration-scaffold Jun 27, 2026
17 checks passed
@mswilkison mswilkison deleted the fix/native-buildtx-gate-taproot branch June 27, 2026 22:06
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