Skip to content

feat(rewarding): DelegateProfile contract-read bridge for IIP-59#4912

Draft
envestcc wants to merge 2 commits into
masterfrom
iip-59/pr4.5-delegateprofile-bridge
Draft

feat(rewarding): DelegateProfile contract-read bridge for IIP-59#4912
envestcc wants to merge 2 commits into
masterfrom
iip-59/pr4.5-delegateprofile-bridge

Conversation

@envestcc

Copy link
Copy Markdown
Member

Summary

  • Add read-only bridge over the on-chain DelegateProfile contract (mainnet io1lfl4ppn2c3wcft04f0rk0jy9lyn4pcjcm7638u) so PutPollResult can snapshot per-delegate commission rates from the amended IIP-59 design.
  • Bridge fetches blockRewardPortion and epochRewardPortion per delegate via getProfileByField, inverts voter-take portions into commission basis points, and returns a per-delegate map keyed by identity string.
  • Package has no dependency on protocol.StateManager; exercised by 14 unit tests using an ABI-round-tripping in-process fake reader.

Design notes

  • Read API: getProfileByField(_delegate, _field) returns bytes invoked twice per delegate. Avoids reverse-engineering getEncodedProfile's undocumented composite return format.
  • Registered semantics: empty bytes for either portion field marks the delegate Registered=false, and the caller MUST fall back to the legacy Hermes path. A partial profile (one field set, other empty) is treated as unregistered on purpose — the IIP relies on the "either fully opted-in or fully legacy" invariant so a delegate cannot end up with one reward stream migrated mid-fork.
  • Explicit zero: a non-empty single 0x00 byte encodes an explicit 0% voter-take and yields Registered=true with a 100% commission split. This is distinguishable from "field never set".
  • Range check: values exceeding uint64 or 10000 basis points are rejected rather than silently truncated via SetBytes → Uint64.
  • Determinism: Snapshot iterates the caller-supplied delegate list in order (2N sequential reads); errors abort with the offending delegate address named.

Sequencing

Refs iotexproject/iips#74.

  • Depends on: none (isolated read-only package).
  • Follow-up PR 2': PutPollResult calls Snapshot, freezes commission rates into the poll snapshot per delegate. That PR wires the concrete ContractReader around evm.SimulateExecution.
  • Follow-ups PR 3'/4'/4.6/4.7: consume the snapshotted rates at epoch close, block-reward folding, compound bridge, and batched delegate-distributed log emitter respectively.

Test plan

  • go build ./...
  • go vet ./action/protocol/rewarding/delegateprofile/...
  • go test ./action/protocol/rewarding/delegateprofile/ — 14/14 pass
  • Reviewer: confirm getProfileByField return-encoding assumption (big-endian uint256 bytes, empty ⇒ unset) matches the deployed contract behaviour.
  • Reviewer: confirm mainnet contract address is the intended source of truth for this fork (governance handover is a separate prereq).

Draft — subsequent PRs in the IIP-59 stack build on this bridge.

Co-Authored-By: Claude Opus 4.7 noreply@anthropic.com

🤖 Generated with Claude Code

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

Copy link
Copy Markdown
Member Author

Force-pushed one additional commit (49d2c6245) folding in a bridge semantic change previously landed on PR #4915:

  • Bridge.Snapshot now absorbs per-delegate read errors (RPC hiccup, out-of-range portion, ABI mismatch) and returns a Registered=false entry for the affected delegate instead of erroring out. Nil-reader / nil-address still hard-error — those are wiring bugs, not on-chain data issues.
  • Rationale: the previous behaviour let one malformed on-chain DelegateProfile field deterministically halt block production at every subsequent epoch boundary. Deterministic reward-path fallback (route bad delegate through legacy Hermes) beats deterministic block-production failure when the fallback exists.
  • Errors are logged via log.L().Warn with delegate + contract for observability.

Test updates in the same commit rename the affected assertions (*_Rejected*_DegradesToUnregistered) and add TestSnapshot_PerDelegateErrorIsolated to confirm one bad delegate does not poison batched successful reads.

PR #4915 rebased on top; its follow-up commit now only touches the staking-side docstring + one test rename.

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