eth,server,cmd: Add LIP-118 reward caller support - #4011
Conversation
LIP-118 lets an orchestrator authorize a separate, low-privilege address to call reward on its behalf, so the stake-holding wallet can stay cold. The contract side shipped in livepeer/protocol#648 and is live on Arbitrum One, but go-livepeer had no support for it. Add SetRewardCaller/GetRewardCaller/RewardForTranscoder to LivepeerEthClient and regenerate the BondingManager bindings. Reward() is refactored into a shared rewardFor(transcoder, delegated) so the transcoder pool hint math is identical on both paths and always keyed on the transcoder rather than the caller. The reward service now takes the orchestrator address, reads eligibility from the orchestrator's record (a reward caller is not itself a transcoder, so its record is empty and inactive), and re-checks authorization every round so a revoked caller reports a clear error instead of a bare on-chain revert. Authorization is validated at startup, but only when reward is actually going to run: -ethOrchAddr also designates ticket recipients for gateways, redeemers and orchestrators that never call reward, and those must not pay for an extra lookup or inherit a new failure mode. An explicit -reward with an unauthorized wallet exits; an auto-enabled one warns and stays off. Registration status is deliberately not a hard failure, since an orchestrator may still be registering via livepeer_cli against the node itself. The service URI lookup also moves to the orchestrator address. It is registered against the transcoder, so a node on a caller wallet previously resolved an empty URI and exited at startup. Guard the orchestrator config actions against delegated wallets. The contracts key these on msg.sender: reward cut / fee share reverts via isRegisteredTranscoder, but ServiceRegistry.setServiceURI has no caller check at all and would silently succeed against the wrong key. setOrchestratorConfig compounded this by diffing against an empty record and firing both transactions. orchestratorInfoHandler now reports the orchestrator's record so livepeer_cli shows real status, stake and LastRewardRound on a caller wallet. Add a "Set reward caller" livepeer_cli action and a /setRewardCaller endpoint, where an empty value unsets via the zero address. This signs with the node's wallet, so it is a pre-migration step run while the node still holds the orchestrator key. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds go-livepeer client, server, and CLI support for LIP-118 “reward caller” delegation, enabling an orchestrator to authorize a separate low-privilege wallet to call reward() on its behalf (keeping the stake-holding wallet cold).
Changes:
- Extend contract bindings +
ethclient APIs to set/get a reward caller and call reward on behalf of a transcoder. - Make reward execution and CLI endpoints orchestrator-aware when running on a delegated (reward caller) wallet, including startup authorization checks and correct service URI lookup.
- Add server + eth unit tests covering authorized/unauthorized/revoked and delegated-wallet behaviors, plus CLI wizard action to set/unset the reward caller.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| server/webserver.go | Wires new CLI HTTP endpoints for reward caller management and routes /reward through server instance. |
| server/handlers.go | Adds delegated-wallet detection/guard, reward-caller endpoints, and orchestrator-aware /reward + /orchestratorInfo. |
| server/handlers_test.go | Adds coverage for delegated wallet behavior and reward caller set/unset; updates reward handler test wiring. |
| eth/stubclient.go | Extends mock/stub eth clients for reward caller and delegated reward calls. |
| eth/rewardservice.go | Makes reward service operate on an orchestrator address (delegated path) with per-round authorization re-check. |
| eth/rewardservice_test.go | Adds tests for delegated reward caller paths, revocation, and lookup errors. |
| eth/contracts/bondingManager.go | Regenerates/adds ABI bindings for LIP-118 methods and events. |
| eth/client.go | Adds SetRewardCaller/GetRewardCaller/RewardForTranscoder and refactors reward hint computation to be transcoder-keyed. |
| cmd/livepeer/starter/starter.go | Adds reward authorization check at startup (reward-gated) and service URI lookup keyed on orchestrator address when delegated. |
| cmd/livepeer/starter/starter_test.go | Updates tests for updated getServiceURI() signature. |
| cmd/livepeer_cli/wizard_transcoder.go | Adds interactive “Set reward caller” flow (set/unset) to the transcoder wizard. |
| cmd/livepeer_cli/livepeer_cli.go | Exposes the new wizard action in CLI options. |
| CHANGELOG_PENDING.md | Adds changelog entry for LIP-118 reward caller support. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // NewRewardService returns a service that calls reward once per round for orchAddr. | ||
| // Pass the zero address when the node's own account is the registered transcoder. |
| var ethOrchAddr ethcommon.Address | ||
| if *cfg.EthOrchAddr != "" { | ||
| ethOrchAddr = ethcommon.HexToAddress(*cfg.EthOrchAddr) | ||
| } |
| if !ethcommon.IsHexAddress(in) { | ||
| return "", fmt.Errorf("invalid hex address address=%v", in) | ||
| } |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #4011 +/- ##
===================================================
- Coverage 35.46423% 35.43518% -0.02905%
===================================================
Files 174 174
Lines 45161 45452 +291
===================================================
+ Hits 16016 16106 +90
- Misses 27871 28066 +195
- Partials 1274 1280 +6
... and 4 files with indirect coverage changes Continue to review full report in Codecov by Harness.
🚀 New features to boost your workflow:
|
What
Client support for LIP-118, which lets an orchestrator authorize a separate, low-privilege address to call
reward()on its behalf so the stake-holding wallet can stay cold (hardware or multisig).The contract side shipped in livepeer/protocol#648 and is live on Arbitrum One. The subgraph and explorer already support it; go-livepeer had none.
How it works
Set a reward caller from
livepeer_cli(or the explorer) on a node holding the orchestrator key, then restart the node with the caller wallet's keystore and-ethOrchAddr <orchestrator>. No new flag:-ethOrchAddris already the flag for the separate-wallet setup, so the node readstranscoderToRewardCaller(orchAddr)and takes the delegated path when it matches its own account.Changes
Contract bindings —
setRewardCaller,rewardForTranscoder,rewardForTranscoderWithHint,transcoderToRewardCaller,RewardCallerSet.eth/client.go—SetRewardCaller/GetRewardCaller/RewardForTranscoder.Reward()is refactored into a sharedrewardFor(transcoder, delegated)so the transcoder pool hint math is identical on both paths and always keyed on the transcoder, never the caller.eth/rewardservice.go— takes the orchestrator address; reads eligibility from the orchestrator's record (a reward caller is not itself a transcoder, so its record is empty andActive == false, which would silently skip reward every round); re-checks authorization each round so a revoked caller reports a clear error instead of a bare revert.cmd/livepeer/starter/starter.go— startup authorization check, and the service URI lookup moves to the orchestrator address (it is registered against the transcoder, so a node on a caller wallet previously resolved an empty URI and exited at startup).server/handlers.go—/setRewardCaller(empty value unsets via the zero address) and/rewardCaller;/rewardis orchestrator-aware;orchestratorInfoHandlerreports the orchestrator's record solivepeer_clishows real status, stake andLastRewardRoundon a caller wallet.cmd/livepeer_cli— a "Set reward caller" action that shows the current value and offers unset.Design decisions
The authorization check is strictly reward-gated.
-ethOrchAddralso designates ticket recipients for gateways, redeemers and orchestrators that never call reward. Those paths must not pay for an extra RPC or inherit a new failure mode, so-reward=falseissues no lookup at all.Explicit
-rewardexits when unauthorized; auto-enabled warns and stays off. The nil-sentinel inflags.goalready distinguishes intent — an explicit-rewardis a stated intent, so silently never rewarding is the worst outcome; auto-enable is a guess, and a guess that comes back "no" should not crash a node that never asked.Registration status is deliberately not a hard failure. An orchestrator may still be registering via
livepeer_cliagainst this very node, so exiting there would deadlock the "become an orchestrator" flow.Orchestrator config actions are now rejected on a delegated wallet. The contracts key these on
msg.sender. Reward cut / fee share reverts viaisRegisteredTranscoder, butServiceRegistry.setServiceURIhas no caller check at all and would silently succeed against the wrong key, costing gas while leaving the orchestrator's real URI untouched.setOrchestratorConfigcompounded this by diffing the request against an empty record and firing both transactions.No
RewardCallerSetevent watching —tryRewardre-reads authorization every round, so a watcher earns nothing.Verification
Verified against the deployed Arbitrum One BondingManager, not just unit tests:
rewardForTranscoderandrewardForTranscoderWithHintrevert with their own"caller must be a reward caller set by the transcoder"require, confirming the signatures.0x5bdeedca9c6346b0ce6b17ffa8227a4dace37039currently has caller0x1B0c26FC2E310eFB2649C24eC9AA966efFDA292F; the new binding returns exactly that, matchingcast. A set-then-unset pair on0x8ad7bcac720ddcc23cfb0b57fb3c7da02368e947correctly reads back as zero.The generated binding diff is provably additive: regenerating from the unmodified ABI reproduces the committed file byte-for-byte (zero-line diff), so the only changes are the reward-caller entries.
Tests cover authorized / unauthorized / revoked / legacy reward paths,
/setRewardCallerset and unset, and the delegated-wallet rejections. The eligibility test was mutation-checked — reverting the lookup to the caller's address makes it fail.go test ./eth/... ./server/... ./cmd/...passes.Not covered
End-to-end devnet run of set → restart → reward → revoke. There is no Arbitrum testnet in the network map (
starter.golists onlyrinkeby,arbitrum-one-rinkeby,mainnet,arbitrum-one-mainnet), so this needs a local chain with-ethController.Note for reviewers
setRewardCallersigns with the node's own wallet, so thelivepeer_cliaction is a pre-migration step — run it while the node still holds the orchestrator key, then reconfigure. Operators who prefer a hardware wallet can use the explorer instead.🤖 Generated with Claude Code