Add reward caller (using transcoder-to-reward caller mapping direction) - #648
Conversation
Additional notes on the implementationNote-01:It is possible for a transcoder to set the RewardCaller as themself and call functions designated for RewardCallers. By doing so, the transcoder itself will be able to use the Note-02:It is possible for a TranscoderA to set the TranscoderB as RewardCaller. In this case, TranscoderB will be able to claim reward for themself by calling Note-03:In this implementation it is possible that multiple different transcoders set the same address as their RewardCaller. It will allow the same RewardCaller to call Note-04:It is not part of the current implementation, but it's possible to add a functionality to self-revoke RewardCaller. Currently, only the transcoder which set the RewardCaller can unset it by calling |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## delta #648 +/- ##
===============================================
Coverage 100.00000% 100.00000%
===============================================
Files 29 29
Lines 1338 1349 +11
Branches 225 226 +1
===============================================
+ Hits 1338 1349 +11
Continue to review full report in Codecov by Harness.
🚀 New features to boost your workflow:
|
* docs: add Delegated Reward Calling draft LIP Add a draft LIP for delegating the reward() call to a separate low-privilege address so an orchestrator's stake-holding wallet can stay cold. Based on the implementation in livepeer/protocol#648. * Update sidestream author name Co-authored-by: SidestreamCrunchyCarrot <crunchycarrot@sidestream.tech> * chore: add forum discussion thread * docs: assign LIP 118 Assign 118 to this LIP according to the convention to allign it with the authoring PR nr. --------- Co-authored-by: SidestreamCrunchyCarrot <crunchycarrot@sidestream.tech>
|
Non-blocking test suggestion: the auth require is never tested with a wrong caller while one is set (mapping = A, sender = B). It's somewhat implicit in the existing unset-caller test given the current implementation, but a specific test would guard future changes — it pins the check to "only the authorized address" vs "anyone once set": it("should fail if caller is not the reward caller set by the transcoder", async () => {
await bondingManager.connect(transcoder).setRewardCaller(nonTranscoder.address)
await expect(
bondingManager.connect(signers[2]).rewardForTranscoder(transcoder.address)
).to.be.revertedWith("caller must be a reward caller set by the transcoder")
}) |
rickstaa
left a comment
There was a problem hiding this comment.
@SidestreamSweatyPumpkin Implementation is correct, my personal security and code review found no exploits and no griefing angles (full notes in Notion). Summary: setter is self-scoped and revocable; delegated reward is economically identical to a self-call; no new external call sites; storage layout append-safe; all BondingManager unit tests pass.
Biggest finding was that we need to patch the explorer HistoryView in the explorer with the upgrade to ensure reward events still show up correctly for people using this new function. The change is not breaking the subgraph or go-livepeer. For go-livepeer as discussed before the changes are only addative.
Left some minor comments regarding wording and tests.
SidestreamCrunchyCarrot
left a comment
There was a problem hiding this comment.
Sanity checks:
- ✅ No changes unrelated to the PR description
- ✅ CI pass at the latest commit 99c0008
- ✅ Hardhat-based tests run locally on the latest block (
939 passing (6m)) - ✅ Test coverage is at 100%
dob
left a comment
There was a problem hiding this comment.
I'm comfortable with the logic introduced in this change. Though I did leave one open question about the modifiers on the rewardWithHint function to address.
| currentRoundInitialized | ||
| autoCheckpoint(msg.sender) | ||
| { | ||
| function rewardWithHint(address _newPosPrev, address _newPosNext) public { |
There was a problem hiding this comment.
Why did we do away with the whenSystemNotPaused, currentRoundInitialized, autoCheckpoint modifiers on this function?
There was a problem hiding this comment.
They didn't go away. All of those modifiers are being called in the underlying private function _rewardWithHint on line 926:
protocol/contracts/bonding/BondingManager.sol
Lines 922 to 926 in 99c0008
This way, both rewardWithHint and rewardForTranscoderWithHint has the same modifiers.
There was a problem hiding this comment.
To add to this, keeping it in the parent function may increase the risk of introducing side effects if rewardWithHint or any of the other external functions change.
Account history is keyed on the transaction sender:
transactions(where: { from: $account })
Since LIP-118 (livepeer/protocol#648) an orchestrator can nominate a
reward caller to submit reward() on its behalf. That transaction is sent
by the caller, so it drops out of the query and every event inside it -
including the RewardEvent - disappears from the orchestrator's history.
The event itself was always attributed correctly: RewardEvent.delegate is
the orchestrator regardless of who signed. The blind spot is purely in how
the Explorer asks for it. It cannot be fixed in place either, because
Transaction.events is typed as the Event interface, whose filter exposes
only id/timestamp/transaction/round - delegate lives on the concrete
RewardEvent type, so events_: { delegate: ... } does not exist.
So query the event entity directly, keyed on the orchestrator. This is the
same shape as winningTicketRedeemedEvents, already a separate role-keyed
query in this file for the same reason: a ticket's recipient is an event
param, so it survives being redeemed by a separate wallet.
Excluding RewardEvent from the transaction-path list is what keeps
self-called rewards, which appear in both queries, from rendering twice -
there is no id-based dedupe in this component.
Also require every list to be exhausted before paging stops. Keying that
on transactions alone cuts off exactly the accounts this fixes: an
orchestrator delegating every reward call has few transactions but one
reward event per round. This equally affects gateways with more tickets
than transactions, which 90ab4a1 fixed for totalLoaded but not reachedEnd.
Delegated calls render with the existing copy. Distinguishing them would
mean comparing transaction.from against the account, which is approximate
- a multisig or relayer shows as the sender. The authoritative answer is
Transcoder.rewardCaller, pending livepeer/subgraph#253.
livepeer/protocol#648 lets an orchestrator nominate an address to call reward() on its behalf. The subgraph had no visibility into these delegations. Adds Transcoder.rewardCaller (null when unset) and RewardCallerSetEvent, wired to RewardCallerSet(indexed address,indexed address). The Reward path is unchanged: #648 kept emitting Reward(_transcoder), so attribution stays on the orchestrator and both existing handlers remain correct. The RewardCallerSet ABI entry was added by hand from the protocol deployment artifact. See #252.
livepeer/protocol#648 lets an orchestrator nominate an address to call reward() on its behalf. The subgraph had no visibility into these delegations. Adds Transcoder.rewardCaller (null when unset) and RewardCallerSetEvent, wired to RewardCallerSet(indexed address,indexed address). The Reward path is unchanged: #648 kept emitting Reward(_transcoder), so attribution stays on the orchestrator and both existing handlers remain correct. The RewardCallerSet ABI entry was added by hand from the protocol deployment artifact. See #252.
What does this pull request do? Explain your changes. (required)
This PR adds a new feature to the livepeer protocol which will allow transcoders to set a single address they trust to claim the rewards every on round without requiring the main wallet to stay unlocked.
Specific updates (required)
contracts/bonding/BondingManager.solcontract is modified to includetranscoderToRewardCallerpublic mappingsetRewardCallerfunction to set or unset arewardCallerrewardForTranscoder,rewardForTranscoderWithHintfunctions to claim rewards usingrewardCalleraccounttest/unit/BondingManager.jstest is modified to include new "reward delegation" sectionyarn test:coverage:checkcommand is added to the CI to insure 100% test coverageHow did you test each of these updates (required)
A new "reward delegation" section contains tests that ensure:
rewardForTranscoderrewardForTranscoderDoes this pull request close any open issues?
No
Checklist:
README and other documentation updatedyarn testpass