From 9d9a3fa6de12fa16de9a33717405c89a523588cc Mon Sep 17 00:00:00 2001 From: ltardivo Date: Wed, 5 Aug 2026 16:55:18 -0700 Subject: [PATCH 1/6] fix(lib): require confirmed revs for load and revision graph Guard first, prev, next, last, and load with confirmed-location checks; require confirmed successors/spends for next/last. Expand InnerComputer tests for unconfirmed starts, mempool next, spent last, and sync. fix (monorepo): Adapt apps and docs to confirmed InnerComputer queries Mine after deletes before finalWithdraw; confirm modules and mints in tests. Chess helper waits for tip confirmation before withdraw/cancel refund. Document observation stability and contract-vs-client computer APIs in docs and docs-2. --- packages/TBC777/src/tbc777.ts | 18 +- packages/TBC777/src/tbc777m.ts | 9 +- packages/TBC777/test/tbc777.test.ts | 9 +- packages/TBC777/test/tbc777m.test.ts | 2 + .../chess-app/src/components/ChessBoard.tsx | 16 +- packages/chess-contracts/README.md | 7 +- .../chess-contracts/src/chess-contract.ts | 76 +++++-- .../test/chess-contract.test.ts | 55 +++++ packages/commodity/test/commodity.test.ts | 14 ++ .../sandbox-and-inner-computer.md | 66 ++++++ packages/docs-2/docs/concepts/how-it-works.md | 21 +- packages/docs-2/docs/intro.md | 11 +- packages/docs/Lib/Computer/decode.md | 4 + packages/docs/Lib/Computer/first.md | 4 + packages/docs/Lib/Computer/getAncestors.md | 9 + packages/docs/Lib/Computer/getTxos.md | 10 + packages/docs/Lib/Computer/latest.md | 4 + packages/docs/Lib/Computer/load.md | 4 + packages/docs/Lib/Computer/next.md | 8 + packages/docs/Lib/Computer/prev.md | 6 + packages/docs/Lib/Computer/sync.md | 4 + packages/docs/Lib/Contract/index.md | 189 ++++++++---------- 22 files changed, 401 insertions(+), 145 deletions(-) diff --git a/packages/TBC777/src/tbc777.ts b/packages/TBC777/src/tbc777.ts index ba6775d8f..aba545a22 100644 --- a/packages/TBC777/src/tbc777.ts +++ b/packages/TBC777/src/tbc777.ts @@ -83,8 +83,8 @@ export type ClaimAmountEntry = [Id, Amount] * the specific `escrowRev` revision supplied to `audit()`. Escrow * implementations should record final-withdrawal entries only in their * terminal (latest) revision. The `finalWithdraw()` method on tokens - * additionally verifies that the supplied revision is currently the live tip - * of the escrow via `computer.last(rev)`. + * additionally verifies the tip via `computer.last(rev)` (requires that tip + * to be spent in a **confirmed** transaction — see `finalWithdraw`). * * SECURITY INVARIANT: Even if an escrow is buggy or malicious and * over-authorizes claims, the audited balance for any token lineage can never @@ -492,11 +492,15 @@ export class TBC777 extends TBC20 { /** * Claim a final withdrawal from the given escrow revision. * - * Performs an explicit terminal-revision check via `computer.last(rev)`. The - * supplied revision must be the current live tip of the escrow at the moment - * of evaluation. This check is intentionally omitted from regular - * `withdraw()` and `getBalance()` so those paths stay free of transient - * observations and remain deterministic under chain extension. + * Performs an explicit terminal-revision check via `computer.last(rev)`. + * InnerComputer only returns a definite `last` when the tip is **spent in a + * confirmed transaction** (mempool-only or unspent tips invalidate). Typical + * flow: record `finalWithdraws` on the tip, `delete` that tip UTXO, wait for + * confirmation, then `finalWithdraw(tipRev)`. + * + * This check is intentionally omitted from regular `withdraw()` and + * `getBalance()` so those paths stay free of transient observations and + * remain deterministic under chain extension. */ async finalWithdraw(rev: Rev) { return this._withdraw(rev, true) diff --git a/packages/TBC777/src/tbc777m.ts b/packages/TBC777/src/tbc777m.ts index 154c7fdb5..fb9a27463 100644 --- a/packages/TBC777/src/tbc777m.ts +++ b/packages/TBC777/src/tbc777m.ts @@ -207,9 +207,12 @@ export class TBC777M extends TBC20 { * Returns the amount this specific token instance (`_id`) is allowed to * withdraw according to the escrow's `finalWithdraws` list. The amount is * only returned if the supplied `rev` is the final (last) revision of the - * escrow (checked via `computer.last(rev)`); otherwise returns `0n`. Only - * matching entries for the token’s root and id are summed. Intended for - * one-time final payouts (e.g. winner-takes-all). + * escrow (checked via `computer.last(rev)`). + * + * Note: InnerComputer `last` invalidates the transition when the tip is still + * unspent or only spent in the mempool. Callers must use a tip whose spend is + * confirmed (same protocol as TBC777 `finalWithdraw`). When `last` succeeds + * but points at a different rev, this returns `0n`. */ static async computeFinalWithdraw(rev: string, _id: string, _root: string): Promise { if ((await computer.last(rev)) !== rev) return 0n diff --git a/packages/TBC777/test/tbc777.test.ts b/packages/TBC777/test/tbc777.test.ts index 1ec56a8ab..e395d3538 100644 --- a/packages/TBC777/test/tbc777.test.ts +++ b/packages/TBC777/test/tbc777.test.ts @@ -67,6 +67,8 @@ describe('TBC777 - Programmable Escrow Token (No-Inflation Focus)', () => { export ${escrowSource} export ${tbc777Source} `) + // Confirm module so InnerComputer.load(mod) (semantic isEqualTo) is stable. + await minter.db.wallet.restClient.mine(1) await ensureFunds(minter, 20e8) @@ -336,13 +338,16 @@ describe('TBC777 - Programmable Escrow Token (No-Inflation Focus)', () => { // Tip is still live/unspent → computer.last(firstRev) returns undefined → // InnerComputer invalidates with the framework non-existent-state message. // (The domain "can only be claimed from last revision" message only appears - // after the tip has been deleted, when last() returns a concrete tip rev.) + // after the tip has been deleted *and that spend is confirmed*, when last() + // returns a concrete tip rev.) expect(e.message).to.include( 'Accessing non-existent on-chain state inside a smart contract is forbidden', ) } await minter.delete([lastRev]) + // last() requires a confirmed spending input of the tip — mine the delete. + await mine() await t.finalWithdraw(lastRev) expect(t.amount).to.eq(FRESH_TOKEN_AMOUNT) @@ -952,6 +957,8 @@ describe('TBC777 - Programmable Escrow Token (No-Inflation Focus)', () => { } await minter.delete([lastRev]) + // last() requires a confirmed spending input of the tip — mine the delete. + await mine() await t.finalWithdraw(lastRev) expect(t.amount).to.eq(FRESH_TOKEN_AMOUNT) diff --git a/packages/TBC777/test/tbc777m.test.ts b/packages/TBC777/test/tbc777m.test.ts index e71ba9c53..e87726680 100644 --- a/packages/TBC777/test/tbc777m.test.ts +++ b/packages/TBC777/test/tbc777m.test.ts @@ -44,6 +44,8 @@ describe('TBC777M', () => { await Promise.all([black.faucet(10e8), white.faucet(1e8), minter.faucet(10e8)]) await ensureFunds(minter) mod = await minter.deploy(`export ${TBC20}`) + // Confirm module deploy so any InnerComputer.load of `mod` is stable. + await mine() }) it('Should work for a naive escrow', async () => { diff --git a/packages/chess-app/src/components/ChessBoard.tsx b/packages/chess-app/src/components/ChessBoard.tsx index ecb529726..762b396dd 100644 --- a/packages/chess-app/src/components/ChessBoard.tsx +++ b/packages/chess-app/src/components/ChessBoard.tsx @@ -211,7 +211,8 @@ function WinnerModal(data: { Prize: {data.wagerAmount} tokens

- Click "Withdraw Tokens" on the board to collect your prize. + Click "Withdraw Tokens" on the board to collect your prize. Withdrawal waits + until the final game transaction is confirmed on-chain.

) : ( @@ -282,11 +283,10 @@ function ActionButtons({ )} - {/* Withdraw Tokens: shown only when the chess contract declares a payout - for my token AND my token has not already claimed against the current - chess revision. On checkmate the winning move sets withdraws - atomically, so the winner can withdraw immediately — no separate - "Claim Win" round trip. */} + {/* Withdraw Tokens: shown when the chess contract declares a payout for my + token and it has not been claimed yet. The helper waits for the game + tip to confirm before TBC777 withdraw (InnerComputer history must be + confirmed). */} {isPayoutEligible && !hasWithdrawn && (