Skip to content

Tournament read-interface additions for the reference node - #284

Merged
GCdePaula merged 16 commits into
mainfrom
feature/block-finished-view
Aug 31, 2026
Merged

Tournament read-interface additions for the reference node#284
GCdePaula merged 16 commits into
mainfrom
feature/block-finished-view

Conversation

@GCdePaula

@GCdePaula GCdePaula commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

Adaptations to the tournament read API requested by the rollups-node team's alpha.4 review, plus the gas recalibration that review work surfaced. The production Solidity footprint is deliberately small. Diffstat over non-test contract sources: 6 files changed, +159/−25.

Read surface

  • tournamentStanding() gains finishedAt (accepted earlier), populates finalState for INNER_WINNER, and gains winnerExpiresAt (the first inclusive instant the inner winner becomes eliminable; fixed once the tournament finishes).
  • New commitmentStanding(root) per-commitment view: join record, claimer, and a clock snapshot with no per-block-varying field. A projection, not the alpha.3 getCommitment: raw Clock.State stays off the wire and eliminated commitments keep stale snapshots (liveness stays topological).
  • MatchAdvanced gains segmentStartPosition, so a finished match's bisection trajectory survives match deletion in the event history (topic0 changes; only possible pre-release).
  • TournamentDescriptor gains startInstant and allowance (the clone's immutable time envelope; joins close at startInstant + allowance, inclusive).
  • canStageTournamentResult() is total: ROOT_FAILED answers through a new isTournamentFailed flag instead of reverting; stageTournamentResult keeps the revert.
  • ERC-165 on Tournament (through the clones) and MultiLevelTournamentFactory.

Gas recalibration

Gas.WIN_LEAF_MATCH 4,420,000 -> 3,885,000: the old value predated the machine-yield check and the alpha-9 bump, which made the maximum-input leaf-proof path cheaper; the exact-relationship witnesses had been failing. Measured under the pinned release environment; dated record in docs/reviews/2026-08-27-prt-leaf-gas-recalibration/. Leaf bonds shrink accordingly (about 0.027 ETH at the height examples). The reproducible leaf gate's dependency-digest pin, stale since alpha 9, is fixed alongside.

@GCdePaula GCdePaula self-assigned this Aug 27, 2026
@GCdePaula
GCdePaula requested review from guidanoli and vfusco August 27, 2026 14:17
@GCdePaula
GCdePaula force-pushed the feature/block-finished-view branch from b07bb79 to 3408771 Compare August 27, 2026 22:48
@GCdePaula GCdePaula changed the title feat!(prt): expose finish instant in tournament standing Tournament read-interface additions for the reference node Aug 27, 2026
@GCdePaula
GCdePaula requested a review from stephenctw August 27, 2026 22:52
@GCdePaula
GCdePaula marked this pull request as ready for review August 27, 2026 23:31
guidanoli
guidanoli previously approved these changes Aug 28, 2026

@guidanoli guidanoli left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Solid changes! In particular, I really like that tournamentStanding no longer reverts for no-winner tournaments, and instead returns an extra Boolean value signaling failure.

Base automatically changed from feature/prt-machine-yield-check-sling to feature/prt-machine-yield-check August 28, 2026 15:28
Base automatically changed from feature/prt-machine-yield-check to main August 28, 2026 19:29
@guidanoli
guidanoli dismissed their stale review August 28, 2026 19:29

The base branch was changed.

Append finishedAt to the standing ABI, populate it from the existing finish-time authority, and update strict Rust and Lua consumers without a compatibility fallback.
The INNER_WINNER arm of tournamentStanding() now populates finalState
with the winner's claimed final state, which the view already loads to
map the parent commitment. Saves observers a per-tournament log query
and makes the winner arms uniform.

Breaking for strict observers: the field was previously canonical-zero
in this arm. The Rust observer discards the value (events own commitment
records; a zero claim is on-chain-representable, so no shape check), and
the Lua adapter cross-checks it against the folded join record.
A view named "can I stage" should answer, not revert: ROOT_FAILED now
returns (isFinished = true, isTournamentFailed = true, zeroed winner fields)
through a new isTournamentFailed flag instead of reverting with
TournamentFailedNoWinner. The internal classifier is total; the revert
posture moves into stageTournamentResult unchanged (same error, same
precedence).

The node's stage planner asserts loudly on isTournamentFailed, matching the
existing impossible-state idiom: a failed tournament on a defended epoch
means the protocol assumption broke.
MatchAdvanced gains segmentStartPosition (the same value the bisecting
projection reports), so a finished match's bisection trajectory survives
match deletion in the event history. The value is already in match
storage at emit time; the direction remains derivable from consecutive
events except when the waiting side's children are identical, which is
exactly the ambiguity the explicit field removes.

The Lua client folds the position as a per-match breadcrumb (seeded at
zero on creation) and cross-checks it against the live projections,
alongside the existing otherParent/leftNode breadcrumbs. The Rust node
needs only regenerated bindings: its domain event keeps the minimal
deadline-replacement shape.
TournamentDescriptor gains startInstant and allowance, appended so the
existing fields keep their positions. Both are clone-immutable creation
arguments with no read path until now; an inner clone's allowance is
inherited from the parent match clocks at seal time, and joins close at
startInstant + allowance under inclusive expiry (now documented on the
view).

The Lua domain descriptor carries and bounds-checks both fields; the
legacy compact reader decodes the widened tuple. The Rust observer
leaves them undecoded (Hero acts eagerly on current state) and only its
wire fixtures change.
commitmentStanding(root) projects one commitment's raw join record and
clock snapshot: joined flag, claimed final state, claimer, and either
the paused clock's frozen reserve or the running clock's fixed
inclusive deadline. Total over unjoined roots (canonical zeros), and
deliberately a snapshot rather than a status: storage keeps no
elimination tombstone or commitment-to-match link, so liveness stays
topological, and eliminated commitments keep their last written clock
(the alpha.3 caveat, now documented on the struct). Unlike alpha.3's
removed getCommitment, the raw Clock.State encoding stays off the wire
and the claimer is exposed.

No per-block-varying field: running-clock consumers derive remaining
time from the fixed deadline client-side, which suits pinned reads.

The Lua adapter gains the decode arm; the Rust node needs bindings
only: the sling has no consumer (join idempotency is event replay and
its response policy is eager).
TournamentStandingView gains winnerExpiresAt, populated only in the
INNER_WINNER arm: the first inclusive instant at which the winner
becomes eliminable and the standing degrades to
INNER_ELIMINABLE_WINNER_EXPIRED. The arm already evaluates exactly this
boundary through _winnerExpired, so the field adds no new arithmetic
authority, and it is fixed once the tournament finishes (the winner
clock freezes with the tournament). The consensus-consumed innerResult
protocol surface is untouched.

The boundary test now pins the reported instant as the exact flip
block: INNER_WINNER one block before it, eliminable at it. The Rust
observer and Lua adapter canonicality-check the field (nonzero exactly
for INNER_WINNER, strictly after finishedAt) and discard it, and the
legacy compact reader decodes the widened tuple.
Tournament answers supportsInterface for type(ITournament).interfaceId
(reachable through the clones), and MultiLevelTournamentFactory for
both type(IMultiLevelTournamentFactory).interfaceId and the base
type(ITournamentFactory).interfaceId, since type(I).interfaceId
excludes inherited functions. Any interface change flips the id, so
the answer doubles as an exact-generation gate for consumers,
mirroring DaveConsensus.

Applied last in this interface pass so the advertised ids are the
final shipped shapes. No client changes: nothing in this repo
consumes the gate.
…raint

Three commitments from the alpha.4 API review reply:

- epoch-lifecycle.md states that settlement never touches the bond path
  and why (consensus liveness must not depend on the tournament payment
  path; no recipient code on settlement transactions), and names the
  obligation it creates: every node implementation owns driving bond
  recovery per retired tournament, one bond per epoch at stake.
- epoch-lifecycle.md states the staging period's second role: the
  reaction interval in which application-layer foreclosure stops a
  decided-but-wrong result, freezing the epoch with its inputs reported
  as never finalized - an intended terminal state, not a stranded-value
  bug.
- joinTournament NatSpec warns at the decision point that the caller
  becomes the fixed recovery claimer and must be able to receive ETH
  within the gas-bounded recovery payment (comment-only: hash gate
  confirms only metadata fingerprints moved).
EXPECTED_DEPENDENCIES_SHA256 still described the pre-alpha-9 dependency
tree: the pin was last set by the runner extraction (f4695f4) and the
alpha-9 bump (98f355f) never updated it, so the reproducible leaf gas
gate rejected every correctly restored checkout. The new digest was
reproduced from a pristine soldeer.lock restore (wipe and reinstall
yield the same value).
The 4_420_000 allocation predated the machine-yield check and the
alpha-9 rollups-contracts bump, which together made the maximum-input
leaf-proof path cheaper; the exact-relationship witnesses have been
failing since. Under the pinned release environment (forge
1.5.1-v1.5.1, clean tree, matching dependency digests) the maximum
rounded recommendation is 3_885_000 (two-winning orientation; the
one-winning orientation rounds 1_000 lower and is recorded as alternate
slack). The selection adopts the recommendation exactly with zero
retained headroom.

Propagation: leaf terminal allocation 4_550_000 -> 4_015_000, leaf
refund cap 0.221 -> 0.19425 ether, and the leaf-height work reserves
and join bonds shrink accordingly (non-leaf values unchanged). Every
other action family's recommendation stayed within its configured
allocation. Constants-only: ABI and storage unchanged, bytecode and
deployment identity change.
A failed root is a documented terminal state, not a local
contradiction: the ticked Hero path already logs FailedNoWinner and
idles, and plan_stage_tournament_result also runs with no Hero
(Absent), where the assert turned a restart into a crash loop. Treat
isTournamentFailed like not-finished: log at error level and plan
nothing. stageTournamentResult's TournamentFailedNoWinner revert
remains the write-side guard.
The segment-start breadcrumb skipped the fold's defensive-cloning
convention (eliminable_at already clones through bint): ingestion
retained the caller's mutable bint and copy_match aliased fold storage
into accessor results. Clone at both seams, with regressions for both
mutation vectors.
The observer's winnerExpiresAt shape check only required nonzero for
INNER_WINNER, weaker than the stated canonicality rule and the Lua
adapter's check. The winner clock's allowance is positive, so a
canonical expiry strictly follows finishedAt; enforce it and pin the
equality and earlier-expiry rejections.
- The calibration record's network-admission section compared the
  complete-call gas to transaction limits; the right quantity is the
  Prague transaction estimate (5,078,866 units: 30.27% of the EIP-7825
  cap, 8.46% of the observed block limit). Calldata intrinsics sit
  outside the refundable seam, so the allocation is unaffected. Hash
  citations refreshed to the post-autosquash history.
- prt-refund-accounting.md: leaf terminal maximum 4,550,000 ->
  4,015,000 after the recalibration.
- epoch-lifecycle.md: the locked-value exposure is every retired
  tournament's balance (root and inner), not one bond per epoch.
- winnerExpiresAt docstrings state the canonical-zero-after-expiry
  behavior and the finishedAt + clockAllowance recomputation.
- ERC-165 docstrings now state exactly what the id fingerprints:
  declared function signatures only; a changed id proves a new
  generation, an unchanged id proves nothing, and binding pinning
  guards return shapes, struct layouts, and events.
- word_small documents its deliberate sub-2^63 envelope; the semantic
  event decoder keeps full uint64 range where history demands it.
@GCdePaula
GCdePaula force-pushed the feature/block-finished-view branch from 3408771 to 8a8be70 Compare August 28, 2026 19:41
@GCdePaula
GCdePaula requested a review from guidanoli August 28, 2026 19:45

@stephenctw stephenctw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@guidanoli guidanoli left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@GCdePaula
GCdePaula merged commit 5b035d7 into main Aug 31, 2026
9 checks passed
@GCdePaula
GCdePaula deleted the feature/block-finished-view branch August 31, 2026 13:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants