fix: temporarily compute the LIP-118 tally on-chain while the subgraph resyncs#749
Conversation
The voting UI takes its stake-weighted counts from the subgraph, so the manually surfaced LIP-118 poll (#734) renders 0% support and 0% participation — the poll is reachable and votable, but shows no results. Add /api/polls/tally/[poll], which recomputes a poll's tally from chain state: read the Poll contract's Vote logs, then weight each voter through BondingManager, mirroring the subgraph's rules (a registered transcoder votes with its total stake; a delegator that votes itself is subtracted from its delegate's weight; latest vote per address wins). The route is restricted to MANUAL_POLLS so it can't be pointed at arbitrary contracts. Kept cheap on the RPC: log scans are chunked under Infura's 10k block range cap, the scan cursor advances so only new blocks are read after the first pass, and results are memoised for 60s in-process as well as at the CDN. Steady state is one getLogs plus a batched round of contract reads per minute. getTotalStake now falls back to BondingManager.getTotalBonded() when the subgraph is unavailable — the protocol entity is currently null, which would otherwise leave participation at 0%. The poll page also recovers "My Vote" from the computed tally, so a voter isn't told N/A and prompted to vote a second time. Tallies read stake as it is now, which matches the subgraph while a poll is open but drifts once one closes; pinning reads to the end block needs NodeInterface.l2BlockRangeForL1, which reverts intermittently for the same input, so that is documented rather than attempted. Revert once the subgraph has reindexed the poll. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughManual polls now support on-chain stake-weighted tally computation through a cached API endpoint. The voting page refreshes these tallies periodically, merges them with poll data, and uses on-chain vote information when subgraph data is unavailable. ChangesManual poll tally fallback
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant VotingPage
participant useManualPollTally
participant TallyAPI
participant getPollTally
participant OnChainContracts
VotingPage->>useManualPollTally: request tally for manual poll
useManualPollTally->>TallyAPI: GET /polls/tally/{poll}
TallyAPI->>getPollTally: compute tally from startBlockL2
getPollTally->>OnChainContracts: read votes, rounds, and stake data
OnChainContracts-->>getPollTally: return on-chain state
getPollTally-->>TallyAPI: return PollTally
TallyAPI-->>useManualPollTally: return cached tally
useManualPollTally-->>VotingPage: update poll totals and vote status
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull request overview
Adds an on-chain fallback path to compute governance poll tallies while the subgraph is still reindexing, so the voting UI can show stake-weighted support/participation and recover “My Vote” for manually surfaced polls.
Changes:
- Add
GET /api/polls/tally/[poll]to compute and cache a poll tally from on-chain vote logs + BondingManager stake reads (restricted toMANUAL_POLLS). - Add
useManualPollTally+ UI wiring to fill missing tallies for manual polls and recover the user’s vote from the computed tally. - Update
getTotalStaketo fall back to on-chainBondingManager.getTotalBonded()when the subgraph query fails.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| pages/voting/[poll].tsx | Uses the on-chain tally for manual polls and falls back to it for “My Vote”. |
| pages/api/polls/tally/[poll].ts | New API route that serves a cached on-chain-computed tally for manual polls only. |
| lib/api/types/get-poll-tally.ts | Adds types for the on-chain-computed tally payload. |
| lib/api/pollTally.ts | Implements log scanning + stake weighting to compute tallies from chain state. |
| lib/api/polls.ts | Adds on-chain fallback for total stake used in participation calculations. |
| hooks/useSwr.tsx | Adds useManualPollTally SWR hook to poll the new endpoint. |
| constants/manualPolls.ts | Extends manual poll entries with startBlockL2 and adds withManualTally helper. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
A reindexing subgraph answers successfully with an empty protocol entity rather than failing, so getTotalStake returned undefined and left participation at 0% — the symptom the on-chain fallback was added to fix. Split the subgraph read out and branch on the value, so the fallback also covers a successful response with nothing in it. Drop totalStake, updatedAt and registeredTranscoder from the tally response: nothing reads them, and totalStake in particular is misleading since participation takes its denominator from getTotalStake. Removing it also drops the getTotalBonded read that fed it. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
pages/api/polls/tally/[poll].ts (1)
1-90: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winAdd
maxDurationtopages/api/polls/tally/[poll].ts
- Problem: This route can spend tens of seconds on a cold tally, but it doesn’t export
config.maxDuration.- Why it matters: The first request per poll per instance can hit serverless timeouts before the result is cached, causing repeated failures on the same slow path.
- Suggested fix: Export a route
configwith amaxDurationhigh enough for the worst-case cold scan.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@pages/api/polls/tally/`[poll].ts around lines 1 - 90, Export a Next.js route config alongside the default handler in this file, setting config.maxDuration high enough to cover the worst-case cold getCachedPollTally scan before the result is cached.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@lib/api/pollTally.ts`:
- Around line 66-101: Add unit tests covering getVoteLogs and getPollTally,
including chunked range fallback, reorg-buffer rescans, self-delegation versus
registration, and delegator exclusion. Mock the range-log and delegation
dependencies to verify stake-weighted totals and poll outcomes across these edge
cases.
- Around line 210-243: Align the `voteStake` selection in the `voters.map` block
with the `excluded` calculation in the `tallied.map` block by using the same
registration/self-delegation condition. Ensure unregistered self-delegated
addresses cannot retain delegated stake that is also counted through
`nonVoteStake`, preventing double-counting while preserving registered
transcoder weighting.
---
Outside diff comments:
In `@pages/api/polls/tally/`[poll].ts:
- Around line 1-90: Export a Next.js route config alongside the default handler
in this file, setting config.maxDuration high enough to cover the worst-case
cold getCachedPollTally scan before the result is cached.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: d5bdd7c8-fc56-497b-87e3-b880dfdb4774
📒 Files selected for processing (7)
constants/manualPolls.tshooks/useSwr.tsxlib/api/pollTally.tslib/api/polls.tslib/api/types/get-poll-tally.tspages/api/polls/tally/[poll].tspages/voting/[poll].tsx
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
The documented rule gives a registered transcoder its total delegated stake and everyone else their own pending stake. Selecting on self-delegation matches that only because isRegisteredTranscoder is defined as self-delegated with a non-zero bonded amount. Where the two diverge is an orchestrator that unbonded its own stake while delegators remain: still self-delegated, no longer registered, and holding delegated stake it would otherwise vote with. Self-delegation still decides whether a vote is subtracted from a delegate's weight, which is a question about delegating elsewhere rather than registration. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Co-Authored-By: Copilot <copilot-pull-request-reviewer[bot]@users.noreply.github.com>
transcoderTotalStake reads the transcoder pool, which returns zero for an orchestrator outside it, while its delegators' pending stake is still subtracted. The resulting negative weight was added to the yes or no total, reducing it by the stake of everyone who voted. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Co-Authored-By: Copilot <copilot-pull-request-reviewer[bot]@users.noreply.github.com>
…h resyncs (livepeer#749) Stopgap while the subgraph resyncs from scratch (livepeer/subgraph#247), which leaves the manually surfaced LIP-118 poll showing 0% support and 0% participation even for people who already voted. Revert this and livepeer#734 once the subgraph has reindexed the poll. --------- Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]> Co-authored-by: Copilot <copilot-pull-request-reviewer[bot]@users.noreply.github.com>
…h resyncs (livepeer#749) Stopgap while the subgraph resyncs from scratch (livepeer/subgraph#247), which leaves the manually surfaced LIP-118 poll showing 0% support and 0% participation even for people who already voted. Revert this and livepeer#734 once the subgraph has reindexed the poll. --------- Co-authored-by: Copilot <copilot-pull-request-reviewer[bot]@users.noreply.github.com>
Warning
Bandaid — revert once the subgraph has reindexed LIP-118.
Only exists because the subgraph is resyncing from scratch
(livepeer/subgraph#247) and cannot serve poll tallies. Revert this and #734
together; neither is meant to stay.
Problem
The voting UI sources its stake-weighted counts from the subgraph. The subgraph is resyncing from scratch (livepeer/subgraph#247, currently ~block 5.8M of ~486M), so the manually surfaced LIP-118 poll (#734) is reachable and votable but shows 0% support / 0% participation — including for people who already voted, whose sidebar reads "My Vote: N/A".
Change
Adds
GET /api/polls/tally/[poll], which recomputes the tally from chain state.The Poll contract stores nothing —
vote()only emitsVote(voter, choiceID)— so the endpoint reads the vote logs and weights each voter through BondingManager, following the rules the subgraph documents insrc/mappings/poll.ts:transcoderTotalStakereads the transcoder pool and returns 0 outside it, which would otherwise make a delegate's weight negativenonVoteStake), so stake is never double countedYes/Nocount, latest vote per address winsThe route is restricted to
MANUAL_POLLS, so it can't be pointed at arbitrary contracts.Also:
getTotalStakefalls back toBondingManager.getTotalBonded()when the subgraph is unavailable. Theprotocolentity is currentlynull, which would otherwise leave participation at 0% even with a tally present.RPC cost
Infura caps
eth_getLogsat 10k blocks on Arbitrum, so scans are chunked under that limit, 10 in flight. A scan cursor advances so only new blocks are read after the first pass, and results are memoised for 60s in-process as well as viaCache-Controlat the CDN.getLogs+ one batched round of contract reads, max once/60sVerification
Run against Arbitrum with the production Infura key:
Matches an independent
castcomputation to the wei. Page renders 100% support / 15.95K LPT for / 0.05% participation. Guards checked: unknown poll → 404, malformed address → 400,POST→ 405.Known limitation
Stake is read as of now. That matches the subgraph while a poll is open, but the real result freezes when voting closes whereas this keeps moving as stake is bonded, unbonded, or earns rewards — enough to flip the derived passed/rejected status.
Pinning the reads to the poll's end block was implemented and tested, then dropped: it needs
NodeInterface.l2BlockRangeForL1to map the L1 end block onto L2, and non-archive Arbitrum nodes stop resolving that mapping past roughly 67-74 days (measured identically on Infura, arb1 and publicnode). It traded a small drift for a hard failure. The same boundary explains the existinggetL2BlockRangeForL1TODO inlib/api/polls.ts: block 18328665 is a 2023 L1 block, well outside the window. Documented in the module comment instead.Voting closes in ~6 days and the subgraph needs ~2, so this window is not expected to open. If it does, treat the numbers as indicative and confirm from the vote events at the end block.
Revert
This PR is a stopgap and should be reverted, not maintained.
Revert when the subgraph has reindexed LIP-118 (
pollsandpollTallypopulatedfor
0x74479927117d4158b32c03d5400c14bdd7e6a46a):Revert "refactor: temporarily hide minute usage data with warning overlay (#452)" (#470).constants/manualPolls.tsfor the same outage.Reverting removes
lib/api/pollTally.ts,GET /api/polls/tally/[poll], the"My Vote" recovery on the poll page, and the
getTotalBondedfallback ingetTotalStake.Nothing depends on it staying: the UI prefers subgraph data wherever it exists,
so a reindexed poll drops the manual entry and the computed tally on its own,
even before the revert lands. The only cost of leaving it in place is carrying
governance-critical code that duplicates the subgraph's rules and can drift
from them.
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes