Skip to content

feat(staking): add Candidate.VoterRewardOnchainOptIn for IIP-59 (amended)#4911

Closed
envestcc wants to merge 1 commit into
masterfrom
iip-59/pr1-candidate-schema-optin
Closed

feat(staking): add Candidate.VoterRewardOnchainOptIn for IIP-59 (amended)#4911
envestcc wants to merge 1 commit into
masterfrom
iip-59/pr1-candidate-schema-optin

Conversation

@envestcc

Copy link
Copy Markdown
Member

Summary

Adds a persistent per-delegate opt-in flag on staking.Candidate that gates
protocol-native voter reward distribution.

This is the first code PR against the amended IIP-59 design (see
iotexproject/iips#74). The
earlier native-only design and its implementation PRs
(#4865 / #4866 / #4880 / #4881) were superseded and closed on 2026-07-10.

Draft status intentional — will promote to Ready after the IIP amendment
lands and the downstream PRs in the series (PR 4.5 contract-read bridge,
PR 2' snapshot rework, PR 3'/4' rewarding rework) are ready to stack.

What changed

  • stakingpb.Candidate gains field 11: bool voterRewardOnchainOptIn.
  • staking.Candidate Go struct gains VoterRewardOnchainOptIn bool;
    Clone, Equal, toProto, fromProto all thread the new field.
  • New test TestSerWithVoterRewardOnchainOptIn covers:
    • proto round-trip with the flag both false and true,
    • Equal sensitivity to the flag,
    • Clone independence (flipping one copy doesn't affect the other).

Default zero-value keeps every existing candidate opting out on decode,
so behavior is unchanged pre-fork and, post-fork, unchanged for any delegate
that hasn't sent the (later) opt-in action.

Deferred to follow-up PRs

Per the amended IIP-59:

  • SetVoterRewardOptIn native action + handler: mutates the flag with
    a one-epoch delay via the PutPollResult snapshot. Requires an external
    iotex-proto proto change (new action type). Separated to keep this PR
    self-contained.
  • BlockCommissionRate / EpochCommissionRate: per-epoch snapshot
    values populated from the DelegateProfile contract at PutPollResult;
    they live on the poll snapshot (PR 2'), not on the persistent Candidate.
    See the amended proposal for the flow.

Test plan

  • make fmt — clean
  • go build ./... — clean
  • go test ./action/protocol/staking/ — 264 tests pass, includes new roundtrip test
  • make lint — pending review
  • Reviewer: confirm field 11 doesn't collide with any WIP proto branch

Refs

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
@sonarqubecloud

Copy link
Copy Markdown

envestcc added a commit that referenced this pull request Jul 13, 2026
… 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>
envestcc added a commit that referenced this pull request Jul 16, 2026
… 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>
envestcc added a commit that referenced this pull request Jul 21, 2026
… 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>
@envestcc

Copy link
Copy Markdown
Member Author

Superseded by #4953 (consolidated IIP-59 on-chain voter reward distribution). Contents folded into that PR wholesale; review and merge happen there.

@envestcc envestcc closed this Jul 21, 2026
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