Commit 36e06e4
committed
feat(staking,poll): incremental voter weight view + snapshot at PutPollResult (IIP-59 PR 2/6)
Second PR of the IIP-59 protocol-native voter reward distribution
series (spec: iotexproject/iips#73, previous:
PR 1 — commissionRate schema + SetCommissionRate action).
Adds the per-epoch freeze that PR 3's GrantEpochReward will consume:
at PutPollResult (mid-epoch snapshot of the next-epoch active
delegate set), we now capture (a) the delegate's currently-set
commission rate and (b) the per-voter aggregated vote weight for
every delegate that will be paid next epoch. No reward-distribution
logic yet — GrantEpochReward still runs the pre-IIP-59 path; the
snapshot just sits in state until PR 3 reads it. Behavior is
unchanged pre-flag.
Design change vs. the earlier draft on this branch
--------------------------------------------------
The prior version of this PR scanned all buckets at PutPollResult
time to build the per-voter aggregate. That was O(all buckets)
inside a hot state.db call and, worse, required reading from the
contract staking indexer — a db independent of state.db — inside a
consensus mutation path. Both problems are gone in this redesign:
- A live in-memory VoterWeightView is maintained incrementally by
the native handlers and by the contract-staking receipt-driven
event dispatch. PutPollResult reads directly from the view (top-N
candidates only, O(voters-per-cand)), sorts by voter address
bytes, and writes a per-candidate blob.
- The consensus path (Handle → contractsStake.Handle) never
touches the contract staking indexer db. The view is fed through
the same receipt-driven dispatch that today updates
contractStakeView, via a `VoterDeltaSink` plumbed through
context (no widening of ContractStakeView interface, no
breakage for existing mocks/impls).
- The view is memory-only. On protocol Start, `CreateBaseView`
rebuilds it once by scanning current native buckets and asking
each contract-staking indexer for its current per-voter
aggregates. The durable half of the story is the frozen
per-epoch snapshot blobs written under `_voterWeightSnap`, not
the live view.
Storage layout
--------------
- New 1-byte namespace tag `_voterWeightSnap = 5` in the staking
namespace (protocol.go), following the existing _const / _bucket
/ _voterIndex / _candIndex / _endorsement pattern.
- Per-candidate blob key = `_voterWeightSnap || candIdentifier.Bytes()`
→ 21 bytes, mirroring `_voterIndex` / `_candIndex` layout.
- Blob payload = `stakingpb.VoterWeightSnapshot { repeated
VoterWeightEntry (bytes voter, bytes weight) }`. Entries are
written pre-sorted by voter address bytes; the marshalled output
is byte-deterministic given identical logical state — a load-
bearing invariant for the byte-equality skip in the writer and
for cross-node consensus.
Live view
---------
- `action/protocol/staking/voter_weight_view.go`: VoterWeightView
interface + baseVoterWeightView (map[candID]map[voterID]weight)
+ wrapVoterWeightView (change overlay). Wrap layers a change map
on top of the receiver so Snapshot returns cheaply and Revert
discards the overlay. Fork deep-clones for parallel working
sets. Commit folds change into base and prunes zero-weight
entries.
- Deterministic ordering: `Weights(cand)` returns
`[]VoterWeight` sorted by voter address bytes. No map iteration
reaches the consensus-visible output.
- Nil / zero delta is a no-op; negative deltas model bucket exit
(unstake, withdraw, ChangeCandidate off-ramp, TransferStake off-
ramp).
Native handler hooks
--------------------
Every native handler that changes a voter bucket's contribution to
a candidate calls `csm.ApplyVoterWeightDelta(cand, voter, Δ)`:
- handleCreateStake: +weightedVote
- handleUnstake: -weightedVote (skipped for self-stake buckets)
- handleChangeCandidate: -w on prev, +w on new (skipped when the
bucket was prev's self-stake bucket — it was never in the view)
- handleTransferStake: -w on old voter, +w on new voter (skipped
when bucket is already unstaked — it exited the view earlier)
- handleDepositToStake / handleRestake: +Δw on the same
(cand, voter) pair (skipped for self-stake buckets)
- handleCandidateEndorsement: no-op on the voter view (self-stake
gate flips SelfStakeBucketIdx but does not add/remove voter
buckets)
- handleStakeMigrate: -native_w on (cand, oldOwner); the new
contract bucket enters the view via the contract-staking event
sink below
Self-stake buckets are never in the view. The exclusion rule is
uniform: `ContractAddress == "" && Index == cand.SelfStakeBucketIdx`
— fixes review issue iotexproject#5 on the PoC PR (iotexproject#4811) where any Index=0
contract bucket was silently treated as self-stake.
Contract staking sink
---------------------
- `staking/protocol.go` wraps `contractsStake.Handle` with
`WithVoterDeltaSink(ctx, viewData.voterWeights)`. All V1/V2/V3
event processors get the sink through context.
- `systemcontractindex/stakingindex/vote_view_handler.go` pulls
the sink out of context and emits deltas on PutBucket/DeleteBucket:
fresh puts emit +w; owner or candidate changes emit split (-old,
+new); deletes emit -w. Muted buckets emit no delta (matching
the "not in view" invariant).
- All sink calls funnel through `VoterWeightView.Apply` which is
the single source of truth for the aggregation.
Snapshot entry point (staking)
------------------------------
- `action/protocol/staking/voter_weight_snapshot.go`:
`Protocol.SnapshotForEpochReward(ctx, sm, cands)` is the public
API poll calls. For each candidate in the next-epoch active list:
1. copies `staking.Candidate.CommissionRate` into
`state.Candidate.CommissionRate`. poll then persists this via
its existing `NxtCandidateKey` PutState — no extra state key
needed, the `shiftCandidates` path already promotes next →
current at the epoch boundary.
2. reads `vd.voterWeights.Weights(candID)` (already sorted),
encodes with `encodeVoterWeightSnapshot`, and writes an
incremental per-candidate blob under `_voterWeightSnap` —
with a byte-equality skip against the existing stored blob
so steady-state (voter set unchanged epoch-over-epoch) does
zero writes.
- Guarded on `FeatureCtx.NoVoterRewardDistribution` — a no-op
pre-fork.
- Empty entries after aggregation → `DelState`, not "write empty
blob." `VoterWeightsFromSnapshot` returns (nil, nil) for the
ErrStateNotExist path, so PR 3 sees the same result either way,
but keeping state clean matches how the other candidate-scoped
namespaces behave when nothing is left to store.
- State read errors other than ErrStateNotExist propagate wrapped.
Fixes review issue iotexproject#4 on the PoC — corruption / IO errors no
longer silently drop voter entries.
Reader for PR 3
---------------
- `VoterWeightsFromSnapshot(sr, candID) → ([]VoterWeight, error)`:
a single sm.State call, decoded from the persisted blob.
(nil, nil) for a candidate with no snapshot (either not yet
activated, or all voters left).
Tests
-----
- `voter_weight_view_test.go` (19 cases): base apply/aggregate/read,
sorting stability, wrap overlay semantics, Snapshot/Revert parity
against a Fork+Apply reference, Commit fold + zero-prune, nested
Wrap, per-candidate isolation, sink context plumbing.
- `vote_view_handler_test.go` (9 cases, systemcontractindex/
stakingindex): PutBucket fresh / same-cand-same-owner /
owner-change / candidate-change; DeleteBucket / missing-bucket
/ muted-bucket; nil-sink tolerance; sink integration that a
fresh-then-delete sequence balances to zero after Commit.
- `voter_weight_snapshot_test.go`: updated to construct a
populated view rather than seeding buckets, then assert
SnapshotForEpochReward writes the expected blob and
byte-equality skip triggers on unchanged inputs.1 parent 45a343a commit 36e06e4
19 files changed
Lines changed: 2185 additions & 19 deletions
File tree
- action/protocol
- poll
- staking
- stakingpb
- systemcontractindex/stakingindex
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
21 | 21 | | |
22 | 22 | | |
23 | 23 | | |
| 24 | + | |
24 | 25 | | |
25 | 26 | | |
26 | 27 | | |
| |||
212 | 213 | | |
213 | 214 | | |
214 | 215 | | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
215 | 226 | | |
216 | 227 | | |
217 | 228 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
57 | 57 | | |
58 | 58 | | |
59 | 59 | | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
60 | 65 | | |
61 | 66 | | |
62 | 67 | | |
| |||
124 | 129 | | |
125 | 130 | | |
126 | 131 | | |
| 132 | + | |
127 | 133 | | |
128 | 134 | | |
129 | 135 | | |
| |||
176 | 182 | | |
177 | 183 | | |
178 | 184 | | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
179 | 202 | | |
180 | 203 | | |
181 | 204 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
141 | 141 | | |
142 | 142 | | |
143 | 143 | | |
| 144 | + | |
144 | 145 | | |
145 | 146 | | |
146 | 147 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
84 | 84 | | |
85 | 85 | | |
86 | 86 | | |
| 87 | + | |
87 | 88 | | |
88 | 89 | | |
89 | 90 | | |
| |||
92 | 93 | | |
93 | 94 | | |
94 | 95 | | |
| 96 | + | |
95 | 97 | | |
96 | 98 | | |
97 | 99 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
3 | 3 | | |
4 | 4 | | |
5 | 5 | | |
| 6 | + | |
6 | 7 | | |
7 | 8 | | |
8 | 9 | | |
| |||
54 | 55 | | |
55 | 56 | | |
56 | 57 | | |
| 58 | + | |
| 59 | + | |
57 | 60 | | |
58 | 61 | | |
59 | 62 | | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
60 | 67 | | |
61 | 68 | | |
62 | 69 | | |
| |||
100 | 107 | | |
101 | 108 | | |
102 | 109 | | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
103 | 114 | | |
104 | 115 | | |
105 | 116 | | |
| |||
Lines changed: 9 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
68 | 68 | | |
69 | 69 | | |
70 | 70 | | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
71 | 74 | | |
72 | 75 | | |
73 | 76 | | |
| |||
76 | 79 | | |
77 | 80 | | |
78 | 81 | | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
79 | 88 | | |
80 | 89 | | |
81 | 90 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
129 | 129 | | |
130 | 130 | | |
131 | 131 | | |
| 132 | + | |
132 | 133 | | |
133 | 134 | | |
134 | 135 | | |
| |||
137 | 138 | | |
138 | 139 | | |
139 | 140 | | |
140 | | - | |
| 141 | + | |
141 | 142 | | |
142 | 143 | | |
143 | 144 | | |
144 | 145 | | |
145 | 146 | | |
146 | 147 | | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
147 | 151 | | |
148 | 152 | | |
149 | 153 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
91 | 91 | | |
92 | 92 | | |
93 | 93 | | |
| 94 | + | |
| 95 | + | |
94 | 96 | | |
95 | 97 | | |
96 | 98 | | |
| |||
213 | 215 | | |
214 | 216 | | |
215 | 217 | | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
216 | 223 | | |
217 | 224 | | |
218 | 225 | | |
| |||
369 | 376 | | |
370 | 377 | | |
371 | 378 | | |
| 379 | + | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
372 | 383 | | |
373 | 384 | | |
374 | 385 | | |
375 | 386 | | |
376 | 387 | | |
377 | 388 | | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
378 | 392 | | |
379 | 393 | | |
380 | | - | |
| 394 | + | |
381 | 395 | | |
382 | 396 | | |
383 | 397 | | |
| |||
392 | 406 | | |
393 | 407 | | |
394 | 408 | | |
| 409 | + | |
| 410 | + | |
| 411 | + | |
395 | 412 | | |
396 | 413 | | |
397 | 414 | | |
| |||
437 | 454 | | |
438 | 455 | | |
439 | 456 | | |
| 457 | + | |
| 458 | + | |
| 459 | + | |
| 460 | + | |
| 461 | + | |
| 462 | + | |
| 463 | + | |
| 464 | + | |
| 465 | + | |
440 | 466 | | |
441 | 467 | | |
442 | 468 | | |
| |||
451 | 477 | | |
452 | 478 | | |
453 | 479 | | |
| 480 | + | |
| 481 | + | |
| 482 | + | |
| 483 | + | |
| 484 | + | |
454 | 485 | | |
455 | 486 | | |
456 | 487 | | |
| |||
549 | 580 | | |
550 | 581 | | |
551 | 582 | | |
| 583 | + | |
| 584 | + | |
| 585 | + | |
| 586 | + | |
| 587 | + | |
552 | 588 | | |
553 | 589 | | |
554 | 590 | | |
| |||
668 | 704 | | |
669 | 705 | | |
670 | 706 | | |
| 707 | + | |
| 708 | + | |
| 709 | + | |
| 710 | + | |
| 711 | + | |
671 | 712 | | |
672 | 713 | | |
673 | 714 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
61 | 61 | | |
62 | 62 | | |
63 | 63 | | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
64 | 67 | | |
65 | 68 | | |
66 | 69 | | |
| |||
374 | 377 | | |
375 | 378 | | |
376 | 379 | | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
| 383 | + | |
| 384 | + | |
| 385 | + | |
| 386 | + | |
| 387 | + | |
| 388 | + | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
377 | 398 | | |
378 | 399 | | |
379 | 400 | | |
| 401 | + | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
| 405 | + | |
| 406 | + | |
| 407 | + | |
| 408 | + | |
| 409 | + | |
| 410 | + | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
| 414 | + | |
| 415 | + | |
| 416 | + | |
| 417 | + | |
| 418 | + | |
| 419 | + | |
| 420 | + | |
| 421 | + | |
| 422 | + | |
380 | 423 | | |
381 | 424 | | |
382 | 425 | | |
| |||
837 | 880 | | |
838 | 881 | | |
839 | 882 | | |
840 | | - | |
| 883 | + | |
| 884 | + | |
| 885 | + | |
| 886 | + | |
| 887 | + | |
| 888 | + | |
841 | 889 | | |
842 | 890 | | |
843 | 891 | | |
| |||
0 commit comments