Make tournament events authoritative, and teach the node to follow them - #274
Merged
Conversation
Make the contracts authoritative for match phase, orientation, clocks, timeouts, and tournament results through total typed observer views, while retaining the event fold for history-derived structure. Introduce rich Rust and Lua domain models, strict pinned adapters, pure Hero and GC planners, late intent fulfillment, and single-mutation dispatch. Persist finalized event progress independently and rebuild the latest tail as disposable range-fetched state. Pin semantic reads to the sampled head and rely on contract mutators to revalidate stale or raced work. Replace receipt-driven submission with an exclusive-signer, fire-and-forget transaction lane at the latest mined nonce, supporting exact rebroadcast and bounded fee-bumped replacement. Align sealed-leaf timeout handling across the contracts and both clients, close fail-closed validation gaps, and add exact-boundary, replacement, and recovery evidence.
- establish scoped agent guidance and reorganize living engineering docs - make devnet and machine artifacts self-verifying across setup and tests - align harness and measurement provenance with current workflows
Merge the semantic observer into ITournament and retire the legacy surface: the ten raw views are gone, arbitrationResult yields to tournamentStanding in DaveConsensus, and the parent protocol pair becomes one typed innerResult that maps the inner winner to its contested parent commitment and carries the carryover allowance as a duration. CommitmentJoined indexes its commitment, and the event counters stay as documented fetch-pruning and range-integrity affordances. Reconcile the rebased observer campaign onto the merged contract review, make the observer methods thin compositions over promoted MatchClocks and Match helpers, and give standing, elimination, and propagation one finished-instant and winner-expiry authority. Tests reach retired raw state through a vm.load inspector pinned to the storage layout, keeping the suites raw-layout witnesses while closure predicates become independent oracles. Both clients migrate in kind, the Rust legacy-shadow scaffolding is deleted, the gc scenarios pin their adversarial pairing by construction, and the chain-recording oracles are regenerated. Gas allocations are unchanged; witnesses re-pin the smaller runtime's interim headroom.
Result staging moves no value: tryRecoveringBond is an explicit, permissionless action, removing the last value-moving call from the progress and settlement paths. Terminal recovery pays one bond plus a tenth of the forfeited residual and burns the other nine tenths, keeping the recycling bound at ninety percent of the pooled reserves while giving defenders a bounty that is zero in undisputed operation. Restate terminal conservation and the anti-recycling argument, rework the staging tests to prove acceptance advances before any recovery, and record the node-side recovery action and the self-healing batch-submission lane as a later campaign.
Close campaign steps 9 and 10: the plan and decision log become frozen provenance, the dashboard records the executed retirement and the declined aggregate views, and the living specification stays in dispute-game.md and node-architecture.md. The node-architecture text drops its reference to the deleted raw-getter shadow. Serialize the STF FFI recipes with one forge thread: the machine snapshot helper is not safe for concurrent writers sharing one scratch cache, the same footgun the leaf-gas runner already guards.
The proving verbs claimed "prove without applying" for the revert check, so prove_transition reported the discarded rejected state as the closing slot's post-transition hash while the builder emits the restored checkpoint as that leaf. The hero's pre-send check then vetoes its own winLeafMatch every tick (PostStateMismatch) and the honest node forfeits stf_revert disputes by clock. Apply the revert after building the witness, in all three encodings of the misconception: the production prover, the toy model, and the prototype differential oracle (blind here because both proof paths were wrong the same way). Pin the shape in-crate with a yield-image test asserting builder, prover, and prototype agree that the closing leaf is the restored pre-feed state.
tryRecoveringBond was the one mutator left without a capability-view twin: its consumer had to rebuild the gate from the standing arm, a joined-commitment inference, and the balance, with the winning claimer unobservable. Factor the classification into one private view shared by the mutator and the new bondRecovery() function - TOURNAMENT_RUNNING and NO_WINNER are the revert arms, RECOVERED the no-op arm, RECOVERABLE carries the claimer and the payment a successful recovery transfers. Emit BondRecovered on the terminal payment, completing the economics surface next to PartialBondRefund. No storage change (layout hash unchanged); ABI and bytecode hashes move by exactly the added view and event. Behavior of the mutator is preserved arm for arm, pinned by three lifecycle tests including the rejected-payment retry.
Record the review-round resolutions: settlement stays off the wave (exactly-once steps whose content derives from finalized data, owning the base nonce while the wave fills above), fees go fully stateless (fresh market quote every tick; the recorded 1.1x last-sent guard is dropped as a compounding hazard under wave reshuffles, its corner covered by estimator headroom and clock allowances; no dedup memo either - the mempool or builder already arbitrates duplicates and replacements, so the lane carries zero mutable state), and recovery planning becomes stateless over chain reads (CommitmentJoined submitter logs for discovery, the bondRecovery view for capability, an in-memory scan frontier for termination). Block-builder submission is the expected production transport; correctness never depends on builder trust.
A concurrent build-devnet rebuilds the bundle in place, and the node's blockchain_reader tests read it through bare canonicalize().unwrap() - a race that presented as an unnamed ENOENT panic. The fingerprint already brackets the rebuild as a completeness marker; make the bracket airtight (drop it before the first artifact deletion) and have the test helpers check it before resolving paths, failing with the build-devnet/doctor pointer instead of a stack trace. Also ignore the stf FFI gate's stray prt/contracts/logs litter where it falls: the fingerprint's hash_contract_files uses git ls-files --exclude-standard, so one ignore entry retires both of that file's failure arms (fingerprint staleness and accidental commit).
Delete the replacement slot and its fee machinery: the retained floor, the 12.5% bump ratchet, the rebroadcast fingerprint, and the underpriced retry loop. The mempool (or block builder) is the authority on duplicates and replacements; the lane reads the mined nonce at latest, quotes the market fresh, signs, sends, and reports the pool's verdict - "already known", "replacement underpriced", and a stale nonce are benign, and callers re-derive their complete intent every tick (docs/plans/self-healing-batch-submission.md). submit_wave() lands the batch shape: consecutive nonces from the mined count, position is priority, per-transaction verdicts never abort the tail. The single submit() remains as the settlement path's one-call wrapper. The anvil-backed tests now pin the new contract: pool-side dedup, underpriced-wait instead of forced eviction, prefix inclusion shifting the base, reorg reuse, and restart invisibility.
The one-action-per-tick constraint dies with the stateless lane. The hero tick now yields its full wave contribution - the prepared hero action first, then every currently legal cleanup innermost-first - and the epoch manager concatenates the pending settlement step (base nonce, undisputed phases only) ahead of it and submits everything through submit_wave. Position is priority: hero-before-cleanup and settlement-never-behind-dispute-work are nonce order now, not arbitration. EpochWritePlan, the one-gc-intent tick, and the hero's own submission path are deleted; ArenaSender becomes a pure request factory and the manager's loop owns all submission. No intra-wave dependency filter: one live match yields at most one intent and an eliminable child's tournament is never recursed into, so a plan cannot invalidate its own suffix; reverts stay the safety net if a future planner change breaks that argument.
The recovery planner rides every wave behind dispute and settlement work: discovery is one CommitmentJoined log scan filtered by the indexed submitter, capability is the bondRecovery() view (the same classification tryRecoveringBond acts on), and termination is a block cursor plus candidates retiring on terminal dispositions - all in memory, rebuilt by a boot rescan, so losing state costs a rescan and never a wrong action. Discovery is permissionless and therefore spoofable, and paying a spoofed candidate would let attacker code burn the gas limit every tick. Candidates are verified before any send by comparing their ERC-1167 prelude to a storage-known root tournament's, which also authenticates the submitter topic: on a genuine clone it is the msg.sender of a real join, which an attacker cannot forge. View reads are free and skip verification. multi_sybil now proves the lane end to end: after settlement the root tournament's balance drains to zero - one bond back plus a tenth of three forfeited sybil residuals, the other nine tenths burned.
Oracle-anchored audit round 2 (six dimensions, adversarial verification, completeness critic; recorded in docs/plans/ node-audit.md): the recovery scan cursor now advances on finalized instead of the reorg-able latest head, matching the reader convention; verify_candidates removes entries per verdict so a transient get_code_at error no longer discards the unprocessed remainder; the BondDisposition mirror gains the finding-4-class source-parse pin; a Failed wave verdict logs at error level (a stalled nonce tail while clocks run is not a warning); the prototype oracle's revert predicate aligns to production's RX_REJECTED; and three docs describing the deleted one-write-per-tick lane now describe the wave. The Lua-mirror parity pass came back clean at full granularity. Two leads recorded, not closed: the funding envelope (pool reserves gas_limit x fee cap across the wave) and post-inclusion revert observability.
Replace the recovery planner's discovery: instead of scanning CommitmentJoined logs chain-wide by the indexed submitter and verifying candidates as genuine clones before any send, walk each unretired epoch's dispute tree root-down - the root address from our own storage (written from the trusted DaveConsensus stream), children from each trusted tournament's own NewInnerTournament events over finalized blocks. One bondRecovery() read per tree node then decides; the claimer answers "did we join and win" from the contract's own records, so no join history is needed. The scanned design's verification argument held under audit, but its posture was wrong: it admitted an attacker-writable candidate set and made a filter's correctness load-bearing forever. A candidate set derived from the node's own knowledge needs no filter - the topic scan, the unverified queue, and the clone-prelude machinery all delete, and the audit's two recovery findings dissolve with them. The rejected alternative and its lesson are recorded in docs/plans/bond-recovery-redesign.md.
The view asserted cross-view clock shapes the timeout mutators never check: classifyTimeoutAt is total by design and the mutators gate on its outcome, so the view was stricter than the mutation path it fronts. As corruption tripwires the asserts sat on the wrong side of the read/write divide - they could not stop a corrupt transition, only punish observers, and the node observes every live match through this view every tick: one impossible shape would have blinded the honest hero across every match while all clocks ran. Classify the stored clocks as they are, and observe canonical zeros for the one shape clock arithmetic cannot process (an uninitialized clock, which no transition path creates). Shape invariants stay enforced where transitions create the shapes; assertLeafRace and assertInnerSeal delete with their only caller. The observer tests flip from pinning the panic to pinning what the total view reports on each odd shape. ABI and storage-layout hashes are unchanged; the bytecode shrink cheapened the seal path across a rounding boundary, so SEAL_LEAF_MATCH's interim retained headroom moves to 2,000 (recorded in the 2026-08-04 repin record; allocations stay frozen for the deferred acceptance run).
Emit absolute match elimination schedules, build one recursive event-authoritative dispute, and restrict contract observations to standings and the Hero path. Retire the redundant fold and stale chain-recording oracle.
GCdePaula
requested review from
guidanoli and
stephenctw
and removed request for
guidanoli
August 10, 2026 10:44
GCdePaula
marked this pull request as ready for review
August 10, 2026 10:46
Move durable invariants into living documentation, retain only active review aids, include the devnet fingerprint in release archives, and remove stale tooling references.
guidanoli
reviewed
Aug 10, 2026
Move the leaf-seal counter with the event counters, confine raw slot reads to white-box test probes, and document deployment-generation compatibility.
State transaction priority as a same-tick property, explain the intentional empty lane during join finality gaps, and document successful log completeness as part of the configured RPC trust boundary. Align the Solid cursor and child-validation wording with the implementation.
stephenctw
approved these changes
Aug 12, 2026
Collaborator
There was a problem hiding this comment.
I went through the Solidity surface against the review guide (ITournament / Tournament / MatchClocks, plus the refund docs).
What I checked and am happy with:
eliminableAtlines up with the inclusiveELIMINATE_BOTHboundary for both clock shapes, and the emit/cancel path (create, advance, leaf seal, delete, inner seal) looks rightbondRecovery/tryRecoveringBondstay on the same arms, including the bounty formula and the failed-recipient retry casetournamentStandingandinnerResultshare the same finish/expiry authority, so parent reads shouldn’t drift
I also re-read on the latest tip — the reserve-doc payment wording looks consistent with the code, and the earlier review threads on counter placement / named returns look settled.
guidanoli
reviewed
Aug 12, 2026
guidanoli
reviewed
Aug 13, 2026
Expose the trusted factory parameter table without repeating total depth in tournament clones. Validate the compiled root stride and machine span before the node opens its database, and report factory deployment fingerprints.
Make the root justfile authoritative for the CI and doctor Foundry version, and have the setup action query that pin after installing Just.
guidanoli
reviewed
Aug 15, 2026
guidanoli
left a comment
Collaborator
There was a problem hiding this comment.
Looking great! Just raising one minor nit.
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.
This PR redraws the contract-client boundary around one rule: each fact should have one production authority.
The tournament event stream now owns enumerable history and recursive structure: commitments, matches, child tournaments, and contract-authored match-elimination schedules. Narrow point views remain only where events are insufficient: tournament standing, the validator's one active match path, and terminal bond recovery. The Rust node builds those sources into one recursive
Disputeinstead of maintaining parallel event and view projections of the same state.The result removes the old
O(matches)point-read scan. Cleanup of clock-bearing matches is derived directly from events; tournament cleanup needs at most one standing read per relevant tournament; and match-detail reads follow only the Hero's recursive path.This PR also retains two earlier pieces of the same contract-client campaign:
stf_revert-shaped dispute.Why this shape
The earlier observer made both events and views describe candidate placement, live matches, clocks, phases, and recursive topology, then reconciled them on every tick. That was useful while the event model was incomplete, but it was not a good permanent abstraction: both answers came from the same contract and provider, so their agreement did not prove log completeness or provider honesty. It did add linear RPC work and a second authority for every fact.
The completed design keeps the useful boundaries while deleting that redundancy:
MatchCreated,MatchAdvanced, andLeafMatchSealedcarry the inclusive block at which that match becomes eliminable. A later response replaces the prior schedule; deletion cancels it.tournamentStanding()handles tournament closure and carryover; match views are read only along the validator's engaged path;bondRecovery()classifies terminal recovery.We deliberately keep the dispute in memory. The node persists the finalized raw event prefix and its cursor, reconstructs on restart, and retains one active recursive value. Moving this projection into SQLite, adding persistent collections, or creating aggregate contract views would add machinery before measurements justify it.
Finalized Solid and Latest Foam
The reader maintains one finalized Solid dispute. Each tick it advances that prefix, commits the recognized logs and cursor, deep-clones the result, and recursively extends the clone through a sampled Latest height to obtain disposable Foam.
Foam is never promoted, reverse-applied, compared with the previous tail, or persisted. The next tick derives it again from Solid. A reorg or mixed latest response may suppress or delay one eager action, but it cannot mutate the finalized prefix.
The action policy follows the consequence of being wrong:
Every mutation shares one exclusive-signer transaction lane. A Hero action outranks cleanup; a tick emits at most one of them. Recovery runs only when no dispute or settlement mutation is ready, so maintenance cannot occupy a nonce needed by defense. Pending recoveries are rediscovered across epoch rotation and restart without a durable queue.
Contract and compatibility changes
This is a coordinated, version-paired contract/client change:
ITournament;innerResult()replaces the former result pair;CommitmentJoined.commitmentis indexed;eliminableAt;LeafMatchSealed, and an event counter;bondRecovery()exposes the same terminal classification used bytryRecoveringBond().Storage layout and raw Match and Clock encodings are internal to a deployment generation: these contracts are not upgradeable, and no supported client reads raw slots.
leafMatchSealedCounttherefore lives with the other event counters, while the Solidity inspector and the one E2E clock probe follow the resulting layout. The storage fingerprint records implementation impact; it is not a promise to preserve slot numbers.This is an explicit fresh-deployment cutover. The immutable clone payload changes from
levels: uint64tokind: TournamentKind; theMatchCreatedandMatchAdvancedsignatures change, indexingCommitmentJoined.commitmentchanges its topics/data layout, andLeafMatchSealedis new. A fresh Tournament implementation,MultiLevelTournamentFactory, and dependent Dave bundle must deploy with matching bindings, Rust and Lua clients, and artifacts. Old and new implementations, factories, clones, event layouts, and persisted logs must not be mixed, and no live dispute crosses the cutover. There is no dual decoder.As a final repository-layout cleanup, the two papers now live together under
docs/papers/, while the repository-wide E2E harness lives undertest/e2e/; package-local tests remain with their components. Git records both PDFs and all 41 harness files as renames, preserving their history.Diff distribution (approximate)
Counts are grouped by subsystem from
next/3.0...HEAD; renames and cross-cutting files make the categories approximate.Review guide
The Solidity implementation is the protocol trust boundary: result-selection defects can become consensus failures, while clock or accounting defects can break liveness and resource bounds. The Rust node is a separate validator-safety boundary: a bad event transition, deadline decision, commitment, or proof can forfeit a dispute the honest validator should win.
Solidity first
Read
docs/dispute-game.md,docs/prt-refund-accounting.md, andprt/contracts/AGENTS.mdbesideITournament.sol,Tournament.sol, andMatchClocks.sol.Questions to verify
eliminableAtequal the mutator's timeout boundary, inclusively, after creation, advancement, and leaf sealing?LeafMatchSealedcomplete the event history, with every white-box slot probe following the intentionally internal layout?tryRecoveringBond()preserve its revert, no-op, retry, payment, and burn behavior arm-for-arm withbondRecovery()?Recursive node model
Read
docs/plans/recursive-dispute-reader.mdanddocs/node-architecture.md, then reviewtournament/dispute.rs,reader.rs, andobserver.rs.Questions to verify
NewInnerTournamentfrom an already trusted parent?Action policy, recovery, and STF
Review the Hero actor/planners, epoch manager, recovery planner, transaction lane, and the revert path in
engine/.Questions to verify
Validation
On the final local candidate, the following pass:
just checkjust prt-contracts::test-all: 272 dispute tests, 3 STF FFI tests, and 2 fuzzy STF tests with 256 runs eachjust test-prt-gas: 18 PRT gas tests and 12 leaf-proof FFI gas testsjust rollups-contracts::test: 5 testsjust test-lua-client: 62 testsjust lint-luajust rollups-tests::test-sealed-leaf-timeoutsjust test-rollups-echojust test-rollups-honeypotjust prt-contracts::compatibility-hashesjust rollups-contracts::build-devnetgit diff --checkThe compatibility report confirms the intended wire ABI, records the storage and deployment-bytecode impact, and places the Tournament runtime below the EIP-170 limit. The regenerated devnet bundle fingerprints the new deployment generation.
Out of scope