Skip to content

fix: read attester statuses from chain in the staking summary - #128

Merged
aminsammara merged 2 commits into
mainfrom
amin/summary-onchain-status
Nov 4, 2026
Merged

aminsammara merged 2 commits into
mainfrom
amin/summary-onchain-status

Conversation

@aminsammara

@aminsammara aminsammara commented Nov 4, 2026 •

Copy link
Copy Markdown
Collaborator

The staking summary's exiting and zombie counts and its TVL came from deposit and withdraw event arithmetic pooled across every rollup. This replaces that with getStatus reads per attester per rollup.

What was wrong

  • zombieStakes was never counted. It was totalStakes − active − exiting, where totalStakes is deposits minus finalizes over all rollups. Any indexing gap, and every attester still validating on an older rollup, landed there.
  • exitingStakes paired initiate and finalize events by attester across rollups.
  • totalValueLocked was 200k × totalStakes minus slashes on the canonical rollup only. Attesters slashed on v5 that followed the latest rollup into v6 through the bonus instance kept their full 200k.

On mainnet the live API reported 3,632 total, 367 exiting and 106 zombie. Reading getStatus on all four rollups gives 3,533 total, 254 exiting and 70 zombie. Another 50 attesters are still validating on v5, and those ended up in the zombie count.

What it does now

  • src/utils/attester-status.ts:
    • for every attester in the deposit table, and every rollup in rollup_version, calls getStatus;
    • then reads GSE.effectiveBalanceOf(rollup, attester) for VALIDATING attesters and getExit(attester).amount for EXITING and ZOMBIE ones;
    • reads in sequential multicalls of 1,000 calls. On mainnet that's 7,246 attesters × 4 rollups in about 16s;
    • caches the result for 60s. A stale snapshot is served at once while one background read refreshes it, and a failed refresh keeps the previous snapshot. Only the first request after startup waits.
  • summary.ts builds exitingStakes, zombieStakes, totalValueLocked and activeValueLocked from that read.
    • activeStakes is still getActiveAttesterCount. It logs a warning if it disagrees with the canonical VALIDATING count from getStatus, which would mean the indexer is missing deposits.
    • New stats.legacyActiveStakes counts attesters validating on a non-canonical rollup.
    • New rollups[] gives a per-rollup breakdown.
  • Both dashboard sublines show "+N on older rollups" when that bucket isn't empty.

The ABIs for getStatus, getExit and getGSE are identical on all four mainnet rollups (v4, v4.x, v5, v6), and they share one GSE. I checked each against live attesters.

Testing

  • New unit tests for the reader and cache: yarn jest tests/unit/utils/attester-status.test.ts.
  • I ran readRollupStakeTotals against mainnet with the full attester set. It matches a direct getStatus sweep.
  • Indexer and dashboard typecheck clean, and the full indexer suite passes (39 tests).
  • Fixed in this PR as well:
    • failed-deposits.test.ts was failing on main because the jest mock for ponder didn't export inArray.
    • ponder.config.ts failed typecheck with TS2742 because tsconfig.json asked for declaration output. Nothing emits declarations (the only tsc call is --noEmit), so I removed declaration and declarationMap.

Notes

  • Multicall needs Multicall3 at its canonical address. Mainnet and Sepolia have it. A fresh local anvil without a fork doesn't, so the summary would fail there.
  • Provider details and the beneficiary overview still classify attesters through attester-state.ts, which uses events. They can move to the same reader in a follow-up.

The summary derived its exiting and zombie counts, and the headline TVL,
from deposit and withdraw event arithmetic pooled across every rollup.
Zombies were whatever was left after subtracting active and exiting from
deposits minus finalizes, so any indexing gap, and every attester still
validating on an older rollup, showed up as a zombie. On mainnet this
reported 367 exiting and 106 zombie sequencers where the rollups hold 254
and 70, and a TVL of 200k times the attester count with no slashes taken
off for attesters that followed the latest rollup through the bonus
instance.

The summary now calls getStatus for every attester that ever deposited,
on every rollup the registry has announced, and reads the stake from the
GSE for validating attesters and from the exit record for exiting and
zombie ones. Attesters still validating on an older rollup get their own
bucket, and the response carries a per-rollup breakdown. The read is a
few dozen multicalls, so it sits behind a 60s cache that serves the
previous snapshot while it refreshes.
@aminsammara
aminsammara requested a review from a team as a code owner November 4, 2026 12:17
failed-deposits.ts imports inArray from ponder, but the jest mock for
ponder only re-exported and, eq and or, so all 24 failed-deposits tests
threw before asserting anything.

tsconfig asked for declaration files, which makes tsc require every
exported type to be nameable; ponder.config.ts's default export infers a
type from a ponder internal path, so typecheck failed with TS2742.
Nothing builds the indexer with tsc (ponder loads the sources directly
and the only tsc call is --noEmit), so declaration output is dropped.
@aminsammara
aminsammara merged commit ee3fe51 into main Nov 4, 2026
5 checks passed
aminsammara added a commit that referenced this pull request Nov 5, 2026
After #128 went to prod the summary reported 2,541 validating attesters on the canonical rollup
while getActiveAttesterCount said 3,157, and 0 on v5 where the chain has 50. Active stake read
507M against the ~630M the GSE holds, and that is the dashboard's headline number.

The reader only asked about attesters in the indexer's deposit table, and that table is missing
about 670 attesters that are validating on chain (all 50 on v5, which deposited between blocks
26,229,331 and 26,253,638, and ~616 on v6). Why the indexer lacks them is a separate question; the
summary should not depend on it for the active set, which the rollup can list itself.

Each rollup's active set is now read with getActiveAttesterCount and getAttesterAtIndex and merged
into the attesters asked about, so validating counts and stake are complete whatever the indexer
holds. Exiting and zombie attesters have left the active set, so for those the indexer's list is
still the only source.

The two calls live in a separate ABI rather than ROLLUP_ABI: ponder.config.ts gives ROLLUP_ABI to
the Rollup contract, so extending it changes Ponder's build fingerprint and would force another
schema rebuild on deploy for a function no handler uses.
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.

1 participant