From 16ef938f6bb9336675ac767ae2c2b728edff88b8 Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Mon, 7 Sep 2026 12:52:04 +0200 Subject: [PATCH] refactor(utxo-staking): classify PoX-5 recovery branches Add the native PoX-5 spend-branch classifier required by the thin Wallet Platform adapter. The classifier revalidates the descriptor-bound input and distinguishes the locktime branch from the principal-preimage early-exit branch using native PSBT_IN_SHA256 metadata. Add unit coverage for both native metadata states. Wallet Platform remains responsible for request and wallet policy, legacy conversion boundaries, and error translation; it does not parse PoX-5 descriptors or raw PSBT key-value data. Refs: WAL-2024 --- modules/utxo-staking/src/pox5/recovery.ts | 11 +++++++++++ modules/utxo-staking/test/unit/pox5/recovery.ts | 10 ++++++++++ 2 files changed, 21 insertions(+) diff --git a/modules/utxo-staking/src/pox5/recovery.ts b/modules/utxo-staking/src/pox5/recovery.ts index 7f19547ac6..bed1957431 100644 --- a/modules/utxo-staking/src/pox5/recovery.ts +++ b/modules/utxo-staking/src/pox5/recovery.ts @@ -58,6 +58,17 @@ export function assertPox5EarlyExitSpend(psbt: Psbt, input: pox5.Pox5InputMatch) getMatchedTransactionInput(psbt, input); } +export type Pox5SpendBranch = 'locktime' | 'early-exit'; + +/** Classify a PoX-5 spend from native principal-preimage metadata. */ +export function classifyPox5Spend(psbt: Psbt, input: pox5.Pox5InputMatch): Pox5SpendBranch { + getMatchedTransactionInput(psbt, input); + const hasPrincipalPreimage = psbt + .getInputKeyValues(input.inputIndex) + .some((record) => record.type === 'known' && record.key === 'PSBT_IN_SHA256'); + return hasPrincipalPreimage ? 'early-exit' : 'locktime'; +} + /** Add validated principal-preimage metadata for an early-exit spend. */ export function preparePox5EarlyExit( psbt: Psbt, diff --git a/modules/utxo-staking/test/unit/pox5/recovery.ts b/modules/utxo-staking/test/unit/pox5/recovery.ts index 5e4cf9ec58..4570da2786 100644 --- a/modules/utxo-staking/test/unit/pox5/recovery.ts +++ b/modules/utxo-staking/test/unit/pox5/recovery.ts @@ -8,6 +8,7 @@ import { getKey, getKeyTriple } from '@bitgo/wasm-utxo/testutils'; import { assertPox5EarlyExitSpend, assertPox5LocktimeSpend, + classifyPox5Spend, POX5_MAX_UNLOCK_HEIGHT, preparePox5EarlyExit, } from '../../../src/pox5'; @@ -60,6 +61,15 @@ describe('PoX-5 spend policy', function () { assert.doesNotThrow(() => assertPox5EarlyExitSpend(timestampLocktime.psbt, timestampLocktime.match)); }); + it('classifies the spend from native principal-preimage metadata', function () { + const locktimeSpend = createPox5RecoveryPsbt(UNLOCK_HEIGHT); + assert.equal(classifyPox5Spend(locktimeSpend.psbt, locktimeSpend.match), 'locktime'); + + const earlyExitSpend = createPox5RecoveryPsbt(0); + preparePox5EarlyExit(earlyExitSpend.psbt, 0, earlyExitSpend.match, earlyExitSpend.principalPreimage); + assert.equal(classifyPox5Spend(earlyExitSpend.psbt, earlyExitSpend.match), 'early-exit'); + }); + it('enforces the block-height and unlock-height boundaries', function () { const atHeight = createPox5RecoveryPsbt(UNLOCK_HEIGHT); const aboveHeight = createPox5RecoveryPsbt(UNLOCK_HEIGHT + 1);