Skip to content

eth,server,cmd: Add LIP-118 reward caller support - #4011

Draft
rickstaa wants to merge 2 commits into
masterfrom
feat/reward-caller
Draft

eth,server,cmd: Add LIP-118 reward caller support#4011
rickstaa wants to merge 2 commits into
masterfrom
feat/reward-caller

Conversation

@rickstaa

@rickstaa rickstaa commented Aug 5, 2026

Copy link
Copy Markdown
Member

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: -ethOrchAddr is already the flag for the separate-wallet setup, so the node reads transcoderToRewardCaller(orchAddr) and takes the delegated path when it matches its own account.

Changes

Contract bindingssetRewardCaller, rewardForTranscoder, rewardForTranscoderWithHint, transcoderToRewardCaller, RewardCallerSet.

eth/client.goSetRewardCaller / GetRewardCaller / RewardForTranscoder. 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, 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 and Active == 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; /reward is orchestrator-aware; orchestratorInfoHandler reports the orchestrator's record so livepeer_cli shows real status, stake and LastRewardRound on 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. -ethOrchAddr also 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=false issues no lookup at all.

Explicit -reward exits when unauthorized; auto-enabled warns and stays off. The nil-sentinel in flags.go already distinguishes intent — an explicit -reward is 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_cli against 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 via isRegisteredTranscoder, but ServiceRegistry.setServiceURI has no caller check at all and would silently succeed against the wrong key, costing gas while leaving the orchestrator's real URI untouched. setOrchestratorConfig compounded this by diffing the request against an empty record and firing both transactions.

No RewardCallerSet event watchingtryReward re-reads authorization every round, so a watcher earns nothing.

Verification

Verified against the deployed Arbitrum One BondingManager, not just unit tests:

  • All four selectors dispatch; rewardForTranscoder and rewardForTranscoderWithHint revert with their own "caller must be a reward caller set by the transcoder" require, confirming the signatures.
  • The feature has real mainnet usage. 0x5bdeedca9c6346b0ce6b17ffa8227a4dace37039 currently has caller 0x1B0c26FC2E310eFB2649C24eC9AA966efFDA292F; the new binding returns exactly that, matching cast. A set-then-unset pair on 0x8ad7bcac720ddcc23cfb0b57fb3c7da02368e947 correctly 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, /setRewardCaller set 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.go lists only rinkeby, arbitrum-one-rinkeby, mainnet, arbitrum-one-mainnet), so this needs a local chain with -ethController.

Note for reviewers

setRewardCaller signs with the node's own wallet, so the livepeer_cli action 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

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>
Copilot AI review requested due to automatic review settings August 5, 2026 11:48
@github-actions github-actions Bot added the go Pull requests that update Go code label Aug 5, 2026
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 + eth client 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.

Comment thread eth/rewardservice.go
Comment on lines +32 to +33
// 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.
Comment on lines +1832 to +1835
var ethOrchAddr ethcommon.Address
if *cfg.EthOrchAddr != "" {
ethOrchAddr = ethcommon.HexToAddress(*cfg.EthOrchAddr)
}
Comment on lines +274 to +276
if !ethcommon.IsHexAddress(in) {
return "", fmt.Errorf("invalid hex address address=%v", in)
}
@codecov

codecov Bot commented Aug 5, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 27.46914% with 235 lines in your changes missing coverage. Please review.
✅ Project coverage is 35.43518%. Comparing base (cc49228) to head (098a259).

Files with missing lines Patch % Lines
eth/contracts/bondingManager.go 0.00000% 106 Missing ⚠️
cmd/livepeer_cli/wizard_transcoder.go 0.00000% 39 Missing ⚠️
cmd/livepeer/starter/starter.go 2.63158% 37 Missing ⚠️
server/handlers.go 72.22222% 16 Missing and 4 partials ⚠️
eth/stubclient.go 27.27273% 16 Missing ⚠️
eth/client.go 0.00000% 11 Missing ⚠️
eth/rewardservice.go 81.81818% 5 Missing and 1 partial ⚠️
Additional details and impacted files

Impacted file tree graph

@@                 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     
Files with missing lines Coverage Δ
cmd/livepeer_cli/livepeer_cli.go 0.00000% <ø> (ø)
server/webserver.go 85.57692% <100.00000%> (+0.28280%) ⬆️
eth/rewardservice.go 81.57895% <81.81818%> (+7.03350%) ⬆️
eth/client.go 4.58404% <0.00000%> (-0.07917%) ⬇️
eth/stubclient.go 15.68627% <27.27273%> (+1.15636%) ⬆️
server/handlers.go 57.33906% <72.22222%> (+0.82128%) ⬆️
cmd/livepeer/starter/starter.go 22.58462% <2.63158%> (-0.33855%) ⬇️
cmd/livepeer_cli/wizard_transcoder.go 0.00000% <0.00000%> (ø)
eth/contracts/bondingManager.go 0.00000% <0.00000%> (ø)

... and 4 files with indirect coverage changes


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update cc49228...098a259. Read the comment docs.

Files with missing lines Coverage Δ
cmd/livepeer_cli/livepeer_cli.go 0.00000% <ø> (ø)
server/webserver.go 85.57692% <100.00000%> (+0.28280%) ⬆️
eth/rewardservice.go 81.57895% <81.81818%> (+7.03350%) ⬆️
eth/client.go 4.58404% <0.00000%> (-0.07917%) ⬇️
eth/stubclient.go 15.68627% <27.27273%> (+1.15636%) ⬆️
server/handlers.go 57.33906% <72.22222%> (+0.82128%) ⬆️
cmd/livepeer/starter/starter.go 22.58462% <2.63158%> (-0.33855%) ⬇️
cmd/livepeer_cli/wizard_transcoder.go 0.00000% <0.00000%> (ø)
eth/contracts/bondingManager.go 0.00000% <0.00000%> (ø)

... and 4 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

go Pull requests that update Go code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants