Skip to content

feat: cumulative factors and delegator shares for O(1) stake computation#217

Open
adamsoffer wants to merge 7 commits intolivepeer:mainfrom
adamsoffer:claude/magical-tereshkova
Open

feat: cumulative factors and delegator shares for O(1) stake computation#217
adamsoffer wants to merge 7 commits intolivepeer:mainfrom
adamsoffer:claude/magical-tereshkova

Conversation

@adamsoffer
Copy link
Contributor

@adamsoffer adamsoffer commented Mar 8, 2026

Problem

The subgraph has no way to compute a delegator's actual total stake. The bondedAmount field only reflects the value at the time of the last claim and does not include rewards earned since then. To get a delegator's current stake, clients must call the contract's pendingStake() method directly — and for historical stake at a past round, there is no solution at all.

Similarly, computing an orchestrator's full pending stake (including their unclaimed commission) or their lifetime earnings requires fetching every pool since their last claim and summing across all of them.

This also means there's no way to build time-series charts of delegator stake, show per-round reward breakdowns, or compute orchestrator yield — all of which require knowing stake values across rounds.

Summary

  • Store cumulativeRewardFactor and cumulativeFeeFactor on the Pool entity, matching on-chain PreciseMathUtils (27-decimal fixed-point)
  • Propagate cumulative factors forward each round during pool creation so every pool has valid values (no gaps for missed reward() calls)
  • Add shares field to Delegator (bondedAmount * 10^27 / crf[lastClaimRound]) — invariant across claims, only changes on bond/unbond/rebond
  • Add DelegatorSnapshot entity capturing delegator state at each bond/unbond/rebond for time-series chart support
  • Compute cumulative fee factor in WinningTicketRedeemed handler using previous round's CRF (matching contract behavior)
  • Add orchestrator commission tracking on Transcoder:
    • pendingRewardCommission — unclaimed reward commission (rewardCut portion), resets on claim
    • lifetimeRewardCommission — total reward commission ever earned, never resets
    • pendingFeeCommission — unclaimed fee commission, resets on claim
    • lifetimeFeeCommission — total fee commission ever earned, never resets

What this unlocks

Current pending stake without contract calls — A delegator's up-to-date total stake (equivalent to pendingStake() on-chain) can now be computed entirely from the subgraph: stake = shares * crf / 10^27. For orchestrators specifically, their full pending stake includes unclaimed commission: stake = shares * crf / 10^27 + pendingRewardCommission. No contract calls needed.

Historical stake at any past round — Something previously impossible. Query the delegator's shares (or the relevant DelegatorSnapshot) and the Pool.cumulativeRewardFactor for that round, then compute shares * crf[round] / 10^27.

Per-round reward and fee breakdowns — Rewards earned in a specific round can be derived by comparing cumulative factors between consecutive rounds: rewards = shares * (crf[round] - crf[round-1]) / 10^27. Same pattern applies to fees using cumulativeFeeFactor.

Time-series charts of delegator stake — The shares approach combined with DelegatorSnapshot entities makes it straightforward to build a chart showing a delegator's total stake over time. Between snapshots, shares are constant, so stake at any round is just shares * crf[round] / 10^27. When a bond/unbond/rebond occurs, a new snapshot captures the updated shares value to use going forward.

Orchestrator commission trackingpendingRewardCommission and pendingFeeCommission give the orchestrator's unclaimed commission in a single field each, without needing to sum across pools. lifetimeRewardCommission and lifetimeFeeCommission provide total lifetime earnings.

Orchestrator pool analytics — Cumulative factors stored per-pool per-round enable computing total rewards distributed, effective yield, and fee revenue for any orchestrator over any time range without replaying events.

All changes are additive — no breaking changes to existing queries.

Test plan

  • Run npx graph codegen && npx graph build — verified locally
  • Deploy to a staging subgraph and verify cumulative factors match on-chain values
  • Verify delegator shares remain constant across claim events
  • Verify shares * crf[round] / 10^27 matches actual bonded amounts
  • Verify pending stake computed from subgraph matches contract pendingStake() return value
  • Verify DelegatorSnapshot entities are created on bond/unbond/rebond but not on claim
  • Verify pendingRewardCommission and pendingFeeCommission reset to zero after orchestrator claims
  • Verify lifetimeRewardCommission and lifetimeFeeCommission never reset

🤖 Generated with Claude Code

…cient historical stake computation

Store cumulativeRewardFactor and cumulativeFeeFactor on Pool entity
(matching on-chain PreciseMathUtils), propagate them forward each round,
and introduce a shares field on Delegator that enables O(1) stake lookups
via `stake = shares * crf[round] / 10^27`. DelegatorSnapshot entities
capture state at bond/unbond/rebond events for time-series chart support.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@adamsoffer adamsoffer requested a review from rickstaa as a code owner March 8, 2026 20:54
adamsoffer and others added 6 commits March 9, 2026 12:04
…ommission

Tracks the orchestrator's total rewardCut commission across all rounds,
incremented each time reward() is called. Never resets, so clients can
read lifetime earnings in a single field.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ission

Resets Transcoder.cumulativeRewards to zero when the orchestrator claims
earnings, so the field represents pending/unclaimed commission rather
than lifetime total. This lets clients compute full orchestrator pending
stake as: shares * crf / 10^27 + cumulativeRewards

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Separate never-reset counter alongside cumulativeRewards (which resets
on claim). Gives clients a single field for lifetime orchestrator
commission without summing historical claim events.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Rename cumulativeRewards/lifetimeRewards to pendingRewardCommission/
lifetimeRewardCommission for clarity. Add pendingFeeCommission and
lifetimeFeeCommission on Transcoder, computed in WinningTicketRedeemed
handler. Both pending fields reset on claim.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The previous implementation did two divisions (num * 10^27 / denom, then
base * result / 10^27), causing intermediate truncation that compounded
each round in the CRF calculation. Solidity's version does one division
(base * num / denom). While the CRF ratio error cancelled out for
delegator stake, pendingRewardCommission accumulated ~0.23 LPT/round
drift. This fix ensures exact match with the contract.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The contract's cumulativeRewards includes two components: the rewardCut
commission plus rewards earned by the transcoder's own staked commission
(activeCumulativeRewards). The subgraph was only tracking the first.

Add activeCumulativeRewards field on Transcoder, snapshotted from
pendingRewardCommission at the start of each round in newRound. The
reward handler now computes both components matching the contract's
updateTranscoderWithRewards logic. Reset on claim.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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