Skip to content

Commit 309c87e

Browse files
V30 Gate 3: Harden BTC fee PSBT posture
Adds package-owned BTC fee network policy and Taproot PSBT posture types, enforces quote timestamp windows and signed-PSBT evidence, extends BTD/API tests for no-custody handoff, replacement, reorg, and mainnet blocking, and wires V30 Gate 3 specification parity plus checker/workflow coverage.
1 parent 161df58 commit 309c87e

12 files changed

Lines changed: 662 additions & 13 deletions

.github/workflows/bitcode-gate-quality.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ jobs:
8181
node scripts/check-v29-gate10-local-staging-promotion-readiness.mjs --promotion-mode --skip-branch-check
8282
node scripts/check-v30-gate1-roadmap-and-gating.mjs --skip-branch-check
8383
node scripts/check-v30-gate2-protocol-package-api-boundaries.mjs --skip-branch-check
84+
node scripts/check-v30-gate3-bitcoin-taproot-psbt-fee-rigor.mjs --skip-branch-check
8485
else
8586
echo "Unexpected BITCODE_SPEC.txt pointer: $POINTER" >&2
8687
exit 1

BITCODE_SPEC_V30.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,9 @@ The V30 domain model extends V29 operationally:
183183
- `SettlementUnlock`: BTC fee proof, BTD range/read-license/right transfer, paid disclosure decision, delivery admission, and reconciliation state.
184184
- `BtcFeeQuote`: deterministic BTC fee quote with quote root, measurement root, purpose, network, sats, pricing version, expiration, and lifecycle state.
185185
- `WalletSignerSessionRecovery`: signer-session posture proving whether the Reader wallet can sign a PSBT without server custody.
186-
- `BtcFeeOperationPosture`: operational state over quote, signer, PSBT, broadcast, finality, replacement, reorg, failure, and blocked readiness.
186+
- `BtcFeeNetworkPolicy`: proof-rooted environment and network admission posture over local, staging-testnet, and production-mainnet value-bearing BTC settlement.
187+
- `BtcFeeTaprootPsbtPosture`: proof-rooted Taproot commitment, script path, PSBT handoff, broadcast observation, replacement, reorg, and finality posture.
188+
- `BtcFeeOperationPosture`: operational state over quote, signer, network policy, Taproot/script posture, PSBT, broadcast, finality, replacement, reorg, failure, and blocked readiness.
187189
- `BtdAssetPackMintReceipt`: typed receipt binding AssetPack id, BTD range, depositor ownership, source-safe preview root, mint measurement root, settlement conservation root, and ledger projection root.
188190
- `BtdReadReceipt`: typed receipt binding Reader, read request, accepted Need, Finding Fits result root, source-safe preview root, paid/unpaid disclosure state, read-right state, and delivery admission.
189191
- `BtdRightsTransferReceipt`: typed receipt binding BTC fee finality, BTD owner or license transition, range projection, paid unlock, delivery proof, and reconciliation status.
@@ -317,7 +319,9 @@ The operation model must contain:
317319
- quote lifecycle: quoted, accepted, expired, superseded, and failed;
318320
- deterministic quote root over quote id, purpose, network, sats, measurement root, issue time, and expiration;
319321
- signer recovery: missing, prepared authorization required, stored authorization requiring live reconnect, expired, revoked, failed, network mismatch, capability missing, server-custody rejected, or live authorized;
320-
- PSBT handoff: accepted quote prepares a PSBT; the wallet signs; the signed PSBT broadcasts;
322+
- network policy: local and staging-testnet may exercise value-bearing BTC settlement for QA; production-mainnet value-bearing settlement remains blocked until explicit operational approval is attached and proof-rooted;
323+
- Taproot/script posture: Bitcoin fee and anchor paths default to Taproot, name the admitted script path, and expose non-Taproot posture as audit evidence rather than silently treating it as equivalent;
324+
- PSBT handoff: accepted quote prepares an unsigned PSBT; the wallet signs; the signed PSBT is ready for broadcast; broadcast and finality observations are separate states;
321325
- finality states: prepared, signed, broadcast, confirmed, replaced, reorged, and failed;
322326
- blocked-readiness receipts naming the blocker id, summary, required action, quote id, wallet session id, receipt id, and no-server-custody posture;
323327
- Terminal read rows and metrics for state, network, sats, confirmations, quote root, wallet session, payer wallet, PSBT handoff, txid, server custody, and next action.

