Persist attester & PTC duties per epoch in the blockdb#752
Open
pk910 wants to merge 2 commits into
Open
Conversation
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.
Persist attester & PTC duties per epoch in the blockdb
Summary
Slot detail pages resolve attester-committee and PTC (payload-timeliness-committee)
duty mappings from the in-memory
EpochStatscache. After an epoch finalizes thoseduties are deleted (too expensive to keep), so finalized slot pages can no longer
show which validators were in each attestation/PTC committee.
This PR persists the resolved duties for each finalized epoch in the existing
blockdb (s3 / pebble / tiered) as a new object type, and serves them lazily so
the slot page itself never hits the blockdb on render. Committee membership and PTC
members are fetched on demand when a user expands an attestation's aggregation bits
or a PTC group.
Motivation
the duty data is gone once
EpochStatsis pruned.reshuffle per epoch — too expensive for a page render.
Design
Per-epoch duties object (not per-block)
A per-block section was the first idea but is unworkable: a block at slot
Sreferencesattestation slots in
[S-32, S-1], which can fall in the previous epoch, and blocksare written to the blockdb at finalization — by which point the previous epoch is already
finalized and its duties pruned, with no cheap way to reload them mid-finalization.
Storing one object per epoch, written at that epoch's own finalization (when its
EpochStatsValuesare still in scope), sidesteps this entirely. Each epoch object isself-contained and written exactly once.
Storage layout (per backend)
A semantic
DutiesEngineinterface lets each backend store natively:…/{firstSlot}_duties. Deterministic layout:a 40-byte header + a flat array of 48-bit global validator indices in
(slotIndex, committee, position)order + a PTC section. Committee byte offsets are apure function of the active-validator count (same
SplitOffsetmath the indexer uses),so any
(slot, committee)is addressable with a ranged read — no index table.[ns][firstSlot][type][slotIndex]) so a single slot is apoint lookup, with no need to load the whole epoch.
The copier converts between the two via the engine-agnostic
EpochDutiesstruct.48-bit (6-byte) indices are future-safe (protocol indices are
uint64) at a third lessstorage than full
uint64. Estimated overhead at 1M validators: ~6 MB/epoch(~0.5 TB/yr) — comparable to the Gloas payload+BAL already stored per block.
Lazy, no page-load blockdb hits
The slot page renders committee indices and aggregation bits (both derived from the block
itself) but not validator lists. A cached JSON endpoint resolves duties on expand:
The
ChainServiceaccessor decides the source per slot: in-memoryEpochStatsforunfinalized epochs (via
GetOrLoadValues, which also restores duties from theunfinalized_dutiestable during the pruned-but-not-yet-finalized window), and the blockdbduties object for finalized epochs.
What's included
Storage layer
blockdb/types/duties_format.go— DUTY object format, encode/decode, deterministicSplitOffset-based offsets, per-slot pebble helpers; round-trip tests (incl. 1M-validatoruneven splits).
blockdb/types/engine.go—DutiesEngineinterface.blockdb/{s3,pebble,tiered}/duties.go— backend implementations.blockdb/blockdb.go—SupportsDuties()+ wrapper methods, engine detection.Producer
indexer/beacon/dutiesexport.go—BuildEpochDuties(resolvesEpochStatsValues→global indices) +
writeEpochDutiesToBlockDb.finalization.goandsynchronizer.goin the same concurrent write phase asblock bodies.
Consumer
services/chainservice_duties.go—GetSlotCommittees/GetSlotPtc(cache-or-blockdb).handlers/slot_duties.go— cached/slot/{slot}/dutiesendpoint.handlers/slot.go— eager duty resolution removed from the page build.templates/slot/attestations.html,ptc_votes.html— lazy fetch on expand.Tooling
cmd/dora-utils/blockdb_copy.go(+blockdb_copy_duties.go) — duties copy pass (--no-duties).cmd/dora-utils/blockdb_sync.go— local duty computation from beacon state at each epochboundary, with
--no-dutiesand--duties-onlyflags.Debug / observability
block_duties_sizecolumn on theepochstable (pgsql + sqlite migrations).db.UpdateEpochDutiesSizerecords the stored size after each epoch's duties are written./debug/cacheBlockDB tab gains a Duties row (count / size) alongside Beacon Blocksand Exec Data.
Config
indexer.disableBlockDBDuties(envINDEXER_DISABLE_BLOCK_DB_DUTIES) to opt out.Incidental fix: PTC participation on small validator sets
When the validator set is small, PTC seats are duplicated (a validator holds several of the
512 seats). The old per-aggregate percentage divided votes by the unique validator count
(lower than the seat count), which produced >100% participation. Participation is now
seat-based — voted seats over
PTC_SIZE(512) — so it stays within 0–100%, and thelabels/tooltips reflect the seat model.
Database changes
20260623000000_epoch-duties-size.sql(pgsql + sqlite): addsepochs.block_duties_size bigint DEFAULT 0. Backfilled going forward as epochs finalize.Testing
blockdb/types/duties_format_test.go— encode/decode round-trips for the packed object,per-slot pebble blobs, and the 48-bit index list, including uneven committee splits at 1M
validators (verifies the writer/reader
SplitOffsetagreement).go build ./...,go vet, andgo test ./blockdb/...pass.Notes & follow-ups
blockdb-syncCLI computes duties locally from the beacon state (active-validatorselection + RANDAO-mix seeding). This path could not be runtime-verified here — worth a
spot-check that its committees match a node's
/committeesfor a sample epoch beforerelying on it for backfills.
epochstable,so objects it writes won't appear in the DB-derived
/debug/cachesize stats.🤖 Generated with Claude Code