fix(staking): pay_one_signer iterates sorted delegator keys#713
Merged
github-actions[bot] merged 1 commit intoMay 25, 2026
Merged
Conversation
`pay_one_signer` was the last reward-path function still iterating `self.delegations.values()` directly. `slash()` was hardened in PR #551 to sort by delegator address before iterating, exactly because `HashMap::values()` order is randomised per-process via SipHash seed. The reward-distribution path missed the same treatment. In practice the per-share math here is already order-independent (each delegator's share is computed from the full delegator_pool and full total_delegated, then summed into `delegator_rewards` via saturating_add, which is associative). So this is NOT a proven cause of the 2026-05-24 STATE-FP `fp` divergence — that drift sits elsewhere (likely in coinbase / total_minted accumulation, still being tracked). What this commit fixes is the structural asymmetry: two functions in the same crate, doing pro-rata distribution against the same HashMap, should reason about iteration order the same way. Leaving one sorted and the other random invites future regressions where someone adds an order-dependent operation downstream and only one path is safe. Lifts the same sorted-keys pattern from `slash()` (line 556) into `pay_one_signer`. Cost: one Vec<String> + sort per call. Reward path already does heavier work (u128 multiplications + map writes) so the sort cost is irrelevant at the call frequency (~4 calls per block).
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
📝 WalkthroughWalkthroughThe PR fixes non-deterministic behavior in Estimated code review effort🎯 2 (Simple) | ⏱️ ~8 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
15 tasks
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.
pay_one_signerwas the last reward-path function still iteratingself.delegations.values()directly.slash()was hardened in PR #551 to sort by delegator address before iterating, exactly becauseHashMap::values()order is randomised per-process via SipHash seed. The reward-distribution path missed the same treatment.In practice the per-share math here is already order-independent (each delegator's share is computed from the full delegator_pool and full total_delegated, then summed into
delegator_rewardsvia saturating_add, which is associative). So this is NOT a proven cause of the 2026-05-24 STATE-FPfpdivergence — that drift sits elsewhere (likely in coinbase / total_minted accumulation, still being tracked).What this commit fixes is the structural asymmetry: two functions in the same crate, doing pro-rata distribution against the same HashMap, should reason about iteration order the same way. Leaving one sorted and the other random invites future regressions where someone adds an order-dependent operation downstream and only one path is safe.
Lifts the same sorted-keys pattern from
slash()(line 556) intopay_one_signer. Cost: one Vec + sort per call. Reward path already does heavier work (u128 multiplications + map writes) so the sort cost is irrelevant at the call frequency (~4 calls per block).Summary
Risk tier
Check ONE:
sentrix-core,sentrix-trie,sentrix-staking,sentrix-bft),block_executor,apply_block_*,state_rootpathRequired by tier
🟢 Low — minimum bar
🟡 Medium — adds
#[test]in same PR🟠 High — adds
🔴 Critical — adds
Test plan
Rollback plan
Related
Summary by CodeRabbit