Skip to content

feat(replay): epoch boundary rewards, bit-exact at 807 -> 808 - #10

Merged
Mctursh merged 3 commits into
masterfrom
feat/per-slot-feature-gating
Sep 13, 2026
Merged

Mctursh merged 3 commits into
masterfrom
feat/per-slot-feature-gating

Conversation

@Mctursh

@Mctursh Mctursh commented Sep 13, 2026

Copy link
Copy Markdown
Owner

Replays the epoch crossing: inflation pool, points, commission split, partitioned stake distribution, and the EpochRewards sysvar lifecycle.

Verified against mainnet. Slot 349056000 computes
Hndh3PL56XTD7r24CgA6MLW13Z7tbWzPawLMkz6FRhMF, which matches the vote, and all 228 distribution blocks are consensus-verified. 885,548 stake rewards and 417 vote commissions match mainnet to the lamport. The post-distribution EpochRewards sysvar matches agave's own recorded values, down to the slot it deactivates on.

Five things here aren't obvious from agave's source and cost real time:

  • capitalization has to be tracked per slot and read at the boundary. Taking it from the seed snapshot made the pool 8,820,967 lamports too big, inflating every reward by 6e-8.
  • Clock's epoch, leader_schedule_epoch and epoch_start_timestamp only move at a crossing. Carrying them forward is right for every other slot, which is why 8,971 passing slots said nothing about it.
  • EpochRewards.total_rewards is the whole inflation pool, not what's paid. distributed_rewards starts at the vote commission (paid at the boundary) and accumulates per partition. Reading the source the other way round cost a full run.
  • vote_state_of gates on the owner alone, like agave's VoteAccount::try_from. An is_correct_size_and_initialized check rejects not-yet-resized 3731-byte V1_14_11 accounts and inflates every reward.
  • checkpoint metadata must commit in the same transaction as the checkpoint. Three separate commits meant a crash could leave meta from one chunk with a checkpoint slot from the previous one, and the resume then double-counted that chunk's burned fees.

Replays the epoch crossing: inflation pool, points, commission split,
partitioned stake distribution, and the EpochRewards sysvar lifecycle.

Verified against mainnet. Slot 349056000 computes
Hndh3PL56XTD7r24CgA6MLW13Z7tbWzPawLMkz6FRhMF, which matches the vote, and
all 228 distribution blocks are consensus-verified. 885,548 stake rewards
and 417 vote commissions match mainnet to the lamport. The
post-distribution EpochRewards sysvar matches agave's own recorded values,
down to the slot it deactivates on.

Five things here aren't obvious from agave's source and cost real time:

- capitalization has to be tracked per slot and read at the boundary.
  Taking it from the seed snapshot made the pool 8,820,967 lamports too
  big, inflating every reward by 6e-8.
- Clock's epoch, leader_schedule_epoch and epoch_start_timestamp only move
  at a crossing. Carrying them forward is right for every other slot,
  which is why 8,971 passing slots said nothing about it.
- EpochRewards.total_rewards is the whole inflation pool, not what's paid.
  distributed_rewards starts at the vote commission (paid at the boundary)
  and accumulates per partition. Reading the source the other way round
  cost a full run.
- vote_state_of gates on the owner alone, like agave's
  VoteAccount::try_from. An is_correct_size_and_initialized check rejects
  not-yet-resized 3731-byte V1_14_11 accounts and inflates every reward.
- checkpoint metadata must commit in the same transaction as the
  checkpoint. Three separate commits meant a crash could leave meta from
  one chunk with a checkpoint slot from the previous one, and the resume
  then double-counted that chunk's burned fees.
@Mctursh
Mctursh merged commit 723e172 into master Sep 13, 2026
2 checks passed
@Mctursh
Mctursh deleted the feat/per-slot-feature-gating branch September 13, 2026 20:05
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