Skip to content

Improve state transition performance#782

Open
pk910 wants to merge 1 commit into
masterfrom
pk910/statetransition-performance
Open

Improve state transition performance#782
pk910 wants to merge 1 commit into
masterfrom
pk910/statetransition-performance

Conversation

@pk910

@pk910 pk910 commented Jul 1, 2026

Copy link
Copy Markdown
Member

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+, epochState reconstructs an epoch's pre-state by replaying the
parent epoch's blocks (tryReplayFromParentState) and/or advancing slots
(PrepareEpochPreState). Profiling a full-epoch replay on hoodi (1.43M
validators) showed three dominant costs:

Component Baseline (hoodi)
Per-block committee shuffling (attestation processing) ~13.0s
Per-block full-state HTR (state-root verification) ~16.5s
Epoch transition (process_epoch) ~0.9s
Total ~29.5s

Changes

1. statetransition/committees.go — cache + parallelize the shuffle

Every 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).

  • Compute the full permutation once per seed (computeShuffledList),
    memoized in the committee cache keyed by seed (current + previous epoch
    coexist), and slice each committee out of it.
  • Replace the per-call map with a flat []Root of round source hashes.
  • Parallelize the independent per-position updates within each shuffle
    round across all cores (rounds stay sequential).

Result: per-block attestation processing ~300ms → ~6ms.

2. statetransition/rewards.go — map-free epoch transition

process_rewards_and_penalties built three map[ValidatorIndex]bool
participant sets (and process_inactivity_updates a fourth) over the full
registry 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.goHashTreeRoot helper

Adds (*StateTransition).HashTreeRoot(state) so callers have one entry point
for the state root.

4. epochstate.go — verify the replay once, not per block

The 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_slot hint. Each
canonical block already carries its own post-state root, so:

  • Feed each block's stated state_root forward as the next block's hint.
  • Verify once, after the last block, that the final state hashes to the last
    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_roots and therefore
changes 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:

hoodi (1.43M) mainnet (2.2M)
Baseline ~29.5s (per-block HTR-bound, similar profile)
After ~2.9s ~2.4s

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 committee
shuffle 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
GetTree vs HashTreeRoot divergence in dynssz. Root-caused and reported with
a fix in pk910/dynamic-ssz#191; once released it enables shaving the
remaining ~0.9s/mainnet final + skipped-slot HTRs.

Comment thread indexer/beacon/statetransition/committees.go
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