Skip to content

IIP-59: recompute voter weights, drain voter-major - #82

Merged
envestcc merged 1 commit into
masterfrom
iip-59/v4-view-removal-voter-major
Aug 6, 2026
Merged

IIP-59: recompute voter weights, drain voter-major#82
envestcc merged 1 commit into
masterfrom
iip-59/v4-view-removal-voter-major

Conversation

@envestcc

@envestcc envestcc commented Aug 6, 2026

Copy link
Copy Markdown
Member

Brings the IIP in line with the implementation on iotexproject/iotex-core#4953. The previous revision described a design that has since been replaced.

What changed in the design

No committed weight table. The previous revision specified a (candidate, voter) weight table materialized into state at each era boundary, and required nodes to load it rather than recompute. That table is gone. Weights are recomputed on demand from bucket state.

What consensus commits is now the input, not the output: a per-delegate scalar snapshot plus a copy-on-write era window. The window exists because settlement mutates the very state it measures — a compound payout grows the bucket whose weight a later chunk would read — so without it the era's split would stop being a function of the state at the freeze height H. Section 4 is restructured accordingly: 4.1 the scalar snapshot, 4.2 the window, 4.3 on-demand recomputation.

Voter-major settlement. The drain walks the voter address space in 256 shards keyed on the first address byte, not the delegate list. A voter is paid once for everything they are owed across every delegate, in one combined transfer. Consequences, all specified:

  • the per-block budget unit changes from (candidate, voter) pairs to distinct voters;
  • a second bound limits index keys scanned, because the first address byte is grindable by an attacker generating keys — without it one stuffed shard could be read in full before the first voter is paid;
  • the last-positive-voter dust rule is removed. A shard walk over a per-shard-resolved membership has no notion of "last". Dust is swept at completion instead.

The payout clamp is now mandatory and normative. The numerator is a stateless recompute; the denominator is a frozen path-dependent accumulator. They can disagree. The clamp turns every disagreement into an under-payment rather than an over-payment, and section 9's residual sweep collects what the clamp and floor division leave behind.

Also in this revision

  • 1.1 replaces the multi-block owner-index seeding window with a single backfill at the activation block, including the ordering argument for why no era window can be open at that point.
  • 10 records a real gap rather than papering over it: an overrunning drain loses its window at the next freeze height, roughly one and a half epochs before overrun recovery detects it. The only guard is capacity sizing, hence the new EpochsPerRewardEra >= 2 requirement in section 14. Note this is not yet enforced in genesis validation in code.
  • 11.1 adds bool[] compounded as the authoritative routing discriminator. compoundBucketIds[i] == 0 is ambiguous because native bucket 0 is a real bucket, so consumers MUST NOT infer routing from it.
  • 12 renames the Web3 views and drops the return fields shaped around the retired entry list.
  • 14 marks VoterWeightSeedBatchSize deprecated and unused, and notes that capacity estimates made under the old pair-based unit remain safe but conservative.

Two corrections to pre-existing text

  • The snapshot hash domain separator is v2, not v1. v1 scoped a digest over the frozen (voter, weight) list — a preimage of an entirely different shape — and bumping keeps the domains disjoint.
  • Orphan refunds go to the rewarding fund's unclaimed balance, not its available balance. The old text did not match the implementation.

Removed normative text

Two requirements in the old section 4 directly contradict the implementation and are deleted:

  • "A node MUST load these entries at startup rather than recomputing them"
  • "Implementations MUST NOT instead commit a summary of a locally reconstructed table"

The property they were protecting is still obtained, by a different route — the Rationale now carries an entry explaining that agreement comes from the committed copy-on-write window rather than from a committed result.

Unchanged

Section 1's migration rule, sections 2, 3, 5, 6, 11.4, 11.5, 13, and Backward Compatibility. The settlement seed derivation is unchanged — same domain string, same parent-hash input; only its use changed, from an entry offset to a starting shard.

🤖 Generated with Claude Code

The previous revision specified a committed (candidate, voter) weight
table materialized at each era boundary, and a delegate-major drain that
walked that table. Both are gone.

Weights are now recomputed on demand from bucket state. What consensus
commits is the input, not the output: a per-delegate scalar snapshot plus
a copy-on-write era window that makes reads as of the freeze height H
reproducible even though settlement mutates the very buckets it measures.
Section 4 is restructured around that: 4.1 the scalar snapshot, 4.2 the
window, 4.3 on-demand recomputation.

Settlement walks the voter address space in 256 shards rather than the
delegate list, so a voter is paid once across every delegate they staked
with. That changes the per-block budget unit from (candidate, voter)
pairs to distinct voters, adds a second bound on index keys scanned
because the first address byte is grindable, and removes the
last-positive-voter dust rule -- a shard walk has no notion of "last".
Floor-division dust and clamp shortfalls are swept at completion instead.

Because a stateless recompute and a frozen path-dependent accumulator can
disagree, the payout clamp is now mandatory and normative: every
disagreement becomes an under-payment, never an over-payment.

Also in this revision:

- 1.1 replaces the multi-block owner-index seeding window with a single
  backfill at the activation block, with the ordering argument for why no
  era window can be open at that point.
- 10 records a real gap: an overrunning drain loses its window at the next
  freeze height, about 1.5 epochs before overrun recovery detects it. The
  only guard is capacity, hence the new EpochsPerRewardEra >= 2
  requirement in 14.
- 11.1 adds bool[] compounded as the authoritative routing discriminator;
  compoundBucketIds[i] == 0 is ambiguous because native bucket 0 is real.
- 12 renames the Web3 views and drops the entry-list-shaped return fields.
- Two corrections: the snapshot hash domain separator is v2, not v1 (v1
  scoped the retired (voter, weight) list digest), and orphan refunds go
  to the rewarding fund's unclaimed balance, not its available balance.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@envestcc
envestcc merged commit 3069176 into master Aug 6, 2026
@envestcc
envestcc deleted the iip-59/v4-view-removal-voter-major branch August 6, 2026 14:35
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