BITCODE_SPEC_V30_DELTA.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,22 @@ Closure acceptance:
116116
- testnet/mainnet distinction is policy-enforced and proof-rooted;
117117
- focused BTD/API tests prove fee lifecycle and blocked-readiness behavior.
118118

119+
Gate 3 implementation centers these semantics in
120+
`packages/btd/src/btc-fee-operation.ts` and `packages/btd/src/bitcoin-fees.ts`.
121+
`BtcFeeNetworkPolicy` admits local and staging-testnet fee operation while
122+
blocking value-bearing production-mainnet settlement unless explicit operational
123+
approval is attached. `BtcFeeTaprootPsbtPosture` names the Bitcoin commitment
124+
method, script path, PSBT handoff state, and broadcast observation state so
125+
prepared, signed, broadcast, confirmed, replaced, reorged, and failed receipts
126+
remain distinguishable. Signed receipts require a signed PSBT, quote timestamps
127+
must be usable for PSBT preparation, and server-custody signer sessions remain
128+
fail-closed before handoff.
129+
130+
Gate 3 evidence is covered by
131+
`packages/btd/__tests__/btc-fee-operation.test.ts`,
132+
`packages/api/src/routes/__tests__/btd-crypto.test.ts`, and
133+
`scripts/check-v30-gate3-bitcoin-taproot-psbt-fee-rigor.mjs`.
134+
119135
### Gate 4: BTD AssetPack Mint And Read Receipts
120136

121137
Gate 4 creates typed BTD receipt boundaries.

BITCODE_SPEC_V30_NOTES.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,24 @@ standalone demonstration imports.
9090
3. **Gate 3: Bitcoin Taproot PSBT Fee Rigor**
9191
- Harden BTC fee quote, signer recovery, PSBT, Taproot/script posture, broadcast, replacement, reorg, finality, and testnet/mainnet boundaries.
9292

93+
## Gate 3 Bitcoin Taproot PSBT fee rigor notes
94+
95+
Gate 3 makes BTC fee operation inspectable before any later BTD receipt,
96+
projection, or interface depends on it. `BtcFeeOperationPosture` now carries the
97+
network policy, Taproot/PSBT posture, PSBT handoff state, and broadcast
98+
observation state beside quote, signer, receipt, and blocked-readiness data.
99+
100+
The network policy deliberately treats staging-testnet as the operational lane
101+
for current QA. Production-mainnet value-bearing settlement is not admitted by
102+
default. It must carry explicit operational approval before the posture leaves
103+
blocked-readiness, and that approval state contributes to the policy proof root.
104+
105+
The PSBT path distinguishes prepared unsigned PSBT, signed ready-to-broadcast
106+
PSBT, broadcast submission, finality observation, replacement/reorg repair, and
107+
failure. Signed receipt advancement requires a signed PSBT. Broadcast,
108+
confirmed, replaced, and reorged observations require transaction id evidence.
109+
Server-custody signer posture remains rejected before PSBT handoff.
110+
93111
4. **Gate 4: BTD AssetPack Mint And Read Receipts**
94112
- Make BTD mint, read, and rights-transfer receipts typed, proof-rooted, stored, streamed, and source-safe.
95113

BITCODE_SPEC_V30_PARITY_MATRIX.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ No `_legacy/` source is active source truth.
5656
| Draft family and branch posture | Gate 1 | `BITCODE_SPEC_V30.md`, DELTA, NOTES, PARITY, `BITCODE_SPEC.txt`, branch `v30/gate-1-roadmap-and-gating` | drafted | V30 family validates in draft mode over active V29 and `check:v30-gate1` passes. |
5757
| Roadmap truth | Gate 1 | `SPECIFICATIONS_ROADMAP.md`, README, PR template, workflow posture | drafted | Roadmap states V29 active, V30 draft, and coherent V31-V37 responsibilities. |
5858
| Protocol package API boundaries | Gate 2 | `packages/btd/src/api-boundaries.ts`, `packages/api/src/routes/btd-crypto.ts`, package READMEs/tests | drafted | Shared Protocol/BTD objects have package-owned builders, parsers, validators, JSON-safe serializers, and tests. |
59-
| Bitcoin Taproot PSBT fee rigor | Gate 3 | BTD fee/signer/PSBT primitives, API route adapters, tests | pending | BTC fee and signer states are typed, testnet/mainnet-safe, no-custody, and proof-rooted. |
59+
| Bitcoin Taproot PSBT fee rigor | Gate 3 | `packages/btd/src/btc-fee-operation.ts`, `packages/btd/src/bitcoin-fees.ts`, BTD/API tests, gate checker | drafted | BTC fee and signer states are typed, testnet/mainnet-safe, no-custody, Taproot/PSBT aware, and proof-rooted. |
6060
| BTD AssetPack mint/read receipts | Gate 4 | BTD receipt primitives, asset-pack postprocess, harness evidence, Terminal/API readback | pending | Mint, read, and rights-transfer receipts bind BTD range, preview, paid unlock, delivery, and ledger projection. |
6161
| Testnet ledger projection hardening | Gate 5 | BTD reconciliation, Supabase readback, object-storage evidence, repair tests | pending | Ledger/database/object-storage projections are synchronized or blocked with deterministic repair posture. |
6262
| Source-to-shares proof cleanup | Gate 6 | BTD/source-to-shares proof primitives, settlement conservation tests | pending | Measurement contribution, fee allocation, zero-cell/refit tail, and conservation invariants are testable. |
@@ -112,6 +112,26 @@ No `_legacy/` source is active source truth.
112112
- Gate 2 does not introduce bridge chain-of-record behavior.
113113
- Gate 2 does not admit value-bearing mainnet settlement.
114114
- Gate 2 does not remove existing API route persistence adapters; it narrows their policy and receipt derivation responsibilities to package calls.
115+
116+
## Gate 3 Parity
117+
118+
| Requirement | Source evidence | Current V30 judgment |
119+
| --- | --- | --- |
120+
| BTC fee quotes validate PSBT-usable timestamp windows | `packages/btd/src/btc-fee-operation.ts`, `packages/btd/__tests__/btc-fee-operation.test.ts` | drafted |
121+
| Wallet signer recovery rejects server custody explicitly | `buildWalletSignerSessionRecovery`, `packages/btd/__tests__/btc-fee-operation.test.ts` | drafted |
122+
| PSBT handoff is typed across prepared, signed, broadcast, finality, replacement/reorg repair, and failure | `BtcFeePsbtHandoffState`, `deriveBtcFeePsbtHandoffState`, `BtcFeeOperationPosture` | drafted |
123+
| Taproot/script posture is proof-rooted and attached to fee operation posture | `BtcFeeTaprootPsbtPosture`, `buildBtcFeeTaprootPsbtPosture`, API route tests | drafted |
124+
| Testnet/mainnet distinction blocks value-bearing production-mainnet settlement unless explicitly approved | `BtcFeeNetworkPolicy`, `buildBtcFeeNetworkPolicy`, `network-policy` blocked-readiness tests | drafted |
125+
| Signed receipts require signed PSBT evidence before broadcast | `packages/btd/src/bitcoin-fees.ts`, `packages/btd/__tests__/btc-fee-operation.test.ts` | drafted |
126+
| API settlement route serializes the added operation posture safely | `packages/api/src/routes/__tests__/btd-crypto.test.ts` | drafted |
127+
| Gate checker protects the BTC fee rigor surface | `scripts/check-v30-gate3-bitcoin-taproot-psbt-fee-rigor.mjs`, `pnpm run check:v30-gate3`, gate-quality workflow | drafted |
128+
129+
## Gate 3 accepted boundaries
130+
131+
- Gate 3 does not admit value-bearing production-mainnet settlement by default.
132+
- Gate 3 does not implement a bridge chain-of-record path.
133+
- Gate 3 does not custody Reader private keys or sign PSBTs server-side.
134+
- Gate 3 does not replace later Gate 4 BTD mint, read, and rights-transfer receipts.
115135
- Gate 1 does not promote `BITCODE_SPEC.txt` to V30.
116136
- Gate 1 may retarget workflows to active V29 / draft V30 so later gates are greenable.
117137
- Gate 1 may update roadmap scope for V31-V37 to align with V28/V29 promotion learning without opening those versions.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
"check:v29-gate10": "node scripts/check-v29-gate10-local-staging-promotion-readiness.mjs",
5757
"check:v30-gate1": "node scripts/check-v30-gate1-roadmap-and-gating.mjs",
5858
"check:v30-gate2": "node scripts/check-v30-gate2-protocol-package-api-boundaries.mjs",
59+
"check:v30-gate3": "node scripts/check-v30-gate3-bitcoin-taproot-psbt-fee-rigor.mjs",
5960
"check:spec-quality": "node scripts/run-bitcode-spec-quality.mjs --mode basic",
6061
"check:spec-quality:title": "node scripts/run-bitcode-spec-quality.mjs --mode strict-from-title",
6162
"check:spec-quality:v24": "node scripts/run-bitcode-spec-quality.mjs --mode strict-version --version V24",

