Skip to content

fix(abstract-utxo): accept PoX-5 lockup descriptor in offline vault - #9961

Merged
OttoAllmendinger merged 3 commits into
masterfrom
otto/WAL-2156-accept-pox-5-lockup
Oct 14, 2026
Merged

OttoAllmendinger merged 3 commits into
masterfrom
otto/WAL-2156-accept-pox-5-lockup

Conversation

@OttoAllmendinger

Copy link
Copy Markdown
Contributor

Add a PoX-5 lockup validator to the descriptor policy (offlineVault and
prod getPolicyForEnv), accepting only canonical lockup descriptors whose
staker keys are the wallet keys. Template validators now return false for
descriptors they cannot parse instead of throwing. Half-signing applies
the PoX-5 staking spend policy (nLockTime, non-final sequence, exactly
one lockup input). The descriptor explanation now includes locktime.

@linear-code

linear-code Bot commented Oct 14, 2026

Copy link
Copy Markdown
Contributor

WAL-2156

@github-actions

github-actions Bot commented Oct 14, 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/WAL-2156-accept-pox-5-lockup branch 2 times, most recently from b690fb4 to 50a1c48 Compare October 14, 2026 10:57
Move the tbtcstx, tbtcstxprivate1 (tbtcreg) and tbtcstxsignet (tbtcsig)
wasm-utxo coin name mapping from per-class wasmName overrides into
toWasmUtxoCoinName, so callers that only have a UtxoCoinName (e.g. the
offline vault) can use these coins with wasm-utxo.

Refs: WAL-2156
Move POX5_MAX_UNLOCK_HEIGHT, assertPox5LocktimeSpend, assertPox5EarlyExitSpend,
classifyPox5Spend and preparePox5EarlyExit into the pox5 namespace of
utxo-descriptors. They only depend on the descriptor matching and preimage
helpers that already live there, and this lets consumers such as abstract-utxo
use the policy without importing the Babylon and CoreDAO staking code that the
utxo-staking root export pulls in.

utxo-staking keeps deprecated re-exports of the moved functions so existing
consumers keep working; its PoX-5 finalizers call them through utxo-descriptors.

Refs: WAL-2156
Add a PoX-5 lockup validator to the descriptor policy (offlineVault and
prod getPolicyForEnv), accepting only canonical lockup descriptors whose
staker keys are the wallet keys. Template validators now return false for
descriptors they cannot parse instead of throwing. Half-signing applies
the PoX-5 staking spend policy (nLockTime, non-final sequence, exactly
one lockup input). The descriptor explanation now includes locktime.

Refs: WAL-2156
@OttoAllmendinger
OttoAllmendinger force-pushed the otto/WAL-2156-accept-pox-5-lockup branch from 50a1c48 to 1afbaae Compare October 14, 2026 11:06
@OttoAllmendinger
OttoAllmendinger marked this pull request as ready for review October 14, 2026 11:42
@OttoAllmendinger
OttoAllmendinger requested a review from a team as a code owner October 14, 2026 11:42

@Doddanna17 Doddanna17 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — clean, security-conscious change with exactly the right tests.

What I checked:

  • Lockup validator is correctly strict. isCanonicalPox5Lockup rebuilds the descriptor from the wallet keys and accepts only when canonical.toString() === descriptor.toString(), which binds the staker keys to the wallet keys and rejects non-canonical encodings. getValidatorPox5Wallet = eachOneOf(Wsh2Of3, pox5Lockup) cleanly lets a wallet carry both descriptor kinds.
  • Spend policy is right. assertPox5SpendPolicy no-ops when there are no PoX-5 inputs; otherwise requires exactly one lockup input, branch locktime (early-exit rejected), nLockTime ≥ max(unlockHeights), and a non-final sequence; getMatchedTransactionInput also verifies the PSBT input's witnessUtxo script equals the descriptor's scriptPubkey.
  • Template validators return false instead of throwing for unparseable descriptors — needed for the eachOneOf fallthrough and doesn't weaken acceptance (still rejects). Covered by a test.
  • Tests hit the real attack vectors: foreign/permuted staker keys, malformed + non-canonical encodings, nLockTime below unlock / as a timestamp, final sequence, >1 lockup input, plus getPolicyForEnv(prod) accept/reject and a spend-policy compat test for the code moved from utxo-staking.

Non-blocking:

  • names.ts maps tbtcstx/tbtcstxprivate1 → tbtcreg and tbtcstxsignet → tbtcsig; btc/tbtcbgsig pass through (valid wasm names) — no gap.
  • API surface changed (new policy exports + deprecated utxo-staking re-exports) — downstream must pick up the matching SDK version.
  • tryParseDescriptor now hides "not a template" vs "genuinely malformed" — fine here, just noting.

CI green on 1afbaaec. Approving.

@OttoAllmendinger
OttoAllmendinger merged commit d9f5b45 into master Oct 14, 2026
26 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants