Skip to content

fix(staking): pay_one_signer iterates sorted delegator keys#713

Merged
github-actions[bot] merged 1 commit into
mainfrom
fix/staking-pay-one-signer-deterministic-order
May 25, 2026
Merged

fix(staking): pay_one_signer iterates sorted delegator keys#713
github-actions[bot] merged 1 commit into
mainfrom
fix/staking-pay-one-signer-deterministic-order

Conversation

@satyakwok
Copy link
Copy Markdown
Collaborator

@satyakwok satyakwok commented May 25, 2026

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 + 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:

  • 🟢 Low — docs, tools/, tests/, CI configs, dependency patch bumps in dev-only crates, comments
  • 🟡 Medium — non-consensus production code (RPC handlers, network plumbing, observability, ops scripts)
  • 🟠 High — consensus-critical crates (sentrix-core, sentrix-trie, sentrix-staking, sentrix-bft), block_executor, apply_block_*, state_root path
  • 🔴 Critical — Voyager activation, fork-height changes, hard-fork rollouts, anything that flips env vars on mainnet

Required by tier

🟢 Low — minimum bar

  • CI green (tests + clippy + audit + gitleaks)

🟡 Medium — adds

  • New public function or behavioural change has at least one corresponding #[test] in same PR
  • Brief description of how this was tested (manual run, integration test, etc.)

🟠 High — adds

  • Regression test that fails on main and passes with this change — paste test name in PR body
  • Designed against documented invariant (link the audit/runbook/design doc)
  • Fresh-brain review by someone other than the author (per the consensus-change review checklist)
  • Single conceptual unit per PR (no bundling — bundling consensus changes burned us on v2.1.12 → 2026-04-25 livelock)

🔴 Critical — adds

  • Testnet rehearsal completed with success criteria + log evidence linked here
  • Bake window observed: minimum 2h on testnet at the same configuration before mainnet
  • Coordinated rollback plan documented in PR body — exact commands operator runs if it fails
  • Operator sign-off at activation moment (not just PR approval — separate moment for the actual flip)

Test plan

Rollback plan

Related

Summary by CodeRabbit

  • Bug Fixes
    • Resolved nondeterminism in delegator reward distribution that could cause inconsistent state across processes. Staking rewards are now distributed deterministically.

Review Change Stack

`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).
@github-actions github-actions Bot enabled auto-merge (squash) May 25, 2026 14:04
@codecov
Copy link
Copy Markdown

codecov Bot commented May 25, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@satyakwok satyakwok self-assigned this May 25, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 25, 2026

📝 Walkthrough

Walkthrough

The PR fixes non-deterministic behavior in StakeRegistry::pay_one_signer by eliminating HashMap iteration ordering issues. The delegator-pool reward distribution now derives a sorted list of delegator addresses and uses that deterministic order both to compute the total_delegated denominator and to iterate delegators when computing reward shares. This ensures consistent rounding and distribution results across all processes.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~8 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description provided is extensive and contextual, but does not follow the required template structure and is missing several mandatory checklist items required for High risk-tier consensus-critical changes. Complete all sections of the risk-tier template: fill Summary section, populate Test plan with specific regression test name, provide Rollback plan, and ensure all High-tier checklist items are addressed with concrete evidence (test names, design doc links, review assignment).
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: sorting delegator keys in pay_one_signer for deterministic iteration.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/staking-pay-one-signer-deterministic-order

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot merged commit c58eeef into main May 25, 2026
17 checks passed
github-actions Bot pushed a commit that referenced this pull request May 25, 2026
Ships PRs #711 (trie prune race augment-walk), #712 (B3b total_minted
self-heal), #713 (pay_one_signer sorted delegators), #714
(collect_reachable depth-aware empty check). See CHANGELOG entry for
the per-PR narrative and the 2026-05-24 incident root-cause analysis.
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