PRT Contracts Review#273
Draft
GCdePaula wants to merge 20 commits into
Draft
Conversation
GCdePaula
force-pushed
the
feature/sling-node
branch
from
July 21, 2026 13:55
e7b0efd to
d8cc329
Compare
Document the on-chain tournament lifecycle, commitment geometry, clock model, trust boundary, and review scope with the contracts as the source of truth. Add the audit map, review ledger, Clock design record, and documentation corrections that motivate the subsequent implementation and test campaign. Mark unverified leads and deferred decisions explicitly instead of presenting them as established guarantees.
Pay the winning claimer at most one bond, burn the residual tournament balance, and make repeated successful recovery a permissionless no-op. Preserve both the claimer and the full balance when recipient payment fails so settlement remains retryable. Cover zero-balance, failed-payment, repeated-recovery, payout-cap, and downstream Dave settlement behavior. This closes the path where terminal recovery could brick application settlement or recycle losing reserves into a Sybil budget.
Charge timeout winners from live remaining time, introduce MatchClocks as the two-clock policy boundary, and derive proof and timeout settlement from one outcome classifier. Preserve the inclusive equality boundary as double elimination. Replace front-loaded match effort with a non-bankable response discount applied after each eligible response. Cover legal clock shapes, deadline boundaries, time conservation, both winner orientations, sealed-leaf settlement, and the absence of pairing or re-entry grants.
Keep the two-level deployment migration explicitly gated and leave canonical production geometry unchanged. Freeze the historical three-level parameters, bind legacy suites to that provider, and quarantine role-oriented history under characterization tests. Separate behavior fixtures from canonical conformance so tests inject the shape they need and future parameter updates do not rewrite unrelated expectations. The temporary two-level production switch and its exact revert are omitted from the cleaned history.
Require the stored Match state to exist before sealing an inner match and creating its child tournament. Preserve existence-before-phase error ordering and pin the missing-match regression at the public entry point.
Remove the unused duration subtraction helper and a duplicate deployment import. Document that block-number calibration outside Ethereum is experimental and can be invalidated by changes in the underlying chain timing model. This cleanup changes no supported dispute-game behavior and leaves alternate base-layer timing as an explicit deployment gate.
Remove test-only topology counters from production, separate child balance recovery from parent winner propagation, and make the work reserve and price caps explicit in Bond. Characterize pooled reserves without relying on mutable instrumentation. Bound recipient callback gas, skip zero-value calls, avoid copying return data, and retain retry semantics for failed terminal payments. Prove that rejecting, reverting, data-amplifying, and gas-exhausting recipients cannot reenter or block unrelated tournament progress.
Measure the successful advance, leaf and inner seal, timeout, child propagation, and child elimination branches with storage-aware fixtures. Replace stale refund allocations with the first complete reviewed action table and pin its derived reserve arithmetic. Retain both winner orientations and first-write paths so later implementation changes fail the witnesses when they cross a reviewed margin.
Remove the ineffective zero check on a hashed Match identifier and derive existence from the mapped Match state instead. Apply the same stored-state guard to child-resolution paths and getMatchCycle so absent and deleted matches cannot look valid through a nonzero keccak slot. Pin default-ID hashing, unlinked child tournaments, and nonexistent and deleted cycle queries.
Add a release-pinned gas runner, scoped coverage recipe, independent bisection parity model, configurable full Merkle trees, and inspectable single-level tournament fixtures. Build stateful positive and rejection campaigns plus coherent two-level claim and tournament fixtures. Exercise recursive winner propagation, double elimination, late entry, clock carryover, sequential children, strict response deadlines, and proof-timeout convergence without importing canonical geometry.
Pin raw state encoding, validation order, events, errors, and compatibility witnesses before changing the implementation. Derive explicit phases and sealed views from the existing representation without adding storage. Centralize branch selection, revealer rotation, divergence encoding, sealing parity, and winner attribution behind phase-oriented verbs. Retire legacy helpers after all callers migrate, preserve the deployed ABI and semantic storage layout, and compare both gas-sensitive branch orientations.
Extend coherent claim fixtures and trace concurrent child populations without assuming root identity determines the selected parent side. Add sealed-state existence checks and harden the retained gas witnesses. Characterize callback isolation and reproduce the production refund cap with an independent formula over base fee, priority fee, balance, and recipient behavior. Record the exact supported refund boundary and the remaining proof-gas limits.
Reject zero or no-code implementation, parameter-provider, and state-transition dependencies, and reject a zero canonical maximum allowance. Validate geometry tables, row tiling, level counts, height and stride bounds, and root extent in test-owned fixtures. Exercise strict four-level recursion, sequential dangling populations, and an exhaustive bounded one-level delay model. Document these as configuration and finite-model evidence rather than a multi-level liveness theorem.
Raise the intentionally provisional leaf-proof allocation and derive every bond from positive-height match work plus the maximum terminal path. Remove the additional Sybil principal so reserves follow the reviewed gas table and work price cap automatically. Pin the terminal maximum, per-height work reserve, join deposits, refund cap, and automatic propagation from Gas into Bond policy values.
Finish the authoritative Match phase API and sealed decoder, then separate single-clock arithmetic tests from pair-policy tests. Centralize the response discount, existence-before-phase precedence, paused allowance boundary, and duration maximum; reject zero-height Match creation and remove dead duration helpers. Recalibrate advance and inner-seal refunds to 125,000 and 363,000 gas while the terminal maximum remains unchanged. Complete role-guard, phase, parity, final-state child-selection, raw-state preservation, and compatibility coverage.
Consolidate the Clock and Match design outcomes, confirmed findings, test assessment, compatibility witnesses, coverage qualifications, and refund calibration procedure after the implementation campaign. Separate historical checkpoints from the current contract and test candidate. Record exact pre-rebase gas provenance and make the planned Foundry-version bump an explicit recalibration and release gate.
Use the release version pinned by the rebased CI and container as the authoritative refund-calibration runner. Keep the recipe fail-closed so unpinned or dirty measurements remain diagnostic only.
Distinguish the final Solidity and test tree from the clean calibration candidate, and close the Foundry-version rebase trigger with an exact official 1.5.1 record. Document the unchanged 18-witness recommendations, compatibility hashes, 231-test dispute gate, 204-test coverage map, and four downstream integration tests.
GCdePaula
force-pushed
the
feature/contracts-review
branch
from
July 21, 2026 14:57
bbb0280 to
ac058eb
Compare
GCdePaula
requested review from
guidanoli and
stephenctw
and removed request for
guidanoli
July 21, 2026 17:07
Move the completed internal dispute-game review out of the contract workspace into a dated, explicitly historical archive. Extract the durable refund-accounting proof, Foundry test architecture, and gas calibration procedure into maintained documents. Move the storage-layout normalizer into contract tooling and add a compatibility-hash recipe. Rework root and nested agent guidance into cumulative, scoped routers, add the nested Claude import shim, and remove stale pre-sling repository orientation.
…tion The GC's elimination gate compared the expired side's overdue time against the opponent's static allowance where the reviewed contracts compare live remaining time, equality eliminating both: two 100-block clocks racing from one instant die together on-chain around block 100, but the node waited past block 200 to sweep (node-audit.md finding 8, inherited from the pre-rewrite reference). The Hero's win gate had the mirror defect - opponent-timed-out alone - so inside the eliminate-both zone it submitted winMatchByTimeout transactions the contract rejects (NeitherClockHasTimedOut). Mirror MatchClocks.classifyTimeoutAt once as tournament::classify_timeout and decide both paths with it: the GC eliminates exactly on EliminateBoth and the Hero fires only when its own side wins. The Lua client carried both gates verbatim; align its gc and honest strategy the same way (its clock gains remaining()). Unit tests pin the contract's bisection and leaf-race boundary matrices, both hero-side zones, and the shared-deadline sweep. Folded in, opportunistically: - adopt the contracts' outputsMerkleRoot naming across the Rust side: Settlement.outputs_merkle_root(_proof), the settlement_info columns (fresh stores are born v4; migrate_to_latest refuses a pre-rename store loudly with the wipe-and-rebuild message), and machine.outputs_merkle_root_with_proof(). - the STF-upgrade campaign plan (docs/plans/stf-upgrade.md), staging the next node campaign's TODOs.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR completes a security, correctness, documentation, test-coverage, and readability review of the PRT dispute game.
The campaign began as a review of the contracts under
prt/contracts/, explicitly excluding the implementation semantics of the state-transition function. It produced:The deployed
TournamentABI and semantic storage layout are preserved. Production bytecode changes intentionally, so deployment artifacts and CREATE2-derived addresses must be regenerated before release.The off-chain clients ride along only where the contract changes demand it: the Rust node and the Lua client now mirror the contracts' shared timeout classification, and the node adopts the contracts'
outputsMerkleRootnaming in its settlement types and schema. No other node behavior changes.Main security and correctness changes
Terminal bond settlement
Terminal recovery is now bounded, retryable, and idempotent:
This closes two related findings:
The
feature/sling-nodetarget already uses staged consensus and catches recovery failure during staging. This PR does not introduce that staged design; it fixes the underlying Tournament behavior and adapts the regression coverage to the target'sstageTournamentResult/acceptStagedTournamentResultlifecycle.Bounded recipient callbacks
Both refund and terminal-payment callbacks are now bounded:
PartialBondRefund.retremains ABI-compatible but is always empty;This keeps arbitrary recipient behavior outside the critical progress path while preserving the existing per-tournament reentrancy protection.
Clock conservation and timeout partition
The sealed-leaf timeout path previously charged the prospective winner from its stored allowance rather than its live remaining time. Because both clocks run after leaf sealing, this could restore time already consumed during the leaf race.
It also created a window in which both timeout victory and double elimination were valid. Transaction ordering could therefore decide between incompatible outcomes.
The fix:
NONE,ONE_WINS,TWO_WINS, andELIMINATE_BOTH;winnerRemaining == loserOverdue, toELIMINATE_BOTH;MatchClocksis now the pair-level policy boundary.Clockremains responsible for one-clock arithmetic at an explicit observation instant.The off-chain clients are aligned with the same classification. The Rust node's garbage collector previously compared the expired side's overdue time against the opponent's static allowance and could wait up to a full allowance longer than the contract to sweep a doubly dead match; it now eliminates exactly on
ELIMINATE_BOTH, equality included. Its Hero previously claimed a timeout win on opponent expiry alone, which inside the eliminate-both zone submits awinMatchByTimeoutthe contract rejects; it now acts only when the classification names its side the winner. The Lua client's gc and honest strategy carried the same two gates and received the identical fix.Back-loaded response effort
The former design granted
matchEffortwhen commitments were paired. A fresh or late commitment could therefore receive bankable time before doing any useful work.Pairing and survivor re-entry now grant no time. Instead, every successful bisection response, including final leaf or inner sealing, receives a non-bankable response discount:
The response must still arrive before the starting balance expires. The discount can reduce the time charged for the successful response, but it can never increase the clock above its previous balance.
The public configuration shape is preserved. On Ethereum,
matchEffortis now the scalar five-minute response budget, currently 25 blocks, rather than a front-loaded sum over commitment heights.Match existence and phases
Match existence previously relied in part on checking whether a keccak-derived Match ID hash was zero. That check was dead code: a valid keccak output is not a meaningful storage-existence sentinel.
Existence now comes from mapped Match state. Nonexistent and deleted matches are rejected consistently before phase-specific interpretation.
The Match library now derives explicit phases from the existing representation:
UNINITIALIZEDBISECTINGREADY_TO_SEALSEALEDThe refactor centralizes:
No new Match storage is introduced. The raw tuple, sealed encoding, Tournament ABI, storage layout, selectors, and event signatures remain compatibility surfaces.
Factory and configuration validation
Factories now reject zero or no-code implementation, provider, and state-transition dependencies. The canonical provider rejects a zero maximum allowance.
Test-owned validation covers:
Canonical geometry validation remains a test/deployment responsibility rather than a recurring runtime cost.
Refund, gas, and bond policy
Refunds are bounded, best-effort subsidies for altruistic validators. They are not:
The selected action allocations are:
ADVANCE_MATCHWIN_MATCH_BY_TIMEOUTELIMINATE_MATCH_BY_TIMEOUTSEAL_INNER_MATCH_AND_CREATE_INNER_TOURNAMENTWIN_INNER_TOURNAMENTELIMINATE_INNER_TOURNAMENTSEAL_LEAF_MATCHWIN_LEAF_MATCHWIN_LEAF_MATCHis a provisional subsidy for the exercised ordinary-proof path, not a universal upper bound across input and proof classes.The join bond is now derived automatically from the work it may need to fund:
The current maximum terminal allocation is the 948,000-gas leaf seal plus leaf win path.
WORK_PRICE_CAPremains 50 gwei and the priority-fee cap remains 10 gwei.There is no separately maintained or additional Sybil-principal constant. Losing reserves may fund successful bounded progress refunds; the residual present at successful terminal recovery is burned after paying at most one bond. This does not claim a fixed positive burn for every losing commitment.
The calibration procedure is deliberately manual but reproducible and fail-closed. It checks the exact Foundry release, configuration, clean-tree state, environment overrides, and all retained witnesses before recommending a table change.
See GAS-CALIBRATION.md and REFUND-DESIGN.md.
Test architecture
The dispute gate grew from 42 to 231 tests.
Behavior tests are no longer coupled to the canonical Top/Middle/Bottom constants. Tests inject the geometry required by the property being exercised, including one-, two-, and four-level shapes.
The historical three-level suites remain as frozen characterization coverage rather than defining all PRT semantics.
New coverage includes:
The bounded delay models are executable finite checks, not a proof of general multi-level liveness.
See TEST-REPORT.md.
Documentation
The PR adds a code-aligned description of the current protocol in dispute-game.md and expands dimensioning.md, the glossary, READMEs, AGENTS guidance, and Solidity documentation.
The permanent review corpus includes:
The documentation distinguishes current behavior, historical checkpoints, accepted policy, deferred work, and non-claims.
Findings disposition
arbitrationResultdocumentationCompatibility and deployment
Preserved:
TournamentABI;Match.Statetuple;Intentionally changed:
PartialBondRefund.ret, which is now always empty;Compatibility witnesses:
Before deployment, regenerate and review all deployment artifacts and CREATE2-derived addresses.
The checked-in canonical contracts still use the historical three-level table:
The selected two-level deployment table remains an integration gate:
This PR does not switch canonical geometry and does not claim node or Lua-client agreement with the selected table.
Validation
The final tree was validated with official Forge
1.5.1-v1.5.1, Solc 0.8.30, optimized IR, 200 optimizer runs, and Prague EVM.Results:
gc_matchandmulti_sybile2e scenarios (timeout elimination and timeout win against live sybils) passed against a freshly rebuilt devnet.The post-rebase Forge 1.5.1 rerun required no further Gas or Bond changes. The retained advance and full-proof inner-seal witnesses reproduced the selected 125,000 and 363,000 allocations.
Branch mappings remain investigative because optimized-IR source maps and the deliberate coverage exclusions do not provide a complete semantic branch metric.
Explicit non-claims and deferred work
This PR does not claim:
Ethereum is the supported timing target. Non-Ethereum registrations remain experimental. In particular, the current Arbitrum calibration is not valid for its parent-chain
NUMBERsemantics.State-transition halt and exception semantics remain documented as a deferred lead for the separate workstream already addressing them.
Review guide
The commits are intentionally sequenced and are best reviewed chronologically. The highest-risk production changes are:
5f3a071- bounded and idempotent terminal bond settlement;f7d74a9- Clock behavior, timeout partition, and back-loaded effort;b5eca6a- callback isolation and recovery/propagation separation;d61d3c2- Match phase and bisection refactor;301e438- final refund calibration and bond derivation;ac3bea0- final Clock and Match invariant centralization;185098e- the off-chain companion: node and Lua-client timeout-classification alignment, plus theoutputsMerkleRootnaming adoption.The geometry, lifecycle, recursive, callback, and compatibility test commits around these changes form the evidence fence. The final documentation and Forge 1.5.1 commits record the release-verification provenance.