diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index f9afe08498..237fb4bcc4 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -12,6 +12,8 @@ #### Orchestrator +- [#4011](https://github.com/livepeer/go-livepeer/pull/4011) Add LIP-118 reward caller support, so a low-privilege wallet can call `reward()` on behalf of an orchestrator whose own wallet stays offline. Set one with the new "Set reward caller" action in `livepeer_cli`, then run the node with that wallet and `-ethOrchAddr ` (@rickstaa) + #### Transcoder ### Bug Fixes 🐞 diff --git a/cmd/livepeer/starter/starter.go b/cmd/livepeer/starter/starter.go index a9a8e32838..da373d729b 100755 --- a/cmd/livepeer/starter/starter.go +++ b/cmd/livepeer/starter/starter.go @@ -1262,27 +1262,64 @@ func StartLivepeer(ctx context.Context, cfg LivepeerConfig) { glog.Infof("Redeemer started on %v", *cfg.HttpAddr) } + // Reward is called for recipientAddr, which is the node's own account unless + // -ethOrchAddr points at a separately registered orchestrator. Authorization is + // only ever checked when reward is actually going to run: -ethOrchAddr is also + // used for ticket recipients by gateways, redeemers and orchestrators that never + // call reward, and those must not pay for an extra lookup or inherit a new + // failure mode. var reward bool - if cfg.Reward == nil { - // If the node address is an on-chain registered address, start the reward service - t, err := n.Eth.GetTranscoder(n.Eth.Account().Address) - if err != nil { - glog.Error(err) - return + if cfg.Reward == nil || *cfg.Reward { + explicit := cfg.Reward != nil + + // A node running on the orchestrator's own wallet calls reward directly. + // Otherwise it must be the reward caller the orchestrator authorized + // on-chain, or every reward transaction would revert. Check up front so the + // operator finds out at startup rather than a round later. + authorized := true + if recipientAddr != n.Eth.Account().Address { + rewardCaller, err := n.Eth.GetRewardCaller(recipientAddr) + if err != nil { + glog.Errorf("Could not look up reward caller for orchestrator %v err=%q", recipientAddr.Hex(), err) + return + } + authorized = rewardCaller == n.Eth.Account().Address + if !authorized { + msg := fmt.Sprintf( + "node account %v is not the reward caller for orchestrator %v (on-chain reward caller is %v); "+ + "set it with livepeer_cli from the orchestrator's wallet", + n.Eth.Account().Address.Hex(), recipientAddr.Hex(), rewardCaller.Hex(), + ) + if explicit { + exit("-reward was set but %s", msg) + } + glog.Warningf("Not starting reward service; %s", msg) + } } - if t.Status == "Registered" { - reward = true - } else { + + switch { + case !authorized: reward = false + case explicit: + // Deliberately not gated on registration status. The orchestrator may + // still be registering, possibly via livepeer_cli against this very + // node, and the reward service is a no-op until it is active. + reward = true + default: + // Auto-enable only when there is a registered orchestrator to reward. + t, err := n.Eth.GetTranscoder(recipientAddr) + if err != nil { + glog.Error(err) + return + } + reward = t.Status == "Registered" } - } else { - reward = *cfg.Reward } if reward { // Start reward service // The node will only call reward if it is active in the current round - rs := eth.NewRewardService(n.Eth, timeWatcher) + rs := eth.NewRewardService(n.Eth, timeWatcher, recipientAddr) go func() { if err := rs.Start(ctx); err != nil { serviceErr <- err @@ -1792,7 +1829,12 @@ func StartLivepeer(ctx context.Context, cfg LivepeerConfig) { } else if n.NodeType == core.OrchestratorNode { *cfg.CliAddr = defaultAddr(*cfg.CliAddr, "127.0.0.1", OrchestratorCliPort) - suri, err := getServiceURI(n, *cfg.ServiceAddr) + var ethOrchAddr ethcommon.Address + if *cfg.EthOrchAddr != "" { + ethOrchAddr = ethcommon.HexToAddress(*cfg.EthOrchAddr) + } + + suri, err := getServiceURI(n, *cfg.ServiceAddr, ethOrchAddr) if err != nil { glog.Exit("Error getting service URI: ", err) } @@ -2261,7 +2303,9 @@ func isLocalURL(u string) (bool, error) { // Else: get on-chain sURI // If on-chain sURI mismatches inferred address: print warning // Return on-chain sURI -func getServiceURI(n *core.LivepeerNode, serviceAddr string) (*url.URL, error) { +// ethOrchAddr is the on-chain registered orchestrator whose service URI should be looked +// up. It is the zero address when the node's own account is the orchestrator. +func getServiceURI(n *core.LivepeerNode, serviceAddr string, ethOrchAddr ethcommon.Address) (*url.URL, error) { // Passed in via CLI if serviceAddr != "" { if serviceAddr == "none" { @@ -2298,8 +2342,14 @@ func getServiceURI(n *core.LivepeerNode, serviceAddr string) (*url.URL, error) { return inferredUri, err } - // On-chain lookup and matching with inferred public address - addr, err = n.Eth.GetServiceURI(n.Eth.Account().Address) + // On-chain lookup and matching with inferred public address. + // The service URI is registered against the orchestrator, so a node running on a + // reward caller wallet must not look it up under its own account. + uriAddr := n.Eth.Account().Address + if ethOrchAddr != (ethcommon.Address{}) { + uriAddr = ethOrchAddr + } + addr, err = n.Eth.GetServiceURI(uriAddr) if err != nil { glog.Errorf("Could not get service URI; orchestrator may be unreachable err=%q", err) return nil, err diff --git a/cmd/livepeer/starter/starter_test.go b/cmd/livepeer/starter/starter_test.go index 7f4eff34e6..a190bd1aae 100644 --- a/cmd/livepeer/starter/starter_test.go +++ b/cmd/livepeer/starter/starter_test.go @@ -91,23 +91,23 @@ func TestIsLocalURL(t *testing.T) { } func TestGetServiceURIServiceAddrScheme(t *testing.T) { - uri, err := getServiceURI(nil, "127.0.0.1:8935") + uri, err := getServiceURI(nil, "127.0.0.1:8935", ethcommon.Address{}) require.NoError(t, err) require.Equal(t, "https://127.0.0.1:8935", uri.String()) - uri, err = getServiceURI(nil, "http://127.0.0.1:8935") + uri, err = getServiceURI(nil, "http://127.0.0.1:8935", ethcommon.Address{}) require.NoError(t, err) require.Equal(t, "http://127.0.0.1:8935", uri.String()) - uri, err = getServiceURI(nil, "https://orch.example.com:443") + uri, err = getServiceURI(nil, "https://orch.example.com:443", ethcommon.Address{}) require.NoError(t, err) require.Equal(t, "https://orch.example.com:443", uri.String()) - uri, err = getServiceURI(nil, "gopher://orch.example.com:443") + uri, err = getServiceURI(nil, "gopher://orch.example.com:443", ethcommon.Address{}) require.NoError(t, err) require.Equal(t, "https://gopher://orch.example.com:443", uri.String()) - uri, err = getServiceURI(nil, "none") + uri, err = getServiceURI(nil, "none", ethcommon.Address{}) require.NoError(t, err) require.Equal(t, "", uri.String()) } diff --git a/cmd/livepeer_cli/livepeer_cli.go b/cmd/livepeer_cli/livepeer_cli.go index e73773cfdb..3b200b5170 100644 --- a/cmd/livepeer_cli/livepeer_cli.go +++ b/cmd/livepeer_cli/livepeer_cli.go @@ -97,6 +97,7 @@ func (w *wizard) initializeOptions() []wizardOpt { {desc: "Invoke \"reward\"", invoke: w.callReward, orchestrator: true}, {desc: "Invoke multi-step \"become an orchestrator\"", invoke: w.activateOrchestrator, orchestrator: true}, {desc: "Set orchestrator config", invoke: w.setOrchestratorConfig, orchestrator: true}, + {desc: "Set reward caller", invoke: w.setRewardCaller, orchestrator: true}, {desc: "Invoke \"deposit broadcasting funds\" (ETH)", invoke: w.deposit, notOrchestrator: true}, {desc: "Invoke \"unlock broadcasting funds\"", invoke: w.unlock, notOrchestrator: true}, {desc: "Invoke \"cancel unlock of broadcasting funds\"", invoke: w.cancelUnlock, notOrchestrator: true}, diff --git a/cmd/livepeer_cli/wizard_transcoder.go b/cmd/livepeer_cli/wizard_transcoder.go index 1c5f731ced..4ff21be01b 100644 --- a/cmd/livepeer_cli/wizard_transcoder.go +++ b/cmd/livepeer_cli/wizard_transcoder.go @@ -228,6 +228,67 @@ func (w *wizard) callReward() { httpGet(fmt.Sprintf("http://%v:%v/reward", w.host, w.httpPort)) } +// setRewardCaller authorizes another address to call reward on this orchestrator's behalf +// (LIP-118), so the orchestrator's own wallet can be kept offline. The authorization is +// keyed on msg.sender, so this must be run against a node holding the orchestrator's key. +func (w *wizard) setRewardCaller() { + if w.offchain { + fmt.Println("Cannot set a reward caller in off-chain mode") + return + } + + current := strings.TrimSpace(httpGet(fmt.Sprintf("http://%v:%v/rewardCaller", w.host, w.httpPort))) + + var rewardCaller string + if current != "" && current != "0x0000000000000000000000000000000000000000" { + fmt.Printf("Current reward caller: %v\n", current) + fmt.Print("Unset the current reward caller? (y/n) - ") + if w.readStringYesOrNo() == "y" { + fmt.Printf("Unsetting the reward caller. %v will no longer be able to call reward.\n", current) + } else { + rewardCaller = w.promptRewardCaller() + } + } else { + fmt.Println("No reward caller is currently set") + rewardCaller = w.promptRewardCaller() + } + + val := url.Values{"rewardCaller": {rewardCaller}} + result, ok := httpPostWithParams(fmt.Sprintf("http://%v:%v/setRewardCaller", w.host, w.httpPort), val) + if !ok { + fmt.Printf("Error setting reward caller: %s\n", result) + return + } + + if rewardCaller == "" { + fmt.Println("\nTransaction sent. Once confirmed, no address can call reward on your behalf.") + return + } + fmt.Printf("\nTransaction sent. Once confirmed, run the node with %v's keystore and -ethOrchAddr %v to call reward from it.\n", + rewardCaller, w.getOrchestratorAddr()) +} + +func (w *wizard) promptRewardCaller() string { + fmt.Print("Enter the address that should be allowed to call reward - ") + return w.readStringAndValidate(func(in string) (string, error) { + if !ethcommon.IsHexAddress(in) { + return "", fmt.Errorf("invalid hex address address=%v", in) + } + if ethcommon.HexToAddress(in) == (ethcommon.Address{}) { + return "", fmt.Errorf("cannot set the zero address; answer y to the unset prompt instead") + } + return in, nil + }) +} + +func (w *wizard) getOrchestratorAddr() string { + t, _, err := w.getOrchestratorInfo() + if err != nil || t == nil { + return "" + } + return t.Address.Hex() +} + func (w *wizard) vote() { if w.offchain { glog.Error("Can not vote in 'offchain' mode") diff --git a/eth/client.go b/eth/client.go index c092a80462..1e63663630 100644 --- a/eth/client.go +++ b/eth/client.go @@ -70,6 +70,12 @@ type LivepeerEthClient interface { // Staking Transcoder(blockRewardCut, feeShare *big.Int) (*types.Transaction, error) Reward() (*types.Transaction, error) + // Reward caller (LIP-118). RewardForTranscoder is called by an address the + // transcoder authorized via SetRewardCaller, so the transcoder's own wallet + // can stay offline. Passing the zero address to SetRewardCaller unsets it. + SetRewardCaller(rewardCaller ethcommon.Address) (*types.Transaction, error) + GetRewardCaller(transcoder ethcommon.Address) (ethcommon.Address, error) + RewardForTranscoder(transcoder ethcommon.Address) (*types.Transaction, error) Bond(amount *big.Int, toAddr ethcommon.Address) (*types.Transaction, error) Rebond(unbondingLockID *big.Int) (*types.Transaction, error) RebondFromUnbonded(toAddr ethcommon.Address, unbondingLockID *big.Int) (*types.Transaction, error) @@ -1021,7 +1027,23 @@ func (c *client) ProposalVoteWithReason(proposalId *big.Int, support uint8, reas } func (c *client) Reward() (*types.Transaction, error) { - addr := c.accountManager.Account().Address + return c.rewardFor(c.accountManager.Account().Address, false) +} + +// RewardForTranscoder calls reward on behalf of `transcoder`. The node's account must +// have been authorized by `transcoder` via SetRewardCaller, otherwise the transaction +// reverts with "caller must be a reward caller set by the transcoder". +func (c *client) RewardForTranscoder(transcoder ethcommon.Address) (*types.Transaction, error) { + return c.rewardFor(transcoder, true) +} + +// rewardFor computes the transcoder pool position hints for `transcoder` and submits the +// reward transaction. The hint math is identical for both paths and must always be based +// on the transcoder's stake, never the caller's. When `delegated` is set the transaction +// is submitted through rewardForTranscoderWithHint, which resolves the authorized caller +// on-chain; otherwise the caller is the transcoder itself. +func (c *client) rewardFor(transcoder ethcommon.Address, delegated bool) (*types.Transaction, error) { + addr := transcoder tr, err := c.GetTranscoder(addr) if err != nil { @@ -1065,9 +1087,26 @@ func (c *client) Reward() (*types.Transaction, error) { hints := simulateTranscoderPoolUpdate(addr, reward.Add(reward, tr.DelegatedStake), transcoders, len(transcoders) == int(maxSize.Int64())) + if delegated { + return c.bondingManager.RewardForTranscoderWithHint(c.transactOpts(), addr, hints.PosPrev, hints.PosNext) + } + return c.bondingManager.RewardWithHint(c.transactOpts(), hints.PosPrev, hints.PosNext) } +// SetRewardCaller authorizes `rewardCaller` to call reward on behalf of the node's +// account. Passing the zero address unsets any existing authorization. This must be sent +// from the transcoder's own wallet; the contract keys the mapping on msg.sender. +func (c *client) SetRewardCaller(rewardCaller ethcommon.Address) (*types.Transaction, error) { + return c.bondingManager.SetRewardCaller(c.transactOpts(), rewardCaller) +} + +// GetRewardCaller returns the address `transcoder` authorized to call reward on its +// behalf, or the zero address if none is set. +func (c *client) GetRewardCaller(transcoder ethcommon.Address) (ethcommon.Address, error) { + return c.bondingManager.TranscoderToRewardCaller(c.callOpts(), transcoder) +} + func (c *client) WithdrawFees(addr ethcommon.Address, amount *big.Int) (*types.Transaction, error) { return c.bondingManager.WithdrawFees(c.transactOpts(), addr, amount) } diff --git a/eth/contracts/bondingManager.go b/eth/contracts/bondingManager.go index 8dcd1a2538..ba687c51f9 100644 --- a/eth/contracts/bondingManager.go +++ b/eth/contracts/bondingManager.go @@ -31,7 +31,7 @@ var ( // BondingManagerMetaData contains all meta data concerning the BondingManager contract. var BondingManagerMetaData = &bind.MetaData{ - ABI: "[{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_controller\",\"type\":\"address\"}],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"newDelegate\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"oldDelegate\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"delegator\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"additionalAmount\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"bondedAmount\",\"type\":\"uint256\"}],\"name\":\"Bond\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"delegate\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"delegator\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"rewards\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"fees\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"startRound\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"endRound\",\"type\":\"uint256\"}],\"name\":\"EarningsClaimed\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"string\",\"name\":\"param\",\"type\":\"string\"}],\"name\":\"ParameterUpdate\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"delegate\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"delegator\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"unbondingLockId\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"Rebond\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"transcoder\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"Reward\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"address\",\"name\":\"controller\",\"type\":\"address\"}],\"name\":\"SetController\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"transcoder\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"activationRound\",\"type\":\"uint256\"}],\"name\":\"TranscoderActivated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"transcoder\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"deactivationRound\",\"type\":\"uint256\"}],\"name\":\"TranscoderDeactivated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"transcoder\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"address\",\"name\":\"finder\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"penalty\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"finderReward\",\"type\":\"uint256\"}],\"name\":\"TranscoderSlashed\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"transcoder\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"rewardCut\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"feeShare\",\"type\":\"uint256\"}],\"name\":\"TranscoderUpdate\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"oldDelegator\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"newDelegator\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"oldUnbondingLockId\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"newUnbondingLockId\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"TransferBond\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"transcoder\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"address\",\"name\":\"treasury\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"TreasuryReward\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"delegate\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"delegator\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"unbondingLockId\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"withdrawRound\",\"type\":\"uint256\"}],\"name\":\"Unbond\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"delegator\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"address\",\"name\":\"recipient\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"WithdrawFees\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"delegator\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"unbondingLockId\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"withdrawRound\",\"type\":\"uint256\"}],\"name\":\"WithdrawStake\",\"type\":\"event\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"_amount\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"_to\",\"type\":\"address\"}],\"name\":\"bond\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"_amount\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"_owner\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"_to\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"_oldDelegateNewPosPrev\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"_oldDelegateNewPosNext\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"_currDelegateNewPosPrev\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"_currDelegateNewPosNext\",\"type\":\"address\"}],\"name\":\"bondForWithHint\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"_amount\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"_to\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"_oldDelegateNewPosPrev\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"_oldDelegateNewPosNext\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"_currDelegateNewPosPrev\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"_currDelegateNewPosNext\",\"type\":\"address\"}],\"name\":\"bondWithHint\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_account\",\"type\":\"address\"}],\"name\":\"checkpointBondingState\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"_endRound\",\"type\":\"uint256\"}],\"name\":\"claimEarnings\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"controller\",\"outputs\":[{\"internalType\":\"contractIController\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"currentRoundTotalActiveStake\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_delegator\",\"type\":\"address\"}],\"name\":\"delegatorStatus\",\"outputs\":[{\"internalType\":\"enumBondingManager.DelegatorStatus\",\"name\":\"\",\"type\":\"uint8\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_delegator\",\"type\":\"address\"}],\"name\":\"getDelegator\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"bondedAmount\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"fees\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"delegateAddress\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"delegatedAmount\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"startRound\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"lastClaimRound\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"nextUnbondingLockId\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_delegator\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"_unbondingLockId\",\"type\":\"uint256\"}],\"name\":\"getDelegatorUnbondingLock\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"withdrawRound\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getFirstTranscoderInPool\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_transcoder\",\"type\":\"address\"}],\"name\":\"getNextTranscoderInPool\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getTotalBonded\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_transcoder\",\"type\":\"address\"}],\"name\":\"getTranscoder\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"lastRewardRound\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"rewardCut\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"feeShare\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"lastActiveStakeUpdateRound\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"activationRound\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"deactivationRound\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"activeCumulativeRewards\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"cumulativeRewards\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"cumulativeFees\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"lastFeeRound\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_transcoder\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"_round\",\"type\":\"uint256\"}],\"name\":\"getTranscoderEarningsPoolForRound\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"totalStake\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"transcoderRewardCut\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"transcoderFeeShare\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"cumulativeRewardFactor\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"cumulativeFeeFactor\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getTranscoderPoolMaxSize\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getTranscoderPoolSize\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_transcoder\",\"type\":\"address\"}],\"name\":\"isActiveTranscoder\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_transcoder\",\"type\":\"address\"}],\"name\":\"isRegisteredTranscoder\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_delegator\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"_unbondingLockId\",\"type\":\"uint256\"}],\"name\":\"isValidUnbondingLock\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"nextRoundTotalActiveStake\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"nextRoundTreasuryRewardCutRate\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_delegator\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"_endRound\",\"type\":\"uint256\"}],\"name\":\"pendingFees\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_delegator\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"_endRound\",\"type\":\"uint256\"}],\"name\":\"pendingStake\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"_unbondingLockId\",\"type\":\"uint256\"}],\"name\":\"rebond\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_to\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"_unbondingLockId\",\"type\":\"uint256\"}],\"name\":\"rebondFromUnbonded\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_to\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"_unbondingLockId\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"_newPosPrev\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"_newPosNext\",\"type\":\"address\"}],\"name\":\"rebondFromUnbondedWithHint\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"_unbondingLockId\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"_newPosPrev\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"_newPosNext\",\"type\":\"address\"}],\"name\":\"rebondWithHint\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"reward\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_newPosPrev\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"_newPosNext\",\"type\":\"address\"}],\"name\":\"rewardWithHint\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_controller\",\"type\":\"address\"}],\"name\":\"setController\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"setCurrentRoundTotalActiveStake\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"_numActiveTranscoders\",\"type\":\"uint256\"}],\"name\":\"setNumActiveTranscoders\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"_ceiling\",\"type\":\"uint256\"}],\"name\":\"setTreasuryBalanceCeiling\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"_cutRate\",\"type\":\"uint256\"}],\"name\":\"setTreasuryRewardCutRate\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint64\",\"name\":\"_unbondingPeriod\",\"type\":\"uint64\"}],\"name\":\"setUnbondingPeriod\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_transcoder\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"_finder\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"_slashAmount\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"_finderFee\",\"type\":\"uint256\"}],\"name\":\"slashTranscoder\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"targetContractId\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"_rewardCut\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"_feeShare\",\"type\":\"uint256\"}],\"name\":\"transcoder\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_transcoder\",\"type\":\"address\"}],\"name\":\"transcoderStatus\",\"outputs\":[{\"internalType\":\"enumBondingManager.TranscoderStatus\",\"name\":\"\",\"type\":\"uint8\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_transcoder\",\"type\":\"address\"}],\"name\":\"transcoderTotalStake\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"_rewardCut\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"_feeShare\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"_newPosPrev\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"_newPosNext\",\"type\":\"address\"}],\"name\":\"transcoderWithHint\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_delegator\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"_amount\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"_oldDelegateNewPosPrev\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"_oldDelegateNewPosNext\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"_newDelegateNewPosPrev\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"_newDelegateNewPosNext\",\"type\":\"address\"}],\"name\":\"transferBond\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"treasuryBalanceCeiling\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"treasuryRewardCutRate\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"_amount\",\"type\":\"uint256\"}],\"name\":\"unbond\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"_amount\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"_newPosPrev\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"_newPosNext\",\"type\":\"address\"}],\"name\":\"unbondWithHint\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"unbondingPeriod\",\"outputs\":[{\"internalType\":\"uint64\",\"name\":\"\",\"type\":\"uint64\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_transcoder\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"_fees\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"_round\",\"type\":\"uint256\"}],\"name\":\"updateTranscoderWithFees\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"addresspayable\",\"name\":\"_recipient\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"_amount\",\"type\":\"uint256\"}],\"name\":\"withdrawFees\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"_unbondingLockId\",\"type\":\"uint256\"}],\"name\":\"withdrawStake\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]", + ABI: "[{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_controller\",\"type\":\"address\"}],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"newDelegate\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"oldDelegate\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"delegator\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"additionalAmount\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"bondedAmount\",\"type\":\"uint256\"}],\"name\":\"Bond\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"delegate\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"delegator\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"rewards\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"fees\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"startRound\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"endRound\",\"type\":\"uint256\"}],\"name\":\"EarningsClaimed\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"string\",\"name\":\"param\",\"type\":\"string\"}],\"name\":\"ParameterUpdate\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"delegate\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"delegator\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"unbondingLockId\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"Rebond\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"transcoder\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"Reward\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"transcoder\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"rewardCaller\",\"type\":\"address\"}],\"name\":\"RewardCallerSet\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"address\",\"name\":\"controller\",\"type\":\"address\"}],\"name\":\"SetController\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"transcoder\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"activationRound\",\"type\":\"uint256\"}],\"name\":\"TranscoderActivated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"transcoder\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"deactivationRound\",\"type\":\"uint256\"}],\"name\":\"TranscoderDeactivated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"transcoder\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"address\",\"name\":\"finder\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"penalty\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"finderReward\",\"type\":\"uint256\"}],\"name\":\"TranscoderSlashed\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"transcoder\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"rewardCut\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"feeShare\",\"type\":\"uint256\"}],\"name\":\"TranscoderUpdate\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"oldDelegator\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"newDelegator\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"oldUnbondingLockId\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"newUnbondingLockId\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"TransferBond\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"transcoder\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"address\",\"name\":\"treasury\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"TreasuryReward\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"delegate\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"delegator\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"unbondingLockId\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"withdrawRound\",\"type\":\"uint256\"}],\"name\":\"Unbond\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"delegator\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"address\",\"name\":\"recipient\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"WithdrawFees\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"delegator\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"unbondingLockId\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"withdrawRound\",\"type\":\"uint256\"}],\"name\":\"WithdrawStake\",\"type\":\"event\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"_amount\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"_to\",\"type\":\"address\"}],\"name\":\"bond\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"_amount\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"_owner\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"_to\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"_oldDelegateNewPosPrev\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"_oldDelegateNewPosNext\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"_currDelegateNewPosPrev\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"_currDelegateNewPosNext\",\"type\":\"address\"}],\"name\":\"bondForWithHint\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"_amount\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"_to\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"_oldDelegateNewPosPrev\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"_oldDelegateNewPosNext\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"_currDelegateNewPosPrev\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"_currDelegateNewPosNext\",\"type\":\"address\"}],\"name\":\"bondWithHint\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_account\",\"type\":\"address\"}],\"name\":\"checkpointBondingState\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"_endRound\",\"type\":\"uint256\"}],\"name\":\"claimEarnings\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"controller\",\"outputs\":[{\"internalType\":\"contractIController\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"currentRoundTotalActiveStake\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_delegator\",\"type\":\"address\"}],\"name\":\"delegatorStatus\",\"outputs\":[{\"internalType\":\"enumBondingManager.DelegatorStatus\",\"name\":\"\",\"type\":\"uint8\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_delegator\",\"type\":\"address\"}],\"name\":\"getDelegator\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"bondedAmount\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"fees\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"delegateAddress\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"delegatedAmount\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"startRound\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"lastClaimRound\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"nextUnbondingLockId\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_delegator\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"_unbondingLockId\",\"type\":\"uint256\"}],\"name\":\"getDelegatorUnbondingLock\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"withdrawRound\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getFirstTranscoderInPool\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_transcoder\",\"type\":\"address\"}],\"name\":\"getNextTranscoderInPool\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getTotalBonded\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_transcoder\",\"type\":\"address\"}],\"name\":\"getTranscoder\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"lastRewardRound\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"rewardCut\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"feeShare\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"lastActiveStakeUpdateRound\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"activationRound\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"deactivationRound\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"activeCumulativeRewards\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"cumulativeRewards\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"cumulativeFees\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"lastFeeRound\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_transcoder\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"_round\",\"type\":\"uint256\"}],\"name\":\"getTranscoderEarningsPoolForRound\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"totalStake\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"transcoderRewardCut\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"transcoderFeeShare\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"cumulativeRewardFactor\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"cumulativeFeeFactor\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getTranscoderPoolMaxSize\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getTranscoderPoolSize\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_transcoder\",\"type\":\"address\"}],\"name\":\"isActiveTranscoder\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_transcoder\",\"type\":\"address\"}],\"name\":\"isRegisteredTranscoder\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_delegator\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"_unbondingLockId\",\"type\":\"uint256\"}],\"name\":\"isValidUnbondingLock\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"nextRoundTotalActiveStake\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"nextRoundTreasuryRewardCutRate\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_delegator\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"_endRound\",\"type\":\"uint256\"}],\"name\":\"pendingFees\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_delegator\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"_endRound\",\"type\":\"uint256\"}],\"name\":\"pendingStake\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"_unbondingLockId\",\"type\":\"uint256\"}],\"name\":\"rebond\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_to\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"_unbondingLockId\",\"type\":\"uint256\"}],\"name\":\"rebondFromUnbonded\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_to\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"_unbondingLockId\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"_newPosPrev\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"_newPosNext\",\"type\":\"address\"}],\"name\":\"rebondFromUnbondedWithHint\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"_unbondingLockId\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"_newPosPrev\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"_newPosNext\",\"type\":\"address\"}],\"name\":\"rebondWithHint\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"reward\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_transcoder\",\"type\":\"address\"}],\"name\":\"rewardForTranscoder\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_transcoder\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"_newPosPrev\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"_newPosNext\",\"type\":\"address\"}],\"name\":\"rewardForTranscoderWithHint\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_newPosPrev\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"_newPosNext\",\"type\":\"address\"}],\"name\":\"rewardWithHint\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_controller\",\"type\":\"address\"}],\"name\":\"setController\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"setCurrentRoundTotalActiveStake\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"_numActiveTranscoders\",\"type\":\"uint256\"}],\"name\":\"setNumActiveTranscoders\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_rewardCaller\",\"type\":\"address\"}],\"name\":\"setRewardCaller\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"_ceiling\",\"type\":\"uint256\"}],\"name\":\"setTreasuryBalanceCeiling\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"_cutRate\",\"type\":\"uint256\"}],\"name\":\"setTreasuryRewardCutRate\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint64\",\"name\":\"_unbondingPeriod\",\"type\":\"uint64\"}],\"name\":\"setUnbondingPeriod\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_transcoder\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"_finder\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"_slashAmount\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"_finderFee\",\"type\":\"uint256\"}],\"name\":\"slashTranscoder\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"targetContractId\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"_rewardCut\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"_feeShare\",\"type\":\"uint256\"}],\"name\":\"transcoder\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_transcoder\",\"type\":\"address\"}],\"name\":\"transcoderStatus\",\"outputs\":[{\"internalType\":\"enumBondingManager.TranscoderStatus\",\"name\":\"\",\"type\":\"uint8\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"name\":\"transcoderToRewardCaller\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_transcoder\",\"type\":\"address\"}],\"name\":\"transcoderTotalStake\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"_rewardCut\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"_feeShare\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"_newPosPrev\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"_newPosNext\",\"type\":\"address\"}],\"name\":\"transcoderWithHint\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_delegator\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"_amount\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"_oldDelegateNewPosPrev\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"_oldDelegateNewPosNext\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"_newDelegateNewPosPrev\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"_newDelegateNewPosNext\",\"type\":\"address\"}],\"name\":\"transferBond\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"treasuryBalanceCeiling\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"treasuryRewardCutRate\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"_amount\",\"type\":\"uint256\"}],\"name\":\"unbond\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"_amount\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"_newPosPrev\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"_newPosNext\",\"type\":\"address\"}],\"name\":\"unbondWithHint\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"unbondingPeriod\",\"outputs\":[{\"internalType\":\"uint64\",\"name\":\"\",\"type\":\"uint64\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_transcoder\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"_fees\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"_round\",\"type\":\"uint256\"}],\"name\":\"updateTranscoderWithFees\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"addresspayable\",\"name\":\"_recipient\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"_amount\",\"type\":\"uint256\"}],\"name\":\"withdrawFees\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"_unbondingLockId\",\"type\":\"uint256\"}],\"name\":\"withdrawStake\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]", } // BondingManagerABI is the input ABI used to generate the binding from. @@ -967,6 +967,37 @@ func (_BondingManager *BondingManagerCallerSession) TranscoderStatus(_transcoder return _BondingManager.Contract.TranscoderStatus(&_BondingManager.CallOpts, _transcoder) } +// TranscoderToRewardCaller is a free data retrieval call binding the contract method 0x47621410. +// +// Solidity: function transcoderToRewardCaller(address ) view returns(address) +func (_BondingManager *BondingManagerCaller) TranscoderToRewardCaller(opts *bind.CallOpts, arg0 common.Address) (common.Address, error) { + var out []interface{} + err := _BondingManager.contract.Call(opts, &out, "transcoderToRewardCaller", arg0) + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// TranscoderToRewardCaller is a free data retrieval call binding the contract method 0x47621410. +// +// Solidity: function transcoderToRewardCaller(address ) view returns(address) +func (_BondingManager *BondingManagerSession) TranscoderToRewardCaller(arg0 common.Address) (common.Address, error) { + return _BondingManager.Contract.TranscoderToRewardCaller(&_BondingManager.CallOpts, arg0) +} + +// TranscoderToRewardCaller is a free data retrieval call binding the contract method 0x47621410. +// +// Solidity: function transcoderToRewardCaller(address ) view returns(address) +func (_BondingManager *BondingManagerCallerSession) TranscoderToRewardCaller(arg0 common.Address) (common.Address, error) { + return _BondingManager.Contract.TranscoderToRewardCaller(&_BondingManager.CallOpts, arg0) +} + // TranscoderTotalStake is a free data retrieval call binding the contract method 0x9ef9df94. // // Solidity: function transcoderTotalStake(address _transcoder) view returns(uint256) @@ -1301,6 +1332,48 @@ func (_BondingManager *BondingManagerTransactorSession) Reward() (*types.Transac return _BondingManager.Contract.Reward(&_BondingManager.TransactOpts) } +// RewardForTranscoder is a paid mutator transaction binding the contract method 0x1a8c41f8. +// +// Solidity: function rewardForTranscoder(address _transcoder) returns() +func (_BondingManager *BondingManagerTransactor) RewardForTranscoder(opts *bind.TransactOpts, _transcoder common.Address) (*types.Transaction, error) { + return _BondingManager.contract.Transact(opts, "rewardForTranscoder", _transcoder) +} + +// RewardForTranscoder is a paid mutator transaction binding the contract method 0x1a8c41f8. +// +// Solidity: function rewardForTranscoder(address _transcoder) returns() +func (_BondingManager *BondingManagerSession) RewardForTranscoder(_transcoder common.Address) (*types.Transaction, error) { + return _BondingManager.Contract.RewardForTranscoder(&_BondingManager.TransactOpts, _transcoder) +} + +// RewardForTranscoder is a paid mutator transaction binding the contract method 0x1a8c41f8. +// +// Solidity: function rewardForTranscoder(address _transcoder) returns() +func (_BondingManager *BondingManagerTransactorSession) RewardForTranscoder(_transcoder common.Address) (*types.Transaction, error) { + return _BondingManager.Contract.RewardForTranscoder(&_BondingManager.TransactOpts, _transcoder) +} + +// RewardForTranscoderWithHint is a paid mutator transaction binding the contract method 0xb556966a. +// +// Solidity: function rewardForTranscoderWithHint(address _transcoder, address _newPosPrev, address _newPosNext) returns() +func (_BondingManager *BondingManagerTransactor) RewardForTranscoderWithHint(opts *bind.TransactOpts, _transcoder common.Address, _newPosPrev common.Address, _newPosNext common.Address) (*types.Transaction, error) { + return _BondingManager.contract.Transact(opts, "rewardForTranscoderWithHint", _transcoder, _newPosPrev, _newPosNext) +} + +// RewardForTranscoderWithHint is a paid mutator transaction binding the contract method 0xb556966a. +// +// Solidity: function rewardForTranscoderWithHint(address _transcoder, address _newPosPrev, address _newPosNext) returns() +func (_BondingManager *BondingManagerSession) RewardForTranscoderWithHint(_transcoder common.Address, _newPosPrev common.Address, _newPosNext common.Address) (*types.Transaction, error) { + return _BondingManager.Contract.RewardForTranscoderWithHint(&_BondingManager.TransactOpts, _transcoder, _newPosPrev, _newPosNext) +} + +// RewardForTranscoderWithHint is a paid mutator transaction binding the contract method 0xb556966a. +// +// Solidity: function rewardForTranscoderWithHint(address _transcoder, address _newPosPrev, address _newPosNext) returns() +func (_BondingManager *BondingManagerTransactorSession) RewardForTranscoderWithHint(_transcoder common.Address, _newPosPrev common.Address, _newPosNext common.Address) (*types.Transaction, error) { + return _BondingManager.Contract.RewardForTranscoderWithHint(&_BondingManager.TransactOpts, _transcoder, _newPosPrev, _newPosNext) +} + // RewardWithHint is a paid mutator transaction binding the contract method 0x81871056. // // Solidity: function rewardWithHint(address _newPosPrev, address _newPosNext) returns() @@ -1385,6 +1458,27 @@ func (_BondingManager *BondingManagerTransactorSession) SetNumActiveTranscoders( return _BondingManager.Contract.SetNumActiveTranscoders(&_BondingManager.TransactOpts, _numActiveTranscoders) } +// SetRewardCaller is a paid mutator transaction binding the contract method 0x0103e60e. +// +// Solidity: function setRewardCaller(address _rewardCaller) returns() +func (_BondingManager *BondingManagerTransactor) SetRewardCaller(opts *bind.TransactOpts, _rewardCaller common.Address) (*types.Transaction, error) { + return _BondingManager.contract.Transact(opts, "setRewardCaller", _rewardCaller) +} + +// SetRewardCaller is a paid mutator transaction binding the contract method 0x0103e60e. +// +// Solidity: function setRewardCaller(address _rewardCaller) returns() +func (_BondingManager *BondingManagerSession) SetRewardCaller(_rewardCaller common.Address) (*types.Transaction, error) { + return _BondingManager.Contract.SetRewardCaller(&_BondingManager.TransactOpts, _rewardCaller) +} + +// SetRewardCaller is a paid mutator transaction binding the contract method 0x0103e60e. +// +// Solidity: function setRewardCaller(address _rewardCaller) returns() +func (_BondingManager *BondingManagerTransactorSession) SetRewardCaller(_rewardCaller common.Address) (*types.Transaction, error) { + return _BondingManager.Contract.SetRewardCaller(&_BondingManager.TransactOpts, _rewardCaller) +} + // SetTreasuryBalanceCeiling is a paid mutator transaction binding the contract method 0x40e6f271. // // Solidity: function setTreasuryBalanceCeiling(uint256 _ceiling) returns() @@ -2392,6 +2486,159 @@ func (_BondingManager *BondingManagerFilterer) ParseReward(log types.Log) (*Bond return event, nil } +// BondingManagerRewardCallerSetIterator is returned from FilterRewardCallerSet and is used to iterate over the raw logs and unpacked data for RewardCallerSet events raised by the BondingManager contract. +type BondingManagerRewardCallerSetIterator struct { + Event *BondingManagerRewardCallerSet // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *BondingManagerRewardCallerSetIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(BondingManagerRewardCallerSet) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(BondingManagerRewardCallerSet) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *BondingManagerRewardCallerSetIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *BondingManagerRewardCallerSetIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// BondingManagerRewardCallerSet represents a RewardCallerSet event raised by the BondingManager contract. +type BondingManagerRewardCallerSet struct { + Transcoder common.Address + RewardCaller common.Address + Raw types.Log // Blockchain specific contextual infos +} + +// FilterRewardCallerSet is a free log retrieval operation binding the contract event 0x15932b87747cac36d3c505e86a946c6d3a79d9d7bdb79d81584d32bbe54516c2. +// +// Solidity: event RewardCallerSet(address indexed transcoder, address indexed rewardCaller) +func (_BondingManager *BondingManagerFilterer) FilterRewardCallerSet(opts *bind.FilterOpts, transcoder []common.Address, rewardCaller []common.Address) (*BondingManagerRewardCallerSetIterator, error) { + + var transcoderRule []interface{} + for _, transcoderItem := range transcoder { + transcoderRule = append(transcoderRule, transcoderItem) + } + var rewardCallerRule []interface{} + for _, rewardCallerItem := range rewardCaller { + rewardCallerRule = append(rewardCallerRule, rewardCallerItem) + } + + logs, sub, err := _BondingManager.contract.FilterLogs(opts, "RewardCallerSet", transcoderRule, rewardCallerRule) + if err != nil { + return nil, err + } + return &BondingManagerRewardCallerSetIterator{contract: _BondingManager.contract, event: "RewardCallerSet", logs: logs, sub: sub}, nil +} + +// WatchRewardCallerSet is a free log subscription operation binding the contract event 0x15932b87747cac36d3c505e86a946c6d3a79d9d7bdb79d81584d32bbe54516c2. +// +// Solidity: event RewardCallerSet(address indexed transcoder, address indexed rewardCaller) +func (_BondingManager *BondingManagerFilterer) WatchRewardCallerSet(opts *bind.WatchOpts, sink chan<- *BondingManagerRewardCallerSet, transcoder []common.Address, rewardCaller []common.Address) (event.Subscription, error) { + + var transcoderRule []interface{} + for _, transcoderItem := range transcoder { + transcoderRule = append(transcoderRule, transcoderItem) + } + var rewardCallerRule []interface{} + for _, rewardCallerItem := range rewardCaller { + rewardCallerRule = append(rewardCallerRule, rewardCallerItem) + } + + logs, sub, err := _BondingManager.contract.WatchLogs(opts, "RewardCallerSet", transcoderRule, rewardCallerRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(BondingManagerRewardCallerSet) + if err := _BondingManager.contract.UnpackLog(event, "RewardCallerSet", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseRewardCallerSet is a log parse operation binding the contract event 0x15932b87747cac36d3c505e86a946c6d3a79d9d7bdb79d81584d32bbe54516c2. +// +// Solidity: event RewardCallerSet(address indexed transcoder, address indexed rewardCaller) +func (_BondingManager *BondingManagerFilterer) ParseRewardCallerSet(log types.Log) (*BondingManagerRewardCallerSet, error) { + event := new(BondingManagerRewardCallerSet) + if err := _BondingManager.contract.UnpackLog(event, "RewardCallerSet", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + // BondingManagerSetControllerIterator is returned from FilterSetController and is used to iterate over the raw logs and unpacked data for SetController events raised by the BondingManager contract. type BondingManagerSetControllerIterator struct { Event *BondingManagerSetController // Event containing the contract specifics and raw log diff --git a/eth/rewardservice.go b/eth/rewardservice.go index bd10a062a3..ef709eb622 100644 --- a/eth/rewardservice.go +++ b/eth/rewardservice.go @@ -3,8 +3,10 @@ package eth import ( "context" "fmt" + "math/big" "sync" + ethcommon "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/golang/glog" "github.com/livepeer/go-livepeer/monitor" @@ -16,17 +18,24 @@ var ( ) type RewardService struct { - client LivepeerEthClient - working bool + client LivepeerEthClient + working bool + // orchAddr is the transcoder reward is called for. It is the zero address when the + // node's own account is the transcoder; otherwise the node acts as a reward caller + // authorized by orchAddr (LIP-118). + orchAddr ethcommon.Address cancelWorker context.CancelFunc tw timeWatcher mu sync.Mutex } -func NewRewardService(client LivepeerEthClient, tw timeWatcher) *RewardService { +// 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. +func NewRewardService(client LivepeerEthClient, tw timeWatcher, orchAddr ethcommon.Address) *RewardService { return &RewardService{ - client: client, - tw: tw, + client: client, + tw: tw, + orchAddr: orchAddr, } } @@ -91,25 +100,59 @@ func (s *RewardService) tryReward() error { currentRound := s.tw.LastInitializedRound() - t, err := s.client.GetTranscoder(s.client.Account().Address) + account := s.client.Account().Address + // Eligibility is always a property of the transcoder, never of the caller. Reading + // the caller's record here would return an empty, inactive transcoder and silently + // skip the reward call for the whole round. + transcoder := s.orchAddr + delegated := transcoder != (ethcommon.Address{}) && transcoder != account + if !delegated { + transcoder = account + } + + t, err := s.client.GetTranscoder(transcoder) if err != nil { return err } - if t.LastRewardRound.Cmp(currentRound) == -1 && t.Active { - tx, err := s.client.Reward() - if err != nil { - return err - } + if t.LastRewardRound.Cmp(currentRound) != -1 || !t.Active { + return nil + } - if err := s.client.CheckTx(tx); err != nil { - return err - } + if !delegated { + return s.sendReward(currentRound, func() (*types.Transaction, error) { return s.client.Reward() }) + } - glog.Infof("Called reward for round %v", currentRound) + // Re-check authorization every round so a revoked reward caller surfaces as a clear + // error rather than a bare on-chain revert. + rewardCaller, err := s.client.GetRewardCaller(transcoder) + if err != nil { + return fmt.Errorf("could not look up reward caller for transcoder %v: %w", transcoder.Hex(), err) + } + if rewardCaller != account { + return fmt.Errorf( + "node account %v is not the reward caller for transcoder %v (on-chain reward caller is %v); "+ + "set it with livepeer_cli from the transcoder's wallet", + account.Hex(), transcoder.Hex(), rewardCaller.Hex(), + ) + } - return nil + return s.sendReward(currentRound, func() (*types.Transaction, error) { + return s.client.RewardForTranscoder(transcoder) + }) +} + +func (s *RewardService) sendReward(currentRound *big.Int, send func() (*types.Transaction, error)) error { + tx, err := send() + if err != nil { + return err } + if err := s.client.CheckTx(tx); err != nil { + return err + } + + glog.Infof("Called reward for round %v", currentRound) + return nil } diff --git a/eth/rewardservice_test.go b/eth/rewardservice_test.go index b707ecd309..5697673383 100644 --- a/eth/rewardservice_test.go +++ b/eth/rewardservice_test.go @@ -2,11 +2,13 @@ package eth import ( "context" - ethcommon "github.com/ethereum/go-ethereum/common" + "errors" "math/big" "testing" "time" + ethcommon "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/core/types" "github.com/golang/glog" @@ -127,3 +129,151 @@ func TestRewardService_ReceiveRoundEvent_TryReward(t *testing.T) { assert.Equal(int64(1), errorLogsAfter-errorLogsBefore) assert.Equal(int64(0), infoLogsAfter-infoLogsBefore) } + +func TestRewardService_TryReward_RewardCaller(t *testing.T) { + var ( + account = ethcommon.HexToAddress("0x1111111111111111111111111111111111111111") + orch = ethcommon.HexToAddress("0x2222222222222222222222222222222222222222") + other = ethcommon.HexToAddress("0x3333333333333333333333333333333333333333") + + eligible = &lpTypes.Transcoder{LastRewardRound: big.NewInt(1), Active: true} + rewarded = &lpTypes.Transcoder{LastRewardRound: big.NewInt(100), Active: true} + inactive = &lpTypes.Transcoder{LastRewardRound: big.NewInt(1), Active: false} + currentRnd = big.NewInt(100) + ) + + tests := []struct { + name string + // orchAddr configured on the service + orchAddr ethcommon.Address + // transcoder records keyed by address + accountRecord *lpTypes.Transcoder + orchRecord *lpTypes.Transcoder + // on-chain reward caller for orch + rewardCaller ethcommon.Address + + wantReward bool + wantRewardForTranscoder bool + wantErr string + }{ + { + name: "not delegated, zero orchAddr, calls reward directly", + orchAddr: ethcommon.Address{}, + accountRecord: eligible, + wantReward: true, + }, + { + name: "not delegated, orchAddr equals account, calls reward directly", + orchAddr: account, + accountRecord: eligible, + wantReward: true, + }, + { + name: "delegated and authorized, calls rewardForTranscoder", + orchAddr: orch, + orchRecord: eligible, + rewardCaller: account, + wantRewardForTranscoder: true, + }, + { + name: "delegated but not authorized, errors without sending a tx", + orchAddr: orch, + orchRecord: eligible, + rewardCaller: other, + wantErr: "is not the reward caller", + }, + { + name: "delegated and revoked, errors without sending a tx", + orchAddr: orch, + orchRecord: eligible, + rewardCaller: ethcommon.Address{}, + wantErr: "is not the reward caller", + }, + { + // The caller's own record is eligible; the orchestrator's is not. Eligibility + // must be read from the orchestrator or the node would reward twice a round. + name: "delegated, eligibility read from orchestrator not caller", + orchAddr: orch, + accountRecord: eligible, + orchRecord: rewarded, + rewardCaller: account, + }, + { + name: "delegated, orchestrator inactive", + orchAddr: orch, + accountRecord: eligible, + orchRecord: inactive, + rewardCaller: account, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert := assert.New(t) + client := &MockClient{} + rs := RewardService{ + client: client, + tw: &stubTimeWatcher{lastInitializedRound: currentRnd}, + orchAddr: tt.orchAddr, + } + + client.On("Account").Return(accounts.Account{Address: account}) + if tt.accountRecord != nil { + client.On("GetTranscoder", account).Return(tt.accountRecord, nil) + } + if tt.orchRecord != nil { + client.On("GetTranscoder", orch).Return(tt.orchRecord, nil) + } + client.On("GetRewardCaller", orch).Return(tt.rewardCaller, nil) + client.On("Reward").Return(&types.Transaction{}, nil) + client.On("RewardForTranscoder", orch).Return(&types.Transaction{}, nil) + client.On("CheckTx").Return(nil) + client.On("GetTranscoderEarningsPoolForRound").Return(&lpTypes.TokenPools{}, nil) + + err := rs.tryReward() + + if tt.wantErr != "" { + assert.ErrorContains(err, tt.wantErr) + } else { + assert.NoError(err) + } + + if tt.wantReward { + client.AssertNumberOfCalls(t, "Reward", 1) + } else { + client.AssertNumberOfCalls(t, "Reward", 0) + } + if tt.wantRewardForTranscoder { + client.AssertCalled(t, "RewardForTranscoder", orch) + } else { + client.AssertNumberOfCalls(t, "RewardForTranscoder", 0) + } + }) + } +} + +func TestRewardService_TryReward_RewardCallerLookupError(t *testing.T) { + assert := assert.New(t) + account := ethcommon.HexToAddress("0x1111111111111111111111111111111111111111") + orch := ethcommon.HexToAddress("0x2222222222222222222222222222222222222222") + + client := &MockClient{} + rs := RewardService{ + client: client, + tw: &stubTimeWatcher{lastInitializedRound: big.NewInt(100)}, + orchAddr: orch, + } + + client.On("Account").Return(accounts.Account{Address: account}) + client.On("GetTranscoder", orch).Return(&lpTypes.Transcoder{LastRewardRound: big.NewInt(1), Active: true}, nil) + client.On("GetRewardCaller", orch).Return(ethcommon.Address{}, errors.New("rpc boom")) + client.On("Reward").Return(&types.Transaction{}, nil) + client.On("RewardForTranscoder", orch).Return(&types.Transaction{}, nil) + + err := rs.tryReward() + + assert.ErrorContains(err, "could not look up reward caller") + assert.ErrorContains(err, "rpc boom") + client.AssertNumberOfCalls(t, "Reward", 0) + client.AssertNumberOfCalls(t, "RewardForTranscoder", 0) +} diff --git a/eth/stubclient.go b/eth/stubclient.go index a5204ef4dd..3f6cd0c27d 100644 --- a/eth/stubclient.go +++ b/eth/stubclient.go @@ -100,6 +100,21 @@ func (m *MockClient) Reward() (*types.Transaction, error) { return mockTransaction(args, 0), args.Error(1) } +func (m *MockClient) RewardForTranscoder(transcoder common.Address) (*types.Transaction, error) { + args := m.Called(transcoder) + return mockTransaction(args, 0), args.Error(1) +} + +func (m *MockClient) SetRewardCaller(rewardCaller common.Address) (*types.Transaction, error) { + args := m.Called(rewardCaller) + return mockTransaction(args, 0), args.Error(1) +} + +func (m *MockClient) GetRewardCaller(transcoder common.Address) (common.Address, error) { + args := m.Called(transcoder) + return args.Get(0).(common.Address), args.Error(1) +} + func (m *MockClient) GetTranscoderEarningsPoolForRound(address common.Address, round *big.Int) (*lpTypes.TokenPools, error) { args := m.Called() return args.Get(0).(*lpTypes.TokenPools), args.Error(1) @@ -266,6 +281,17 @@ type StubClient struct { RoundLocked bool RoundLockedErr error Errors map[string]error + + // OrchByAddr lets a test distinguish the transcoder record of the node's own + // account from that of a separate orchestrator, which is required to exercise + // the reward caller paths. GetTranscoder falls back to Orch when unset. + OrchByAddr map[common.Address]*lpTypes.Transcoder + // RewardCallers maps a transcoder to the address it authorized to call reward. + RewardCallers map[common.Address]common.Address + // RewardedTranscoder records the argument of the last RewardForTranscoder call. + RewardedTranscoder common.Address + // LastSetRewardCaller records the argument of the last SetRewardCaller call. + LastSetRewardCaller common.Address } type stubTranscoder struct { @@ -324,7 +350,21 @@ func (e *StubClient) GetServiceURI(addr common.Address) (string, error) { func (e *StubClient) Transcoder(blockRewardCut, feeShare *big.Int) (*types.Transaction, error) { return nil, nil } -func (e *StubClient) Reward() (*types.Transaction, error) { return nil, nil } +func (e *StubClient) Reward() (*types.Transaction, error) { return nil, e.Errors["Reward"] } +func (e *StubClient) RewardForTranscoder(transcoder common.Address) (*types.Transaction, error) { + e.RewardedTranscoder = transcoder + return nil, e.Errors["RewardForTranscoder"] +} +func (e *StubClient) SetRewardCaller(rewardCaller common.Address) (*types.Transaction, error) { + e.LastSetRewardCaller = rewardCaller + return nil, e.Errors["SetRewardCaller"] +} +func (e *StubClient) GetRewardCaller(transcoder common.Address) (common.Address, error) { + if err := e.Errors["GetRewardCaller"]; err != nil { + return common.Address{}, err + } + return e.RewardCallers[transcoder], nil +} func (e *StubClient) Bond(amount *big.Int, toAddr common.Address) (*types.Transaction, error) { return nil, nil } @@ -351,6 +391,9 @@ func (e *StubClient) GetTranscoder(addr common.Address) (*lpTypes.Transcoder, er if e.Err != nil { return nil, e.Err } + if t, ok := e.OrchByAddr[addr]; ok { + return t, nil + } return e.Orch, nil } func (e *StubClient) GetDelegator(addr common.Address) (*lpTypes.Delegator, error) { return nil, nil } diff --git a/server/handlers.go b/server/handlers.go index b6ac1928e7..efba1bfd54 100644 --- a/server/handlers.go +++ b/server/handlers.go @@ -103,7 +103,13 @@ type orchInfo struct { func (s *LivepeerServer) orchestratorInfoHandler(client eth.LivepeerEthClient) http.Handler { return mustHaveClient(client, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - addr := client.Account().Address + // Report the orchestrator's record, not the node account's. On a delegated + // wallet the account is not a transcoder, so reading it would return an empty + // record and make the CLI show a stale status, stake and LastRewardRound. + addr, delegated := s.delegatedOrchestrator(client) + if !delegated { + addr = client.Account().Address + } t, err := client.GetTranscoder(addr) if err != nil { respond500(w, "could not get transcoder") @@ -451,8 +457,46 @@ func roundInitializedHandler(client eth.LivepeerEthClient) http.Handler { } // Orchestrator registration/activation +// delegatedOrchestrator returns the on-chain orchestrator this node acts for and true when +// that differs from the node's own account, i.e. the node runs on a delegated wallet such +// as a LIP-118 reward caller. It returns false for the common case where the node's own +// account is the orchestrator. +func (s *LivepeerServer) delegatedOrchestrator(client eth.LivepeerEthClient) (ethcommon.Address, bool) { + if s.LivepeerNode == nil || s.LivepeerNode.RecipientAddr == "" { + return ethcommon.Address{}, false + } + orch := ethcommon.HexToAddress(s.LivepeerNode.RecipientAddr) + if orch == client.Account().Address { + return ethcommon.Address{}, false + } + return orch, true +} + +// rejectIfDelegatedWallet blocks actions that the contracts key on msg.sender and that +// therefore only make sense from the orchestrator's own wallet. Without this guard a node +// running on a reward caller wallet would send setServiceURI transactions that succeed but +// write to the caller's key -- the ServiceRegistry has no caller check of its own -- and +// reward cut / fee share transactions that revert. Returns true when the request was +// answered and the caller should stop. +func (s *LivepeerServer) rejectIfDelegatedWallet(w http.ResponseWriter, client eth.LivepeerEthClient) bool { + orch, delegated := s.delegatedOrchestrator(client) + if !delegated { + return false + } + respond400(w, fmt.Sprintf( + "this node runs on wallet %v, which is not orchestrator %v; "+ + "orchestrator configuration must be sent from the orchestrator's own wallet", + client.Account().Address.Hex(), orch.Hex(), + )) + return true +} + func (s *LivepeerServer) activateOrchestratorHandler(client eth.LivepeerEthClient) http.Handler { return mustHaveClient(client, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if s.rejectIfDelegatedWallet(w, client) { + return + } + t, err := client.GetTranscoder(client.Account().Address) if err != nil { glog.Error(err) @@ -583,6 +627,10 @@ func (s *LivepeerServer) activateOrchestratorHandler(client eth.LivepeerEthClien func (s *LivepeerServer) setOrchestratorConfigHandler(client eth.LivepeerEthClient) http.Handler { return mustHaveClient(client, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if s.rejectIfDelegatedWallet(w, client) { + return + } + pixels := r.FormValue("pixelsPerUnit") price := r.FormValue("pricePerUnit") currency := r.FormValue("currency") @@ -1101,10 +1149,21 @@ func registeredOrchestratorsHandler(client eth.LivepeerEthClient, db *common.DB) }))) } -func rewardHandler(client eth.LivepeerEthClient) http.Handler { +func (s *LivepeerServer) rewardHandler(client eth.LivepeerEthClient) http.Handler { return mustHaveClient(client, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - glog.Infof("Calling reward") - tx, err := client.Reward() + orch, delegated := s.delegatedOrchestrator(client) + + var ( + tx *ethtypes.Transaction + err error + ) + if delegated { + glog.Infof("Calling reward for orchestrator %v", orch.Hex()) + tx, err = client.RewardForTranscoder(orch) + } else { + glog.Infof("Calling reward") + tx, err = client.Reward() + } if err != nil { respond500(w, fmt.Sprintf("Error calling reward: %v", err)) return @@ -1118,6 +1177,66 @@ func rewardHandler(client eth.LivepeerEthClient) http.Handler { })) } +// setRewardCallerHandler authorizes an address to call reward on behalf of this node's +// account (LIP-118). An empty rewardCaller param unsets any existing authorization. +func (s *LivepeerServer) setRewardCallerHandler(client eth.LivepeerEthClient) http.Handler { + return mustHaveClient(client, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // The contract keys the mapping on msg.sender, so this only ever authorizes a + // caller for the wallet the node is running on. + if s.rejectIfDelegatedWallet(w, client) { + return + } + + var rewardCaller ethcommon.Address + if v := strings.TrimSpace(r.FormValue("rewardCaller")); v != "" { + if !ethcommon.IsHexAddress(v) { + respond400(w, fmt.Sprintf("invalid reward caller address %v", v)) + return + } + rewardCaller = ethcommon.HexToAddress(v) + } + + if rewardCaller == (ethcommon.Address{}) { + glog.Infof("Unsetting reward caller for %v", client.Account().Address.Hex()) + } else { + glog.Infof("Setting reward caller %v for %v", rewardCaller.Hex(), client.Account().Address.Hex()) + } + + tx, err := client.SetRewardCaller(rewardCaller) + if err != nil { + respond500(w, fmt.Sprintf("Error setting reward caller: %v", err)) + return + } + if err := client.CheckTx(tx); err != nil { + respond500(w, fmt.Sprintf("Error setting reward caller: %v", err)) + return + } + glog.Infof("Reward caller updated") + respondOk(w, nil) + })) +} + +// rewardCallerHandler returns the address currently authorized to call reward for the +// orchestrator this node acts for, or the empty string when none is set. +func (s *LivepeerServer) rewardCallerHandler(client eth.LivepeerEthClient) http.Handler { + return mustHaveClient(client, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + orch, delegated := s.delegatedOrchestrator(client) + if !delegated { + orch = client.Account().Address + } + rewardCaller, err := client.GetRewardCaller(orch) + if err != nil { + respond500(w, fmt.Sprintf("Error getting reward caller: %v", err)) + return + } + if rewardCaller == (ethcommon.Address{}) { + respondOk(w, []byte("")) + return + } + respondOk(w, []byte(rewardCaller.Hex())) + })) +} + // Protocol parameters func protocolParametersHandler(client eth.LivepeerEthClient, db ChainIdGetter) http.Handler { return mustHaveDb(db, mustHaveClient(client, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/server/handlers_test.go b/server/handlers_test.go index 55c3aabca9..9b3415b5ea 100644 --- a/server/handlers_test.go +++ b/server/handlers_test.go @@ -25,6 +25,7 @@ import ( "github.com/livepeer/lpms/ffmpeg" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" ) // Status @@ -1072,7 +1073,8 @@ func TestRewardHandler(t *testing.T) { assert := assert.New(t) client := ð.MockClient{} - handler := rewardHandler(client) + s := &LivepeerServer{} + handler := s.rewardHandler(client) client.On("Reward").Return(nil, nil) client.On("CheckTx", mock.Anything).Return(nil) @@ -1965,3 +1967,132 @@ func mockBigInt(args mock.Arguments) (*big.Int, error) { func trim(str []byte) string { return strings.TrimSpace(string(str)) } + +func TestSetRewardCallerHandler(t *testing.T) { + caller := ethcommon.HexToAddress("0x3333333333333333333333333333333333333333") + + tests := []struct { + name string + form url.Values + wantStatus int + wantSet *ethcommon.Address + }{ + { + name: "sets the reward caller", + form: url.Values{"rewardCaller": {caller.Hex()}}, + wantStatus: http.StatusOK, + wantSet: &caller, + }, + { + name: "empty value unsets via the zero address", + form: url.Values{"rewardCaller": {""}}, + wantStatus: http.StatusOK, + wantSet: ðcommon.Address{}, + }, + { + name: "missing param unsets via the zero address", + form: url.Values{}, + wantStatus: http.StatusOK, + wantSet: ðcommon.Address{}, + }, + { + name: "rejects a malformed address", + form: url.Values{"rewardCaller": {"not-an-address"}}, + wantStatus: http.StatusBadRequest, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert := assert.New(t) + client := ð.MockClient{} + s := &LivepeerServer{} + handler := s.setRewardCallerHandler(client) + + client.On("Account").Return(accounts.Account{}) + client.On("SetRewardCaller", mock.Anything).Return(nil, nil) + client.On("CheckTx", mock.Anything).Return(nil) + + status, _ := postForm(handler, tt.form) + assert.Equal(tt.wantStatus, status) + + if tt.wantSet != nil { + client.AssertCalled(t, "SetRewardCaller", *tt.wantSet) + } else { + client.AssertNumberOfCalls(t, "SetRewardCaller", 0) + } + }) + } +} + +// A node running on a delegated (reward caller) wallet must not send orchestrator +// configuration transactions. setServiceURI in particular has no on-chain caller check, +// so it would silently succeed against the wrong key. +func TestOrchestratorConfigHandlers_RejectDelegatedWallet(t *testing.T) { + account := ethcommon.HexToAddress("0x1111111111111111111111111111111111111111") + orch := ethcommon.HexToAddress("0x2222222222222222222222222222222222222222") + + newServer := func() *LivepeerServer { + n, err := core.NewLivepeerNode(nil, "", nil) + require.NoError(t, err) + n.RecipientAddr = orch.Hex() + return &LivepeerServer{LivepeerNode: n} + } + + tests := []struct { + name string + handler func(*LivepeerServer, eth.LivepeerEthClient) http.Handler + }{ + {"setOrchestratorConfig", func(s *LivepeerServer, c eth.LivepeerEthClient) http.Handler { + return s.setOrchestratorConfigHandler(c) + }}, + {"activateOrchestrator", func(s *LivepeerServer, c eth.LivepeerEthClient) http.Handler { + return s.activateOrchestratorHandler(c) + }}, + {"setRewardCaller", func(s *LivepeerServer, c eth.LivepeerEthClient) http.Handler { + return s.setRewardCallerHandler(c) + }}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert := assert.New(t) + client := ð.MockClient{} + client.On("Account").Return(accounts.Account{Address: account}) + + status, body := postForm(tt.handler(newServer(), client), url.Values{}) + + assert.Equal(http.StatusBadRequest, status) + assert.Contains(body, orch.Hex()) + // No transaction of any kind may be sent from the delegated wallet. + client.AssertNotCalled(t, "SetServiceURI", mock.Anything) + client.AssertNotCalled(t, "Transcoder", mock.Anything, mock.Anything) + client.AssertNotCalled(t, "SetRewardCaller", mock.Anything) + }) + } +} + +// On a delegated wallet /reward must call rewardForTranscoder for the orchestrator rather +// than reward() for the node's own account, which is not a transcoder. +func TestRewardHandler_DelegatedWallet(t *testing.T) { + assert := assert.New(t) + account := ethcommon.HexToAddress("0x1111111111111111111111111111111111111111") + orch := ethcommon.HexToAddress("0x2222222222222222222222222222222222222222") + + n, err := core.NewLivepeerNode(nil, "", nil) + require.NoError(t, err) + n.RecipientAddr = orch.Hex() + + client := ð.MockClient{} + client.On("Account").Return(accounts.Account{Address: account}) + client.On("RewardForTranscoder", orch).Return(nil, nil) + client.On("Reward").Return(nil, nil) + client.On("CheckTx", mock.Anything).Return(nil) + + s := &LivepeerServer{LivepeerNode: n} + status, _ := post(s.rewardHandler(client)) + + assert.Equal(http.StatusOK, status) + client.AssertCalled(t, "RewardForTranscoder", orch) + client.AssertNumberOfCalls(t, "Reward", 0) +} diff --git a/server/webserver.go b/server/webserver.go index 65764b9b4e..c99a396ae8 100644 --- a/server/webserver.go +++ b/server/webserver.go @@ -80,7 +80,9 @@ func (s *LivepeerServer) cliWebServerHandlers(bindAddr string) *http.ServeMux { mux.Handle("/delegatorInfo", delegatorInfoHandler(client)) mux.Handle("/orchestratorEarningPoolsForRound", orchestratorEarningPoolsForRoundHandler(client)) mux.Handle("/registeredOrchestrators", registeredOrchestratorsHandler(client, db)) - mux.Handle("/reward", rewardHandler(client)) + mux.Handle("/reward", s.rewardHandler(client)) + mux.Handle("/setRewardCaller", mustHaveFormParams(s.setRewardCallerHandler(client))) + mux.Handle("/rewardCaller", s.rewardCallerHandler(client)) // Protocol parameters mux.Handle("/protocolParameters", protocolParametersHandler(client, db))