Commit 1dacba5a authored by Adrian Sutton's avatar Adrian Sutton Committed by GitHub

op-challenger: Support claiming bonds for multiple addresses (#9770)

Allows the challenger to be configured to claim bonds for the proposer.
parent f7920585
......@@ -472,6 +472,44 @@ func TestUnsafeAllowInvalidPrestate(t *testing.T) {
})
}
func TestAdditionalBondClaimants(t *testing.T) {
t.Run("DefaultsToEmpty", func(t *testing.T) {
cfg := configForArgs(t, addRequiredArgsExcept(config.TraceTypeAlphabet, "--additional-bond-claimants"))
require.Empty(t, cfg.AdditionalBondClaimants)
})
t.Run("Valid-Single", func(t *testing.T) {
claimant := common.Address{0xaa}
cfg := configForArgs(t, addRequiredArgs(config.TraceTypeAlphabet, "--additional-bond-claimants", claimant.Hex()))
require.Contains(t, cfg.AdditionalBondClaimants, claimant)
require.Len(t, cfg.AdditionalBondClaimants, 1)
})
t.Run("Valid-Multiple", func(t *testing.T) {
claimant1 := common.Address{0xaa}
claimant2 := common.Address{0xbb}
claimant3 := common.Address{0xcc}
cfg := configForArgs(t, addRequiredArgs(config.TraceTypeAlphabet,
"--additional-bond-claimants", fmt.Sprintf("%v,%v,%v", claimant1.Hex(), claimant2.Hex(), claimant3.Hex())))
require.Contains(t, cfg.AdditionalBondClaimants, claimant1)
require.Contains(t, cfg.AdditionalBondClaimants, claimant2)
require.Contains(t, cfg.AdditionalBondClaimants, claimant3)
require.Len(t, cfg.AdditionalBondClaimants, 3)
})
t.Run("Invalid-Single", func(t *testing.T) {
verifyArgsInvalid(t, "invalid additional claimant",
addRequiredArgs(config.TraceTypeAlphabet, "--additional-bond-claimants", "nope"))
})
t.Run("Invalid-Multiple", func(t *testing.T) {
claimant1 := common.Address{0xaa}
claimant2 := common.Address{0xbb}
verifyArgsInvalid(t, "invalid additional claimant",
addRequiredArgs(config.TraceTypeAlphabet, "--additional-bond-claimants", fmt.Sprintf("%v,nope,%v", claimant1.Hex(), claimant2.Hex())))
})
}
func verifyArgsInvalid(t *testing.T, messageContains string, cliArgs []string) {
_, _, err := dryRunWithArgs(cliArgs)
require.ErrorContains(t, err, messageContains)
......
......@@ -99,6 +99,8 @@ type Config struct {
PollInterval time.Duration // Polling interval for latest-block subscription when using an HTTP RPC provider
AllowInvalidPrestate bool // Whether to allow responding to games where the prestate does not match
AdditionalBondClaimants []common.Address // List of addresses to claim bonds for in addition to the tx manager sender
TraceTypes []TraceType // Type of traces supported
// Specific to the output cannon trace type
......
......@@ -83,6 +83,11 @@ var (
EnvVars: prefixEnvVars("HTTP_POLL_INTERVAL"),
Value: config.DefaultPollInterval,
}
AdditionalBondClaimants = &cli.StringSliceFlag{
Name: "additional-bond-claimants",
Usage: "List of addresses to claim bonds for, in addition to the configured transaction sender",
EnvVars: prefixEnvVars("ADDITIONAL_BOND_CLAIMANTS"),
}
CannonNetworkFlag = &cli.StringFlag{
Name: "cannon-network",
Usage: fmt.Sprintf(
......@@ -163,6 +168,7 @@ var optionalFlags = []cli.Flag{
MaxConcurrencyFlag,
MaxPendingTransactionsFlag,
HTTPPollInterval,
AdditionalBondClaimants,
GameAllowlistFlag,
CannonNetworkFlag,
CannonRollupConfigFlag,
......@@ -281,6 +287,16 @@ func NewConfigFromCLI(ctx *cli.Context) (*config.Config, error) {
if maxConcurrency == 0 {
return nil, fmt.Errorf("%v must not be 0", MaxConcurrencyFlag.Name)
}
var claimants []common.Address
if ctx.IsSet(AdditionalBondClaimants.Name) {
for _, addrStr := range ctx.StringSlice(AdditionalBondClaimants.Name) {
claimant, err := opservice.ParseAddress(addrStr)
if err != nil {
return nil, fmt.Errorf("invalid additional claimant: %w", err)
}
claimants = append(claimants, claimant)
}
}
return &config.Config{
// Required Flags
L1EthRpc: ctx.String(L1EthRpcFlag.Name),
......@@ -292,6 +308,7 @@ func NewConfigFromCLI(ctx *cli.Context) (*config.Config, error) {
MaxConcurrency: maxConcurrency,
MaxPendingTx: ctx.Uint64(MaxPendingTransactionsFlag.Name),
PollInterval: ctx.Duration(HTTPPollInterval.Name),
AdditionalBondClaimants: claimants,
RollupRpc: ctx.String(RollupRpcFlag.Name),
CannonNetwork: ctx.String(CannonNetworkFlag.Name),
CannonRollupConfigPath: ctx.String(CannonRollupConfigFlag.Name),
......
......@@ -28,48 +28,52 @@ type Claimer struct {
metrics BondClaimMetrics
contractCreator BondContractCreator
txSender types.TxSender
claimants []common.Address
}
var _ BondClaimer = (*Claimer)(nil)
func NewBondClaimer(l log.Logger, m BondClaimMetrics, contractCreator BondContractCreator, txSender types.TxSender) *Claimer {
func NewBondClaimer(l log.Logger, m BondClaimMetrics, contractCreator BondContractCreator, txSender types.TxSender, claimants ...common.Address) *Claimer {
return &Claimer{
logger: l,
metrics: m,
contractCreator: contractCreator,
txSender: txSender,
claimants: claimants,
}
}
func (c *Claimer) ClaimBonds(ctx context.Context, games []types.GameMetadata) (err error) {
for _, game := range games {
err = errors.Join(err, c.claimBond(ctx, game))
for _, claimant := range c.claimants {
err = errors.Join(err, c.claimBond(ctx, game, claimant))
}
}
return err
}
func (c *Claimer) claimBond(ctx context.Context, game types.GameMetadata) error {
c.logger.Debug("Attempting to claim bonds for", "game", game.Proxy)
func (c *Claimer) claimBond(ctx context.Context, game types.GameMetadata, addr common.Address) error {
c.logger.Debug("Attempting to claim bonds for", "game", game.Proxy, "addr", addr)
contract, err := c.contractCreator(game)
if err != nil {
return fmt.Errorf("failed to create bond contract bindings: %w", err)
}
credit, status, err := contract.GetCredit(ctx, c.txSender.From())
credit, status, err := contract.GetCredit(ctx, addr)
if err != nil {
return fmt.Errorf("failed to get credit: %w", err)
}
if status == types.GameStatusInProgress {
c.logger.Debug("Not claiming credit from in progress game", "game", game.Proxy, "status", status)
c.logger.Debug("Not claiming credit from in progress game", "game", game.Proxy, "addr", addr, "status", status)
return nil
}
if credit.Cmp(big.NewInt(0)) == 0 {
c.logger.Debug("No credit to claim", "game", game.Proxy)
c.logger.Debug("No credit to claim", "game", game.Proxy, "addr", addr)
return nil
}
candidate, err := contract.ClaimCredit(c.txSender.From())
candidate, err := contract.ClaimCredit(addr)
if err != nil {
return fmt.Errorf("failed to create credit claim tx: %w", err)
}
......
......@@ -22,8 +22,8 @@ var (
func TestClaimer_ClaimBonds(t *testing.T) {
t.Run("MultipleBondClaimsSucceed", func(t *testing.T) {
gameAddr := common.HexToAddress("0x1234")
c, m, contract, txSender := newTestClaimer(t, gameAddr)
contract.credit = 1
c, m, contract, txSender := newTestClaimer(t)
contract.credit[txSender.From()] = 1
err := c.ClaimBonds(context.Background(), []types.GameMetadata{{Proxy: gameAddr}, {Proxy: gameAddr}, {Proxy: gameAddr}})
require.NoError(t, err)
require.Equal(t, 3, txSender.sends)
......@@ -32,18 +32,33 @@ func TestClaimer_ClaimBonds(t *testing.T) {
t.Run("BondClaimSucceeds", func(t *testing.T) {
gameAddr := common.HexToAddress("0x1234")
c, m, contract, txSender := newTestClaimer(t, gameAddr)
contract.credit = 1
c, m, contract, txSender := newTestClaimer(t)
contract.credit[txSender.From()] = 1
err := c.ClaimBonds(context.Background(), []types.GameMetadata{{Proxy: gameAddr}})
require.NoError(t, err)
require.Equal(t, 1, txSender.sends)
require.Equal(t, 1, m.RecordBondClaimedCalls)
})
t.Run("BondClaimSucceedsForMultipleAddresses", func(t *testing.T) {
claimant1 := common.Address{0xaa}
claimant2 := common.Address{0xbb}
claimant3 := common.Address{0xcc}
gameAddr := common.HexToAddress("0x1234")
c, m, contract, txSender := newTestClaimer(t, claimant1, claimant2, claimant3)
contract.credit[claimant1] = 1
contract.credit[claimant2] = 2
contract.credit[claimant3] = 0
err := c.ClaimBonds(context.Background(), []types.GameMetadata{{Proxy: gameAddr}})
require.NoError(t, err)
require.Equal(t, 2, txSender.sends)
require.Equal(t, 2, m.RecordBondClaimedCalls)
})
t.Run("BondClaimSkippedForInProgressGame", func(t *testing.T) {
gameAddr := common.HexToAddress("0x1234")
c, m, contract, txSender := newTestClaimer(t, gameAddr)
contract.credit = 1
c, m, contract, txSender := newTestClaimer(t)
contract.credit[txSender.From()] = 1
contract.status = types.GameStatusInProgress
err := c.ClaimBonds(context.Background(), []types.GameMetadata{{Proxy: gameAddr}})
require.NoError(t, err)
......@@ -53,9 +68,9 @@ func TestClaimer_ClaimBonds(t *testing.T) {
t.Run("BondClaimFails", func(t *testing.T) {
gameAddr := common.HexToAddress("0x1234")
c, m, contract, txSender := newTestClaimer(t, gameAddr)
c, m, contract, txSender := newTestClaimer(t)
txSender.sendFails = true
contract.credit = 1
contract.credit[txSender.From()] = 1
err := c.ClaimBonds(context.Background(), []types.GameMetadata{{Proxy: gameAddr}})
require.ErrorIs(t, err, mockTxMgrSendError)
require.Equal(t, 1, txSender.sends)
......@@ -64,8 +79,8 @@ func TestClaimer_ClaimBonds(t *testing.T) {
t.Run("ZeroCreditReturnsNil", func(t *testing.T) {
gameAddr := common.HexToAddress("0x1234")
c, m, contract, txSender := newTestClaimer(t, gameAddr)
contract.credit = 0
c, m, contract, txSender := newTestClaimer(t)
contract.credit[txSender.From()] = 0
err := c.ClaimBonds(context.Background(), []types.GameMetadata{{Proxy: gameAddr}})
require.NoError(t, err)
require.Equal(t, 0, txSender.sends)
......@@ -74,8 +89,8 @@ func TestClaimer_ClaimBonds(t *testing.T) {
t.Run("MultipleBondClaimFails", func(t *testing.T) {
gameAddr := common.HexToAddress("0x1234")
c, m, contract, txSender := newTestClaimer(t, gameAddr)
contract.credit = 1
c, m, contract, txSender := newTestClaimer(t)
contract.credit[txSender.From()] = 1
txSender.sendFails = true
err := c.ClaimBonds(context.Background(), []types.GameMetadata{{Proxy: gameAddr}, {Proxy: gameAddr}, {Proxy: gameAddr}})
require.ErrorIs(t, err, mockTxMgrSendError)
......@@ -84,15 +99,18 @@ func TestClaimer_ClaimBonds(t *testing.T) {
})
}
func newTestClaimer(t *testing.T, gameAddr common.Address) (*Claimer, *mockClaimMetrics, *stubBondContract, *mockTxSender) {
func newTestClaimer(t *testing.T, claimants ...common.Address) (*Claimer, *mockClaimMetrics, *stubBondContract, *mockTxSender) {
logger := testlog.Logger(t, log.LvlDebug)
m := &mockClaimMetrics{}
txSender := &mockTxSender{}
bondContract := &stubBondContract{status: types.GameStatusChallengerWon}
bondContract := &stubBondContract{status: types.GameStatusChallengerWon, credit: make(map[common.Address]int64)}
contractCreator := func(game types.GameMetadata) (BondContract, error) {
return bondContract, nil
}
c := NewBondClaimer(logger, m, contractCreator, txSender)
if len(claimants) == 0 {
claimants = []common.Address{txSender.From()}
}
c := NewBondClaimer(logger, m, contractCreator, txSender, claimants...)
return c, m, bondContract, txSender
}
......@@ -126,12 +144,12 @@ func (s *mockTxSender) SendAndWait(_ string, _ ...txmgr.TxCandidate) ([]*ethtype
}
type stubBondContract struct {
credit int64
credit map[common.Address]int64
status types.GameStatus
}
func (s *stubBondContract) GetCredit(_ context.Context, _ common.Address) (*big.Int, types.GameStatus, error) {
return big.NewInt(s.credit), s.status, nil
func (s *stubBondContract) GetCredit(_ context.Context, addr common.Address) (*big.Int, types.GameStatus, error) {
return big.NewInt(s.credit[addr]), s.status, nil
}
func (s *stubBondContract) ClaimCredit(_ common.Address) (txmgr.TxCandidate, error) {
......
......@@ -11,6 +11,7 @@ import (
"github.com/ethereum-optimism/optimism/op-challenger/game/keccak/fetcher"
"github.com/ethereum-optimism/optimism/op-challenger/sender"
"github.com/ethereum-optimism/optimism/op-service/sources"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/log"
......@@ -105,7 +106,7 @@ func (s *Service) initFromConfig(ctx context.Context, cfg *config.Config) error
if err := s.registerGameTypes(ctx, cfg); err != nil {
return fmt.Errorf("failed to register game types: %w", err)
}
if err := s.initBondClaims(); err != nil {
if err := s.initBondClaims(cfg); err != nil {
return fmt.Errorf("failed to init bond claiming: %w", err)
}
if err := s.initScheduler(cfg); err != nil {
......@@ -196,8 +197,10 @@ func (s *Service) initFactoryContract(cfg *config.Config) error {
return nil
}
func (s *Service) initBondClaims() error {
claimer := claims.NewBondClaimer(s.logger, s.metrics, s.registry.CreateBondContract, s.txSender)
func (s *Service) initBondClaims(cfg *config.Config) error {
claimants := []common.Address{s.txSender.From()}
claimants = append(claimants, cfg.AdditionalBondClaimants...)
claimer := claims.NewBondClaimer(s.logger, s.metrics, s.registry.CreateBondContract, s.txSender, claimants...)
s.claimer = claims.NewBondClaimScheduler(s.logger, s.metrics, claimer)
return nil
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment