Skip to content

Conversation

@7layermagik
Copy link

@7layermagik 7layermagik commented Feb 8, 2026

Summary

Two commits addressing correctness in epoch stake seeding and reward distribution.

1. Fix startup epoch stake seeding (9fb3668)

At startup, setupInitialVoteAcctsAndStakeAccts() was seeding EpochStakesPerVoteAcct and TotalEpochStake using raw delegation.StakeLamports from the AccountsDB scan. This is incorrect — epoch-effective stakes have warmup/cooldown applied via Delegation.Stake(epoch, stakeHistory), and the manifest already stores these correctly.

Changes:

  • Replace raw stake aggregation with global.EpochStakes(block.Epoch) from the epoch stakes cache (loaded by buildInitialEpochStakesCache() from the manifest)
  • Add fail-fast guard if epoch stakes cache is empty at startup
  • Add diagnostic log comparing raw scan total vs epoch-effective total
  • Remove dead code: SetVoteStakeTotals(), voteStakeTotals field, voteStakeTotalsMu mutex in GlobalCtx

Files: pkg/replay/block.go, pkg/global/global_ctx.go

2. Add reward burn accounting (aa8adfd)

Mithril's reward distribution path previously panicked on per-record account-level failures during stake reward payout. Agave and Firedancer instead treat these as "burned" rewards — the sysvar advances by distributed + burned, but capitalization only increases by distributed. This is consensus-critical accounting.

Changes:

  • DistributeStakingRewardsFromSpool now returns (distributedLamports, burnedLamports)
  • Per-record failures that burn (matching Agave/FD):
    • ErrNoAccount from GetAccount → burn (Agave AccountNotFound)
    • Stake state decode failure → burn (FD fd_stake_get_state != 0)
    • Non-stake state → burn (FD !fd_stake_state_v2_is_stake)
    • Lamports overflow → burn (Agave ArithmeticOverflow)
    • Stake state re-encode failure → burn (Agave UnableToSetState)
  • Non-ErrNoAccount GetAccount errors (IO/storage corruption) still panic — these should not be silently burned
  • Callsite: EpochRewards.Distribute(distributed + burned), Capitalization += distributed
  • Nil filtering before StoreAccounts to exclude burned records

Files: pkg/rewards/rewards.go, pkg/replay/rewards.go

🤖 Generated with Claude Code

7layermagik and others added 2 commits February 7, 2026 19:38
Startup was populating block.EpochStakesPerVoteAcct and TotalEpochStake
from raw delegation.StakeLamports sums, which can differ from epoch-effective
stakes when delegations are warming up or cooling down. The correct values
were already loaded into global.EpochStakes() by buildInitialEpochStakesCache()
but weren't being used. This seeds from the epoch stakes cache instead,
matching Agave's get_epoch_stake syscall behavior and the existing epoch
boundary code path.

Also removes dead SetVoteStakeTotals (written but never read).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@7layermagik 7layermagik force-pushed the fix/startup-epoch-stake-seeding branch from d1c1f4e to aa8adfd Compare February 8, 2026 06:45
@smcio smcio merged commit 5908d62 into dev Feb 9, 2026
1 check passed
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.

2 participants