Skip to content

fix(abstract-utxo): check the descriptor policy spend rules when a descriptor wallet signs - #9991

Open
OttoAllmendinger wants to merge 1 commit into
masterfrom
otto/SI-1971-pox5-hot-descriptor-spend-policy
Open

OttoAllmendinger wants to merge 1 commit into
masterfrom
otto/SI-1971-pox5-hot-descriptor-spend-policy

Conversation

@OttoAllmendinger

@OttoAllmendinger OttoAllmendinger commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Description

Follow-up to #9987 (stacked on it; this PR targets its branch and should be retargeted to master once #9987 merges).

#9987 makes the offline vault PoX-5 spend policy branch-aware, but it only covers the offline vault. A hot descriptor wallet signs through coin.signTransaction (transaction/signTransaction.ts), which only validated the descriptor policy and that each input matches a known descriptor. Nothing checked nLockTime, sequence, the principal preimage or the single-lockup-input rule before the SDK user key signed a PoX-5 exit, for late and early exits alike.

The check hangs off the descriptor policy instead of being called from the generic sign path:

  • DescriptorValidationPolicy gets an optional validateSpend(psbt, descriptorMap): it throws when a spend of a descriptor the policy covers must not be signed, and does nothing for inputs it does not recognize. The policy combinators (every, some, eachOneOf) forward it to the policies they are made of.
  • The PoX-5 lockup validator implements it with assertPox5SpendPolicy: exactly one lockup input; late exit: nLockTime at or above the unlock height and below 500,000,000, non-final sequence; early exit: nLockTime 0 and a single PSBT_IN_SHA256 record matching the descriptor stakerCommitment.
  • signTransaction makes one generic call, policy.validateSpend?.(psbt, descriptorMap), and does not know about PoX-5. PoX-5 is named only where it already was (the lockup validators in validatePolicy.ts).
  • Outside of prod getPolicyForEnv still accepts any descriptor, but it keeps the prod policy's validateSpend, so spends are checked in every environment.

PSBTs without a PoX-5 lockup input are unaffected. To let the descriptor policy use it, offlineVault/descriptor/pox5.ts moves to descriptor/pox5SpendPolicy.ts (no behavior change; the offline vault imports it from there). The offline vault still calls assertPox5SpendPolicy directly; moving it onto the policy hook is left for a follow-up.

Issue Number

Refs: SI-1971

Type of change

  • Bug fix (non-breaking change which fixes an issue)

Behavior change to be aware of

A hot descriptor wallet now refuses to sign a PoX-5 lockup spend that violates the policy, where it signed it before. Wallet Platform builds late exits with nLockTime at the unlock height and a non-final sequence, and early exits with nLockTime 0 and the preimage, so correctly built exits are unaffected. This has not been run against the live matrix.

How Has This Been Tested?

modules/abstract-utxo:

  • new test/unit/transaction/descriptor/signTransactionPox5.ts through coin.signTransaction with a descriptor wallet: late exit signs at the unlock height and is rejected below it or with a final sequence; early exit signs with nLockTime 0 and keeps the preimage, and is rejected with a non-zero nLockTime or a preimage the descriptor does not commit to. The policy of prod, adminProd, test and mock all check the spend. Without the validateSpend call the rejection tests fail.
  • npx mocha --recursive test/unit/transaction test/unit/signTransaction.ts test/unit/offlineVault: 446 passing.

Early exit on the hot path is still blocked at the HSM, which rejects early-exit-key partial signatures (SI-2002), and that is not addressed here.

@linear-code

linear-code Bot commented Oct 16, 2026

Copy link
Copy Markdown
Contributor

SI-1971

@OttoAllmendinger
OttoAllmendinger force-pushed the otto/SI-1971-pox5-hot-descriptor-spend-policy branch from 6466f0d to 2ecba1b Compare October 16, 2026 13:00
@OttoAllmendinger OttoAllmendinger changed the title fix(abstract-utxo): apply the PoX-5 spend policy when a descriptor wallet signs fix(abstract-utxo): apply the descriptor extension spend policy when a descriptor wallet signs Oct 16, 2026
@OttoAllmendinger
OttoAllmendinger force-pushed the otto/SI-1971-pox5-hot-descriptor-spend-policy branch from 2ecba1b to 6466f0d Compare October 16, 2026 13:17
@OttoAllmendinger OttoAllmendinger changed the title fix(abstract-utxo): apply the descriptor extension spend policy when a descriptor wallet signs fix(abstract-utxo): apply the PoX-5 spend policy when a descriptor wallet signs Oct 16, 2026
@OttoAllmendinger
OttoAllmendinger added this pull request to stack #9993 October 16, 2026 13:18
@github-actions

github-actions Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

⚠️ Unit tests are failing on Node 26.x (Current release line, non-blocking). This is not an LTS version yet, so it does not block merge, but it signals an incompatibility to fix before Node 26.x becomes LTS.

View run

@OttoAllmendinger
OttoAllmendinger force-pushed the otto/SI-1971-pox5-hot-descriptor-spend-policy branch from 6466f0d to 67d999b Compare October 16, 2026 13:23
@OttoAllmendinger OttoAllmendinger changed the title fix(abstract-utxo): apply the PoX-5 spend policy when a descriptor wallet signs fix(abstract-utxo): check the descriptor policy spend rules when a descriptor wallet signs Oct 16, 2026
Base automatically changed from otto/SI-1971-pox5-offlinevault-early-exit to master October 19, 2026 08:16
…scriptor wallet signs

Hot descriptor wallets sign through coin.signTransaction, which only checked
the descriptor policy and that each input has a known descriptor. Nothing
verified how a PoX-5 lockup was spent before the SDK user key signed it, for
late and early exits alike.

Give DescriptorValidationPolicy an optional validateSpend(psbt, descriptorMap)
that throws for spends of the descriptors it covers that must not be signed,
and forward it through the policy combinators. The PoX-5 lockup validator
implements it with the PoX-5 spend policy the offline vault applies before
half-signing: exactly one lockup input; for a late exit nLockTime at or above
the unlock height and a non-final sequence; for an early exit nLockTime 0 and
the principal preimage the descriptor commits to. signTransaction makes one
generic call and does not know about PoX-5. Outside of prod the policy still
accepts any descriptor, but spends are checked in every environment.

Move the policy from offlineVault/descriptor to descriptor/pox5SpendPolicy
so the descriptor policy can use it.

Refs: SI-1971
@OttoAllmendinger
OttoAllmendinger force-pushed the otto/SI-1971-pox5-hot-descriptor-spend-policy branch from 67d999b to 9098c43 Compare October 19, 2026 08:17
@OttoAllmendinger
OttoAllmendinger marked this pull request as ready for review October 19, 2026 08:17
@OttoAllmendinger
OttoAllmendinger requested a review from a team as a code owner October 19, 2026 08:17

This branch has not been deployed

No deployments
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