packages/api/src/routes/__tests__/btd-crypto.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -667,6 +667,17 @@ describe('BTD crypto API builders', () => {
667667
phase: 'psbt_ready',
668668
canSignPsbt: true,
669669
noServerCustody: true,
670+
psbtHandoffState: 'prepared_unsigned',
671+
broadcastState: 'not_broadcast',
672+
networkPolicy: {
673+
admitted: true,
674+
environment: 'staging-testnet',
675+
mainnet: false,
676+
},
677+
taprootScriptPosture: {
678+
commitmentMethod: 'taproot',
679+
taprootAdmitted: true,
680+
},
670681
quote: { quoteId: 'quote-api-1' },
671682
});
672683
expect(prepared.terminalJournalEntry.transactionKind).toBe('btc_fee_payment');
@@ -1069,6 +1080,12 @@ describe('BTD crypto API builders', () => {
10691080
expect(body.operationPosture).toMatchObject({
10701081
phase: 'psbt_ready',
10711082
canSignPsbt: true,
1083+
psbtHandoffState: 'prepared_unsigned',
1084+
broadcastState: 'not_broadcast',
1085+
networkPolicy: {
1086+
admitted: true,
1087+
environment: 'staging-testnet',
1088+
},
10721089
quote: {
10731090
quoteId: 'quote-api-1',
10741091
sats: '1200',

packages/btd/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ This package owns:
1111
- contributor allocation, access evaluation, ancestry review, and revenue routing
1212
- wallet-signed BTC fee receipts, ledger anchors, Exchange rights transfers,
1313
Terminal journals, reconciliation, telemetry, and upgrade receipts
14+
- BTC fee operation posture, including quote lifecycle, signer recovery,
15+
no-server-custody PSBT handoff, Taproot/script posture, broadcast/finality
16+
observation, replacement/reorg repair, and testnet/mainnet network policy
1417
- Terminal operational health reads that compose deployment lanes, telemetry,
1518
upgrade posture, provider readiness, settlement-network posture, synthetic
1619
testnet minting, journal rows, ledger anchors, and reconciliation state

packages/btd/__tests__/btc-fee-operation.test.ts

Lines changed: 134 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import {
22
advanceBtcFeeQuote,
33
advanceBtcFeeTransactionReceipt,
4+
buildBtcFeeNetworkPolicy,
45
buildBtcFeeOperationPosture,
56
buildBtcFeeQuote,
7+
buildBtcFeeTaprootPsbtPosture,
68
buildPreparedBtcFeeTransactionReceipt,
79
buildWalletSignerSessionRecovery,
810
createWalletSignerSession,
@@ -31,7 +33,7 @@ function authorizedSession(overrides = {}) {
3133
});
3234
}
3335

34-
function acceptedQuote() {
36+
function acceptedQuote(overrides = {}) {
3537
return advanceBtcFeeQuote(
3638
buildBtcFeeQuote({
3739
quoteId: 'quote-1',
@@ -43,6 +45,7 @@ function acceptedQuote() {
4345
relatedAssetPackId: 'asset-pack-1',
4446
issuedAt,
4547
expiresAt,
48+
...overrides,
4649
}),
4750
{ state: 'accepted' },
4851
);
@@ -92,6 +95,109 @@ describe('BTC fee quote lifecycle', () => {
9295
/Invalid BTC fee quote transition/,
9396
);
9497
});
98+
99+
it('fails closed when quote timestamps cannot support PSBT preparation', () => {
100+
expect(() =>
101+
buildBtcFeeQuote({
102+
quoteId: 'quote-expired-at-issue',
103+
feePurpose: 'licensed_read',
104+
network: 'testnet',
105+
sats: 1200n,
106+
measurementRoot: 'need-fit-measurement-root',
107+
issuedAt,
108+
expiresAt: issuedAt,
109+
}),
110+
).toThrow('BTC fee quote expiresAt must be after issuedAt.');
111+
});
112+
113+
it('models PSBT handoff, replacement, and reorg repair states explicitly', () => {
114+
const prepared = preparedReceipt();
115+
expect(() =>
116+
advanceBtcFeeTransactionReceipt(prepared, { finalityState: 'signed' } as never),
117+
).toThrow('signedPsbt must be a non-empty string.');
118+
119+
const signed = advanceBtcFeeTransactionReceipt(prepared, {
120+
finalityState: 'signed',
121+
psbt: 'signed-psbt',
122+
});
123+
const broadcast = advanceBtcFeeTransactionReceipt(signed, {
124+
finalityState: 'broadcast',
125+
txid: 'txid-repair',
126+
});
127+
const replaced = advanceBtcFeeTransactionReceipt(broadcast, {
128+
finalityState: 'replaced',
129+
txid: 'replacement-txid',
130+
});
131+
132+
expect(buildBtcFeeTaprootPsbtPosture({ network: 'testnet', receipt: prepared })).toMatchObject({
133+
psbtHandoffState: 'prepared_unsigned',
134+
broadcastState: 'not_broadcast',
135+
taprootRequired: true,
136+
});
137+
expect(buildBtcFeeTaprootPsbtPosture({ network: 'testnet', receipt: signed })).toMatchObject({
138+
psbtHandoffState: 'signed_ready_to_broadcast',
139+
broadcastState: 'not_broadcast',
140+
});
141+
expect(buildBtcFeeTaprootPsbtPosture({ network: 'testnet', receipt: replaced })).toMatchObject({
142+
psbtHandoffState: 'replacement_or_reorg_repair_required',
143+
broadcastState: 'replaced',
144+
});
145+
});
146+
147+
it('blocks value-bearing mainnet settlement unless operationally approved', () => {
148+
expect(
149+
buildBtcFeeNetworkPolicy({
150+
network: 'mainnet',
151+
environment: 'production-mainnet',
152+
}),
153+
).toMatchObject({
154+
admitted: false,
155+
operationalApprovalRequired: true,
156+
mainnet: true,
157+
proofRoot: expect.stringMatching(/^btc-fee-network-policy-/),
158+
});
159+
160+
const mainnetQuote = acceptedQuote({ network: 'mainnet', quoteId: 'quote-mainnet-1' });
161+
const mainnetSession = authorizedSession({
162+
walletSessionId: 'wallet-session-mainnet',
163+
walletId: 'wallet-mainnet',
164+
address: 'bc1qbitcodereader0000000000000000000000000',
165+
network: 'mainnet',
166+
});
167+
const blocked = buildBtcFeeOperationPosture({
168+
quote: mainnetQuote,
169+
payerSession: mainnetSession,
170+
environment: 'production-mainnet',
171+
at: issuedAt,
172+
});
173+
expect(blocked).toMatchObject({
174+
phase: 'blocked',
175+
networkPolicy: {
176+
admitted: false,
177+
blocker: 'Value-bearing BTC fee settlement on mainnet requires explicit operational approval.',
178+
},
179+
blockedReadiness: {
180+
blockerId: 'network-policy',
181+
noServerCustody: true,
182+
},
183+
});
184+
185+
expect(
186+
buildBtcFeeOperationPosture({
187+
quote: mainnetQuote,
188+
payerSession: mainnetSession,
189+
environment: 'production-mainnet',
190+
valueBearingMainnetApproved: true,
191+
at: issuedAt,
192+
}),
193+
).toMatchObject({
194+
phase: 'quoted',
195+
networkPolicy: {
196+
admitted: true,
197+
operationalApprovalRequired: true,
198+
},
199+
});
200+
});
95201
});
96202

97203
describe('wallet signer recovery', () => {
@@ -133,6 +239,23 @@ describe('wallet signer recovery', () => {
133239
canSign: false,
134240
});
135241
});
242+
243+
it('rejects server-custody signer sessions before PSBT handoff', () => {
244+
const recovery = buildWalletSignerSessionRecovery({
245+
session: {
246+
...authorizedSession(),
247+
serverCustody: true,
248+
} as never,
249+
network: 'testnet',
250+
at: issuedAt,
251+
});
252+
253+
expect(recovery).toMatchObject({
254+
state: 'server_custody_rejected',
255+
canSign: false,
256+
blocker: 'Wallet signer session implies server custody.',
257+
});
258+
});
136259
});
137260

138261
describe('BTC fee operation posture', () => {
@@ -195,6 +318,8 @@ describe('BTC fee operation posture', () => {
195318
phase: 'psbt_ready',
196319
canSignPsbt: true,
197320
canBroadcast: false,
321+
psbtHandoffState: 'prepared_unsigned',
322+
broadcastState: 'not_broadcast',
198323
});
199324

200325
const signed = advanceBtcFeeTransactionReceipt(prepared, {
@@ -220,6 +345,12 @@ describe('BTC fee operation posture', () => {
220345
).toMatchObject({
221346
phase: 'broadcast',
222347
canObserveFinality: true,
348+
psbtHandoffState: 'broadcast_submitted',
349+
broadcastState: 'broadcast',
350+
taprootScriptPosture: {
351+
commitmentMethod: 'taproot',
352+
taprootAdmitted: true,
353+
},
223354
});
224355
expect(
225356
buildBtcFeeOperationPosture({
@@ -232,6 +363,8 @@ describe('BTC fee operation posture', () => {
232363
phase: 'confirmed',
233364
canSettle: true,
234365
noServerCustody: true,
366+
psbtHandoffState: 'finality_observed',
367+
broadcastState: 'confirmed',
235368
});
236369
});
237370
});

packages/btd/src/bitcoin-fees.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,10 @@ export function advanceBtcFeeTransactionReceipt(
124124
const txid = next.txid ?? receipt.txid;
125125
const confirmations = next.confirmations ?? receipt.confirmations;
126126

127+
if (next.finalityState === 'signed') {
128+
assertNonEmptyString(next.psbt, 'signedPsbt');
129+
}
130+
127131
if (['broadcast', 'confirmed', 'replaced', 'reorged'].includes(next.finalityState)) {
128132
assertNonEmptyString(txid, 'txid');
129133
}

0 commit comments

Comments
 (0)