Improve state transition performance#782
Open
pk910 wants to merge 1 commit into
Open
Conversation
barnabasbusa
approved these changes
Jul 3, 2026
qu0b
reviewed
Jul 3, 2026
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.
Improve state transition performance (epoch replay ~30s → ~2.4s)
Summary
Speeds up the Fulu+ beacon state transition used by the indexer to reconstruct
epoch pre-states, from ~30s per epoch to ~2.4s (mainnet) / ~2.9s (hoodi) —
roughly 10–12× — on mainnet/hoodi-sized validator sets (~1.4–2.2M
validators). Output is byte-for-byte identical: every per-block post-state root
still matches the block header on both networks.
Background
For Fulu+,
epochStatereconstructs an epoch's pre-state by replaying theparent epoch's blocks (
tryReplayFromParentState) and/or advancing slots(
PrepareEpochPreState). Profiling a full-epoch replay on hoodi (1.43Mvalidators) showed three dominant costs:
process_epoch)Changes
1.
statetransition/committees.go— cache + parallelize the shuffleEvery committee in an epoch derives from the same swap-or-not permutation
(same attester seed). Previously each committee recomputed its slice of the
permutation using a per-call
map[uint32]source-hash cache (which alone cost~24s in map churn across an epoch).
computeShuffledList),memoized in the committee cache keyed by seed (current + previous epoch
coexist), and slice each committee out of it.
[]Rootof round source hashes.round across all cores (rounds stay sequential).
Result: per-block attestation processing ~300ms → ~6ms.
2.
statetransition/rewards.go— map-free epoch transitionprocess_rewards_and_penaltiesbuilt threemap[ValidatorIndex]boolparticipant sets (and
process_inactivity_updatesa fourth) over the fullregistry each epoch. Since validator indices are dense, these are replaced with
inline participation-flag checks and a single sum pass.
Result: epoch transition ~0.9s → ~0.3s.
3.
statetransition/statetransition.go—HashTreeRoothelperAdds
(*StateTransition).HashTreeRoot(state)so callers have one entry pointfor the state root.
4.
epochstate.go— verify the replay once, not per blockThe replay computed a full-state HTR after every block, purely to (a) verify
the post-state root and (b) provide the next block's
process_slothint. Eachcanonical block already carries its own post-state root, so:
state_rootforward as the next block's hint.block's stated root.
This is as safe as before: any incorrect intermediate root (e.g. an STF
divergence) propagates into the cached
state_roots/block_rootsand thereforechanges the final root, triggering the same fallback to an API state load. It
eliminates N−1 full-state HTRs (each ~0.5s hoodi / ~0.9s mainnet).
Results
Full epoch (32 slots) replay + final epoch transition:
Correctness
Verified with a per-block verification mode (HTR + compare against each block
header) over full epochs on both hoodi and mainnet (both Fulu):
every post-state root matches byte-for-byte (
mismatch=false). The committeeshuffle and map-free rewards changes are pure refactors of the existing
algorithms; the verify-once change does not alter the produced state, only how
often it is checked.
Follow-up (not in this PR)
A further win — a parallel, cached Validators-list HTR (~18× on the validators
subtree, which dominates the remaining single HTR) — is blocked by a
GetTreevsHashTreeRootdivergence in dynssz. Root-caused and reported witha fix in pk910/dynamic-ssz#191; once released it enables shaving the
remaining ~0.9s/mainnet final + skipped-slot HTRs.