fix(challenger): call setAnchorState on resolved DEFENDER_WINS games#2372
fix(challenger): call setAnchorState on resolved DEFENDER_WINS games#2372leopoldjoy wants to merge 12 commits intomainfrom
Conversation
The AnchorStateRegistry was never being updated by any off-chain component. In the multiproof system (AggregateVerifier), claimCredit() does not call closeGame() — unlike FaultDisputeGame where claimCredit() triggers closeGame() which calls setAnchorState(). This meant the anchor root was never advancing after proposals finalized. This fix integrates a best-effort setAnchorState() call into the BondManager lifecycle. After each successful advance_game() tick, if the game is past NeedsResolve and resolved as DEFENDER_WINS, we send a permissionless setAnchorState(game) tx to the ASR contract. The call is self-validating on-chain (checks finalization, airgap delay, DEFENDER_WINS status, and staleness) so premature calls revert harmlessly and are retried on the next tick. Changes: - Add setAnchorState(address) to IAnchorStateRegistry sol! bindings and encode_set_anchor_state_calldata() helper - Add anchor_state_registry_address config to BondManager, CLI, and config - Add try_anchor_update() method called after each advance_game() success - Add anchor_update_tx_outcome_total metric (success/error/skipped) - Add 6 unit tests covering all anchor update code paths
🟡 Heimdall Review Status
|
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Address review feedback: - Cache the on-chain game status on TrackedGame after the first successful RPC read, avoiding redundant status() calls on every poll tick during the airgap delay period (status is immutable after resolution). - Warn at startup when anchor-state-registry-address is configured but bond-claim-addresses is empty, since the anchor updates require the bond manager to function.
… try_resolve Address review feedback: - Call try_anchor_update on the Ok(Some(_)) path before the game is removed from tracking. This gives a last-chance anchor update attempt when the bond lifecycle completes, preventing permanent loss of the update if the airgap delay is longer than the DelayedWETH delay. - Cache the game status in try_resolve when the game is already resolved (status immutable), so try_anchor_update on the same tick can skip the redundant status() RPC round-trip.
Relax the NeedsResolve phase guard in try_anchor_update so that games with a cached status (known to be resolved) can still attempt the anchor update even when the phase hasn't advanced past NeedsResolve. This fixes an edge case where a DEFENDER_WINS game is marked NotClaimable (bond recipient not in our claim set after resolution) and removed before the anchor update is attempted — the phase guard was blocking the last-chance try_anchor_update call because set_phase was never reached.
Cache the game status after we successfully submit a resolve() tx, not just on the 'already resolved' path. Without this, self-resolved DEFENDER_WINS games that are NotClaimable would miss their anchor update because cached_status was None and the NeedsResolve phase guard would trigger.
…f config Remove the --anchor-state-registry-address CLI parameter and all associated config plumbing. The AnchorStateRegistry address is now read from each game contract via anchorStateRegistry() and cached per-game on TrackedGame, eliminating the risk of misconfiguration.
Review SummaryThe implementation is well-structured and follows established crate patterns. The Outstanding concern: anchor update lost on game removalThe most important open issue (flagged in prior inline comments): when the on-chain airgap delay is longer than the This is a correctness issue for the stated goal of advancing the anchor root. Consider either:
Notes on prior review commentsSeveral earlier inline comments (status not cached on success resolve path, Minor observationThe PR description references CLI/config/service changes ( |
Summary
The
AnchorStateRegistrywas never being updated by any off-chain component. In the multiproof system (AggregateVerifier),claimCredit()does not callcloseGame()— unlikeFaultDisputeGamewhereclaimCredit()triggerscloseGame()which callssetAnchorState(). This meant the anchor root was never advancing after proposals finalized.Root Cause
FaultDisputeGame.claimCredit()→ callscloseGame()→ callssetAnchorState()✅AggregateVerifier.claimCredit()→ does NOT callcloseGame()❌closeGame,close_game,setAnchorState, orset_anchor_stateexist in any.rsfile in the repoFix
Integrates a best-effort
setAnchorState()call into theBondManagerlifecycle. After each successfuladvance_game()tick, if the game is pastNeedsResolveand resolved asDEFENDER_WINS, we send a permissionlesssetAnchorState(game)tx to the ASR contract. The call is self-validating on-chain (checks finalization, airgap delay,DEFENDER_WINSstatus, and staleness) so premature calls revert harmlessly and are retried on the next tick.Changes
anchor_state_registry.rs— AddsetAnchorState(address)toIAnchorStateRegistrysol! bindings andencode_set_anchor_state_calldata()helperlib.rs(contracts) — Re-exportencode_set_anchor_state_calldatabond.rs— Addanchor_state_registry_addresstoBondManager,anchor_update_completeflag toTrackedGame, andtry_anchor_update()method called after eachadvance_game()successcli.rs— Add--anchor-state-registry-address/BASE_CHALLENGER_ANCHOR_STATE_REGISTRY_ADDRESSCLI argconfig.rs— Wire throughanchor_state_registry_addressservice.rs— Pass config toBondManager::new()metrics.rs— Addanchor_update_tx_outcome_totalcounter (success/error/skipped)driver.rs(tests) — Updatedefault_bond_manager()for new parameterTesting
6 new unit tests covering all anchor update code paths:
anchor_update_skipped_when_no_asr_configuredanchor_update_skipped_for_needs_resolve_phaseanchor_update_skipped_for_challenger_winsanchor_update_sends_tx_for_defender_winsanchor_update_retries_on_failureanchor_update_not_retried_after_successAll 39 tests pass, clippy clean with
-D warnings.Future Work
Consider adding
closeGame()toAggregateVerifier.claimCredit()in a contract upgrade for defense-in-depth (mirrors theFaultDisputeGamepattern).