feat(rewarding,staking): rework distributeVoterReward for IIP-59 (PR 3')#4924
Draft
envestcc wants to merge 7 commits into
Draft
feat(rewarding,staking): rework distributeVoterReward for IIP-59 (PR 3')#4924envestcc wants to merge 7 commits into
envestcc wants to merge 7 commits into
Conversation
Introduce a read-only bridge over the existing DelegateProfile contract (mainnet io1lfl4ppn2c3wcft04f0rk0jy9lyn4pcjcm7638u) so that PutPollResult can snapshot per-delegate commission rates from the amended IIP-59 design (iotexproject/iips#74). The bridge invokes getProfileByField twice per delegate (blockRewardPortion and epochRewardPortion), inverts the on-chain voter-take portion into commission basis points, and returns a per-delegate map. Empty bytes for either field flag the delegate as unregistered so the caller falls back to the legacy Hermes path; a partial profile is deliberately treated as unregistered to preserve the "either fully opted-in or fully legacy" invariant. Explicit zero voter-take remains distinguishable (single 0x00 byte) and yields a registered 100% commission split. Values exceeding uint64 or 10000 basis points are rejected rather than silently truncated. The package has no dependency on protocol.StateManager and is exercised by 14 unit tests using an in-process ABI-round-tripping fake reader. Refs iotexproject/iips#74. Follow-up PR (2') will call Snapshot at PutPollResult and freeze the returned rates into the poll snapshot. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…legacy path Snapshot originally propagated any per-delegate read error up, which at the IIP-59 consensus entry point (PutPollResult) would deterministically halt block production at every subsequent epoch boundary if one delegate's DelegateProfile field was malformed or a view call transiently failed. Same on-chain state produces the same error on every validator, so the fork stays safe — but wedging the chain is strictly worse than routing the one bad delegate through the well-defined legacy Hermes path. Absorb the per-delegate error, emit Registered=false for that entry, and log for observability. Nil-reader and nil-address stay hard errors: those are wiring bugs, not on-chain data issues, and must surface loudly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds a persistent per-delegate opt-in flag on staking.Candidate that gates protocol-native voter reward distribution. Default false — post-fork the legacy path (full block/epoch reward to RewardAddress, off-chain Hermes service continues) still runs unless the delegate explicitly opts in. Scope of this change is intentionally narrow: - stakingpb.Candidate gains field 11 (voterRewardOnchainOptIn bool). - staking.Candidate Go struct gains VoterRewardOnchainOptIn; Clone, Equal, toProto, fromProto all thread it. Default zero-value keeps existing candidates opting out on decode. - TestSerWithVoterRewardOnchainOptIn covers false/true round-trip through proto, Equal's flag sensitivity, and Clone independence. Deferred to follow-up PRs (per amended IIP-59, iotexproject/iips#74): - SetVoterRewardOptIn native action + handler that mutates the flag with a one-epoch delay via the PutPollResult snapshot (bidirectional flip). - BlockCommissionRate / EpochCommissionRate — these are per-epoch snapshot values populated from the DelegateProfile contract at PutPollResult; they live on the poll snapshot (PR 2') rather than the persistent Candidate. Superseded PRs: #4865 / #4866 / #4880 / #4881 (all closed 2026-07-10). Refs iotexproject/iips#74
… opt-in at PutPollResult (IIP-59)
Introduces the per-candidate poll snapshot IIP-59 needs at each epoch boundary.
Downstream rewarding (PR 3', follow-up) reads this snapshot instead of the
live DelegateProfile contract / live staking.Candidate so a mid-epoch
mutation cannot retroactively re-split rewards that have already begun
accruing.
Wiring:
- stakingpb.CandidatePollSnapshot: block/epoch commission basis points,
registered flag, opt-in flag, per-voter entries (empty in this PR).
stakingpb.VoterWeightEntry per-voter tuple.
- staking._candidatePollSnapshot = 5 tag byte; full key
{tag}||candID.Bytes() under _stakingNameSpace.
- staking.FreezePollSnapshot: writer called from poll/util.setCandidates.
Nil bridge (contract not configured) → skip rate freeze but still
capture opt-in from live Candidate; Registered=false forces legacy
fallback downstream. Any per-delegate error aborts the whole snapshot
write (no partial map).
- staking.PollSnapshotFor: reader; returns ErrStateNotExist pre-fork /
pre-write.
- staking.readLiveOptIn: degrades to false when the poll list names a
candidate that has no staking record, rather than wedging the chain.
- poll.freezeIIP59PollSnapshot: fork + config gate. Guarded by
fCtx.NoVoterRewardDistribution (pre-fork no-op) and
Blockchain.DelegateProfileContractAddress (empty ⇒ nil bridge).
- poll.delegateProfileContractReader: view-call plumbing mirrors
consortium.getContractReaderForGenesisStates verbatim
(address.ZeroAddress caller, evm.SimulateExecution).
- genesis.Blockchain.DelegateProfileContractAddress: per-network config;
default empty.
- protocol.FeatureCtx.NoVoterRewardDistribution: fork gate bound to
!g.IsToBeEnabled(height); zero-value = active post-fork.
Intentionally out of scope for this PR:
- Voter-weight source. Entries is written empty; PR 3' has a degenerate
branch (empty voter list ⇒ full amount as commission). Follow-up PR
fills in the actual weight computation.
- Rewarding consumption of the snapshot (PR 3').
- SetVoterRewardOptIn action to mutate the field this snapshot freezes
(separate PR).
Stacks on iip-59/pr1-candidate-schema-optin (#4911) and
iip-59/pr4.5-delegateprofile-bridge (#4912).
Refs iotexproject/iips#74
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Follows PR 4.5's change to have delegateprofile.Bridge.Snapshot degrade per-delegate read failures to Registered=false instead of erroring out. FreezePollSnapshot's docstring now says "on bridge error the snapshot is still written with Registered=false; opt-in remains captured from the live Candidate" so downstream (PR 3') has a single degradation contract to reason about. Test rename: FreezePollSnapshot_BridgeErrorPropagates → FreezePollSnapshot_BridgeErrorDegradesToLegacy — asserts the snapshot IS written and opt-in is preserved on bridge error. Refs iotexproject/iips#74 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Wire the three read-side bridges landed upstream — PR 2' (frozen poll
snapshot), PR 4.6 (autodeposit compound bridge), PR 4.7 (batched
DelegateDistributed log) — into GrantEpochReward. For each opted-in
delegate, IIP-59 §3.2 now: splits the epoch pool by the frozen commission
basis points, allocates the voter pool proportionally by frozen voter
weight in canonical order, routes each per-voter share to compound
(native AddDeposit) or credit (rewarding unclaimedBalance), credits the
delegate's commission to its reward address, and emits exactly one
batched DelegateDistributed log per delegate.
Feature-flag matrix stays honoured:
NoVoterRewardDistribution=true (pre-fork) → return (nil, false, nil);
caller runs legacy grantToAccount unchanged.
VoterRewardOnchainOptIn=false → same fallback (opt-out).
no poll snapshot yet → same fallback (first
epoch after registration).
Registered=false (bridge degraded) → same fallback.
autoDepositBridge nil → split runs; every voter
routes to credit (compound routing inactive).
Per-item consensus fallback per feedback-consensus-fallback-vs-halt:
malformed on-chain data (bridge RPC error, bucket read error, ineligible
bucket) downgrades the affected voter to credit rather than halting the
block. Wiring errors (nil staking protocol, log-encoder failure) still
hard-fail.
Cross-protocol seam: staking.AddDepositForCompound is a package-exported
entry point for the rewarding-side compound path. Skips the
handleDepositToStake action-plumbing checks that PR 3' has already
enforced upstream (positive bucketID from AutoDeposit.bucket(voter);
IsBucketEligibleForCompound confirmed native/active/AutoStake/Owner).
Does NOT emit a staking receipt log — the batched DelegateDistributed
log is the single source of truth per delegate.
New:
- action/protocol/rewarding/voter_reward.go — distributeVoterReward
+ splitCommission (basis-points helper) + resolveAutoDepositReader.
- action/protocol/rewarding/voter_reward_test.go — 14 tests covering
splitCommission edge cases, fork gate, nil-input guards, missing-
snapshot fallback, bridge-nil default, and Option wiring.
- action/protocol/staking/add_deposit_compound.go — cross-protocol seam.
Modified:
- action/protocol/rewarding/reward.go — one call site + one branch
inside GrantEpochReward's per-delegate loop. splitEpochReward now
returns the filtered candidate list so callers see the same index
domain as addrs/amounts.
- action/protocol/rewarding/protocol.go — Option type +
WithAutoDepositBridge / WithAutoDepositReader; NewProtocol accepts
opts ...Option so 11 existing call sites remain unchanged.
- chainservice/builder.go — construct autodeposit.Bridge from
Blockchain.AutoDepositContractAddress at registerRewardingProtocol;
empty string ⇒ nil bridge (compound routing inactive).
Stacks on:
- iip-59/pr4.7-delegatedistributed-log (#4923) — merge base.
- iip-59/pr2-snapshot-writer (#4915) — merged in via d7fe741.
- iip-59/pr4.6-autodeposit-bridge (#4922) — via #4923.
- iip-59/pr4.5-delegateprofile-bridge (#4912) — via #4915.
Deliberately out of scope: block-reward folding + orphan drain
(PR 4'); voter-weight source population (Entries currently empty
per PR 2' skeleton — downstream degenerate branch pays full amount as
commission until the weight-source follow-up lands).
Refs iotexproject/iips#74
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
6 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.


Summary
PR 3' of the IIP-59 amended stack: wires the three read-side bridges landed
upstream into
GrantEpochRewardso opt-in delegates get their epoch sharesplit on-chain per IIP-59 §3.2.
For each opt-in delegate, the per-delegate loop now:
poll snapshot (PR 2').
canonical (snapshot-order) sequence. Modular-division dust attaches to
the last non-zero-weight voter so
sum(shares) == voterPoolexactly.voter has a registered bucket and it passes
IsBucketEligibleForCompound,the share is compounded via
staking.AddDepositForCompound(new seam);otherwise credited to the voter's rewarding
unclaimedBalance.DelegateDistributedlog per delegate viadistributedlog.Pack(PR 4.7).Feature-flag matrix (honoured)
NoVoterRewardDistributiongrantToAccount(delegate.RewardAddr, full)Consensus fallback
Per [feedback-consensus-fallback-vs-halt], malformed on-chain data (bridge
RPC error, bucket read error, ineligible bucket) downgrades the affected
voter to credit rather than halting the block — pure function of on-chain
state ⇒ same fallback on every validator ⇒ no fork. Wiring bugs (nil
staking protocol, log-encoder failure) still hard-fail.
New
action/protocol/rewarding/voter_reward.go—distributeVoterReward+splitCommissionbasis-points helper +resolveAutoDepositReader.action/protocol/rewarding/voter_reward_test.go— 14 tests coveringsplitCommissionedge cases, fork gate, nil-input guards,missing-snapshot fallback, bridge-nil default, Option wiring.
action/protocol/staking/add_deposit_compound.go— cross-protocol seam.Package-exported entry point for the rewarding-side compound path;
skips the
handleDepositToStakeaction-plumbing checks that PR 3' hasalready enforced. Does NOT emit a staking receipt log — the batched
DelegateDistributedlog is the single source of truth per delegate.Modified
action/protocol/rewarding/reward.go— one call site + one branchinside
GrantEpochReward's per-delegate loop.splitEpochRewardnowalso returns the filtered candidate list so callers see the same
index domain as
addrs/amounts.action/protocol/rewarding/protocol.go—Optiontype +WithAutoDepositBridge/WithAutoDepositReader.NewProtocolaccepts
opts ...Option; 11 existing call sites remain unchanged.chainservice/builder.go— constructautodeposit.BridgefromBlockchain.AutoDepositContractAddressatregisterRewardingProtocol; empty string ⇒ nil bridge (compoundrouting inactive).
Stack (four unmerged dependencies)
d7fe741f7brings PR 2' into scope via a merge commit (no history rewrite).Reviewers should focus on commits above the merge.
Out of scope
Entriesis still empty perPR 2' skeleton; the downstream degenerate branch pays the full amount
as commission until the weight-source follow-up lands. Once weights
are populated, distributeVoterReward already does the correct thing.
Test plan
go build ./...go vet ./action/protocol/rewarding/... ./chainservice/...go test ./action/protocol/rewarding/... -count=1(91 passed)go test ./action/protocol/staking/... -count=1(385 passed)go test ./blockchain/integrity/... -count=1(16 passed)(deferred to PR 5; requires the weight-source follow-up).
Refs iotexproject/iips#74
🤖 Generated with Claude Code