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) { ...@@ -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) { func verifyArgsInvalid(t *testing.T, messageContains string, cliArgs []string) {
_, _, err := dryRunWithArgs(cliArgs) _, _, err := dryRunWithArgs(cliArgs)
require.ErrorContains(t, err, messageContains) require.ErrorContains(t, err, messageContains)
......
...@@ -99,6 +99,8 @@ type Config struct { ...@@ -99,6 +99,8 @@ type Config struct {
PollInterval time.Duration // Polling interval for latest-block subscription when using an HTTP RPC provider 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 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 TraceTypes []TraceType // Type of traces supported
// Specific to the output cannon trace type // Specific to the output cannon trace type
......
...@@ -83,6 +83,11 @@ var ( ...@@ -83,6 +83,11 @@ var (
EnvVars: prefixEnvVars("HTTP_POLL_INTERVAL"), EnvVars: prefixEnvVars("HTTP_POLL_INTERVAL"),
Value: config.DefaultPollInterval, 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{ CannonNetworkFlag = &cli.StringFlag{
Name: "cannon-network", Name: "cannon-network",
Usage: fmt.Sprintf( Usage: fmt.Sprintf(
...@@ -163,6 +168,7 @@ var optionalFlags = []cli.Flag{ ...@@ -163,6 +168,7 @@ var optionalFlags = []cli.Flag{
MaxConcurrencyFlag, MaxConcurrencyFlag,
MaxPendingTransactionsFlag, MaxPendingTransactionsFlag,
HTTPPollInterval, HTTPPollInterval,
AdditionalBondClaimants,
GameAllowlistFlag, GameAllowlistFlag,
CannonNetworkFlag, CannonNetworkFlag,
CannonRollupConfigFlag, CannonRollupConfigFlag,
...@@ -281,31 +287,42 @@ func NewConfigFromCLI(ctx *cli.Context) (*config.Config, error) { ...@@ -281,31 +287,42 @@ func NewConfigFromCLI(ctx *cli.Context) (*config.Config, error) {
if maxConcurrency == 0 { if maxConcurrency == 0 {
return nil, fmt.Errorf("%v must not be 0", MaxConcurrencyFlag.Name) 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{ return &config.Config{
// Required Flags // Required Flags
L1EthRpc: ctx.String(L1EthRpcFlag.Name), L1EthRpc: ctx.String(L1EthRpcFlag.Name),
L1Beacon: ctx.String(L1BeaconFlag.Name), L1Beacon: ctx.String(L1BeaconFlag.Name),
TraceTypes: traceTypes, TraceTypes: traceTypes,
GameFactoryAddress: gameFactoryAddress, GameFactoryAddress: gameFactoryAddress,
GameAllowlist: allowedGames, GameAllowlist: allowedGames,
GameWindow: ctx.Duration(GameWindowFlag.Name), GameWindow: ctx.Duration(GameWindowFlag.Name),
MaxConcurrency: maxConcurrency, MaxConcurrency: maxConcurrency,
MaxPendingTx: ctx.Uint64(MaxPendingTransactionsFlag.Name), MaxPendingTx: ctx.Uint64(MaxPendingTransactionsFlag.Name),
PollInterval: ctx.Duration(HTTPPollInterval.Name), PollInterval: ctx.Duration(HTTPPollInterval.Name),
RollupRpc: ctx.String(RollupRpcFlag.Name), AdditionalBondClaimants: claimants,
CannonNetwork: ctx.String(CannonNetworkFlag.Name), RollupRpc: ctx.String(RollupRpcFlag.Name),
CannonRollupConfigPath: ctx.String(CannonRollupConfigFlag.Name), CannonNetwork: ctx.String(CannonNetworkFlag.Name),
CannonL2GenesisPath: ctx.String(CannonL2GenesisFlag.Name), CannonRollupConfigPath: ctx.String(CannonRollupConfigFlag.Name),
CannonBin: ctx.String(CannonBinFlag.Name), CannonL2GenesisPath: ctx.String(CannonL2GenesisFlag.Name),
CannonServer: ctx.String(CannonServerFlag.Name), CannonBin: ctx.String(CannonBinFlag.Name),
CannonAbsolutePreState: ctx.String(CannonPreStateFlag.Name), CannonServer: ctx.String(CannonServerFlag.Name),
Datadir: ctx.String(DatadirFlag.Name), CannonAbsolutePreState: ctx.String(CannonPreStateFlag.Name),
CannonL2: ctx.String(CannonL2Flag.Name), Datadir: ctx.String(DatadirFlag.Name),
CannonSnapshotFreq: ctx.Uint(CannonSnapshotFreqFlag.Name), CannonL2: ctx.String(CannonL2Flag.Name),
CannonInfoFreq: ctx.Uint(CannonInfoFreqFlag.Name), CannonSnapshotFreq: ctx.Uint(CannonSnapshotFreqFlag.Name),
TxMgrConfig: txMgrConfig, CannonInfoFreq: ctx.Uint(CannonInfoFreqFlag.Name),
MetricsConfig: metricsConfig, TxMgrConfig: txMgrConfig,
PprofConfig: pprofConfig, MetricsConfig: metricsConfig,
AllowInvalidPrestate: ctx.Bool(UnsafeAllowInvalidPrestate.Name), PprofConfig: pprofConfig,
AllowInvalidPrestate: ctx.Bool(UnsafeAllowInvalidPrestate.Name),
}, nil }, nil
} }
...@@ -28,48 +28,52 @@ type Claimer struct { ...@@ -28,48 +28,52 @@ type Claimer struct {
metrics BondClaimMetrics metrics BondClaimMetrics
contractCreator BondContractCreator contractCreator BondContractCreator
txSender types.TxSender txSender types.TxSender
claimants []common.Address
} }
var _ BondClaimer = (*Claimer)(nil) 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{ return &Claimer{
logger: l, logger: l,
metrics: m, metrics: m,
contractCreator: contractCreator, contractCreator: contractCreator,
txSender: txSender, txSender: txSender,
claimants: claimants,
} }
} }
func (c *Claimer) ClaimBonds(ctx context.Context, games []types.GameMetadata) (err error) { func (c *Claimer) ClaimBonds(ctx context.Context, games []types.GameMetadata) (err error) {
for _, game := range games { 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 return err
} }
func (c *Claimer) claimBond(ctx context.Context, game types.GameMetadata) error { 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) c.logger.Debug("Attempting to claim bonds for", "game", game.Proxy, "addr", addr)
contract, err := c.contractCreator(game) contract, err := c.contractCreator(game)
if err != nil { if err != nil {
return fmt.Errorf("failed to create bond contract bindings: %w", err) 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 { if err != nil {
return fmt.Errorf("failed to get credit: %w", err) return fmt.Errorf("failed to get credit: %w", err)
} }
if status == types.GameStatusInProgress { 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 return nil
} }
if credit.Cmp(big.NewInt(0)) == 0 { 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 return nil
} }
candidate, err := contract.ClaimCredit(c.txSender.From()) candidate, err := contract.ClaimCredit(addr)
if err != nil { if err != nil {
return fmt.Errorf("failed to create credit claim tx: %w", err) return fmt.Errorf("failed to create credit claim tx: %w", err)
} }
......
...@@ -22,8 +22,8 @@ var ( ...@@ -22,8 +22,8 @@ var (
func TestClaimer_ClaimBonds(t *testing.T) { func TestClaimer_ClaimBonds(t *testing.T) {
t.Run("MultipleBondClaimsSucceed", func(t *testing.T) { t.Run("MultipleBondClaimsSucceed", func(t *testing.T) {
gameAddr := common.HexToAddress("0x1234") gameAddr := common.HexToAddress("0x1234")
c, m, contract, txSender := newTestClaimer(t, gameAddr) c, m, contract, txSender := newTestClaimer(t)
contract.credit = 1 contract.credit[txSender.From()] = 1
err := c.ClaimBonds(context.Background(), []types.GameMetadata{{Proxy: gameAddr}, {Proxy: gameAddr}, {Proxy: gameAddr}}) err := c.ClaimBonds(context.Background(), []types.GameMetadata{{Proxy: gameAddr}, {Proxy: gameAddr}, {Proxy: gameAddr}})
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, 3, txSender.sends) require.Equal(t, 3, txSender.sends)
...@@ -32,18 +32,33 @@ func TestClaimer_ClaimBonds(t *testing.T) { ...@@ -32,18 +32,33 @@ func TestClaimer_ClaimBonds(t *testing.T) {
t.Run("BondClaimSucceeds", func(t *testing.T) { t.Run("BondClaimSucceeds", func(t *testing.T) {
gameAddr := common.HexToAddress("0x1234") gameAddr := common.HexToAddress("0x1234")
c, m, contract, txSender := newTestClaimer(t, gameAddr) c, m, contract, txSender := newTestClaimer(t)
contract.credit = 1 contract.credit[txSender.From()] = 1
err := c.ClaimBonds(context.Background(), []types.GameMetadata{{Proxy: gameAddr}}) err := c.ClaimBonds(context.Background(), []types.GameMetadata{{Proxy: gameAddr}})
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, 1, txSender.sends) require.Equal(t, 1, txSender.sends)
require.Equal(t, 1, m.RecordBondClaimedCalls) 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) { t.Run("BondClaimSkippedForInProgressGame", func(t *testing.T) {
gameAddr := common.HexToAddress("0x1234") gameAddr := common.HexToAddress("0x1234")
c, m, contract, txSender := newTestClaimer(t, gameAddr) c, m, contract, txSender := newTestClaimer(t)
contract.credit = 1 contract.credit[txSender.From()] = 1
contract.status = types.GameStatusInProgress contract.status = types.GameStatusInProgress
err := c.ClaimBonds(context.Background(), []types.GameMetadata{{Proxy: gameAddr}}) err := c.ClaimBonds(context.Background(), []types.GameMetadata{{Proxy: gameAddr}})
require.NoError(t, err) require.NoError(t, err)
...@@ -53,9 +68,9 @@ func TestClaimer_ClaimBonds(t *testing.T) { ...@@ -53,9 +68,9 @@ func TestClaimer_ClaimBonds(t *testing.T) {
t.Run("BondClaimFails", func(t *testing.T) { t.Run("BondClaimFails", func(t *testing.T) {
gameAddr := common.HexToAddress("0x1234") gameAddr := common.HexToAddress("0x1234")
c, m, contract, txSender := newTestClaimer(t, gameAddr) c, m, contract, txSender := newTestClaimer(t)
txSender.sendFails = true txSender.sendFails = true
contract.credit = 1 contract.credit[txSender.From()] = 1
err := c.ClaimBonds(context.Background(), []types.GameMetadata{{Proxy: gameAddr}}) err := c.ClaimBonds(context.Background(), []types.GameMetadata{{Proxy: gameAddr}})
require.ErrorIs(t, err, mockTxMgrSendError) require.ErrorIs(t, err, mockTxMgrSendError)
require.Equal(t, 1, txSender.sends) require.Equal(t, 1, txSender.sends)
...@@ -64,8 +79,8 @@ func TestClaimer_ClaimBonds(t *testing.T) { ...@@ -64,8 +79,8 @@ func TestClaimer_ClaimBonds(t *testing.T) {
t.Run("ZeroCreditReturnsNil", func(t *testing.T) { t.Run("ZeroCreditReturnsNil", func(t *testing.T) {
gameAddr := common.HexToAddress("0x1234") gameAddr := common.HexToAddress("0x1234")
c, m, contract, txSender := newTestClaimer(t, gameAddr) c, m, contract, txSender := newTestClaimer(t)
contract.credit = 0 contract.credit[txSender.From()] = 0
err := c.ClaimBonds(context.Background(), []types.GameMetadata{{Proxy: gameAddr}}) err := c.ClaimBonds(context.Background(), []types.GameMetadata{{Proxy: gameAddr}})
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, 0, txSender.sends) require.Equal(t, 0, txSender.sends)
...@@ -74,8 +89,8 @@ func TestClaimer_ClaimBonds(t *testing.T) { ...@@ -74,8 +89,8 @@ func TestClaimer_ClaimBonds(t *testing.T) {
t.Run("MultipleBondClaimFails", func(t *testing.T) { t.Run("MultipleBondClaimFails", func(t *testing.T) {
gameAddr := common.HexToAddress("0x1234") gameAddr := common.HexToAddress("0x1234")
c, m, contract, txSender := newTestClaimer(t, gameAddr) c, m, contract, txSender := newTestClaimer(t)
contract.credit = 1 contract.credit[txSender.From()] = 1
txSender.sendFails = true txSender.sendFails = true
err := c.ClaimBonds(context.Background(), []types.GameMetadata{{Proxy: gameAddr}, {Proxy: gameAddr}, {Proxy: gameAddr}}) err := c.ClaimBonds(context.Background(), []types.GameMetadata{{Proxy: gameAddr}, {Proxy: gameAddr}, {Proxy: gameAddr}})
require.ErrorIs(t, err, mockTxMgrSendError) require.ErrorIs(t, err, mockTxMgrSendError)
...@@ -84,15 +99,18 @@ func TestClaimer_ClaimBonds(t *testing.T) { ...@@ -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) logger := testlog.Logger(t, log.LvlDebug)
m := &mockClaimMetrics{} m := &mockClaimMetrics{}
txSender := &mockTxSender{} 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) { contractCreator := func(game types.GameMetadata) (BondContract, error) {
return bondContract, nil 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 return c, m, bondContract, txSender
} }
...@@ -126,12 +144,12 @@ func (s *mockTxSender) SendAndWait(_ string, _ ...txmgr.TxCandidate) ([]*ethtype ...@@ -126,12 +144,12 @@ func (s *mockTxSender) SendAndWait(_ string, _ ...txmgr.TxCandidate) ([]*ethtype
} }
type stubBondContract struct { type stubBondContract struct {
credit int64 credit map[common.Address]int64
status types.GameStatus status types.GameStatus
} }
func (s *stubBondContract) GetCredit(_ context.Context, _ common.Address) (*big.Int, types.GameStatus, error) { func (s *stubBondContract) GetCredit(_ context.Context, addr common.Address) (*big.Int, types.GameStatus, error) {
return big.NewInt(s.credit), s.status, nil return big.NewInt(s.credit[addr]), s.status, nil
} }
func (s *stubBondContract) ClaimCredit(_ common.Address) (txmgr.TxCandidate, error) { func (s *stubBondContract) ClaimCredit(_ common.Address) (txmgr.TxCandidate, error) {
......
...@@ -11,6 +11,7 @@ import ( ...@@ -11,6 +11,7 @@ import (
"github.com/ethereum-optimism/optimism/op-challenger/game/keccak/fetcher" "github.com/ethereum-optimism/optimism/op-challenger/game/keccak/fetcher"
"github.com/ethereum-optimism/optimism/op-challenger/sender" "github.com/ethereum-optimism/optimism/op-challenger/sender"
"github.com/ethereum-optimism/optimism/op-service/sources" "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/ethclient"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
...@@ -105,7 +106,7 @@ func (s *Service) initFromConfig(ctx context.Context, cfg *config.Config) error ...@@ -105,7 +106,7 @@ func (s *Service) initFromConfig(ctx context.Context, cfg *config.Config) error
if err := s.registerGameTypes(ctx, cfg); err != nil { if err := s.registerGameTypes(ctx, cfg); err != nil {
return fmt.Errorf("failed to register game types: %w", err) 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) return fmt.Errorf("failed to init bond claiming: %w", err)
} }
if err := s.initScheduler(cfg); err != nil { if err := s.initScheduler(cfg); err != nil {
...@@ -196,8 +197,10 @@ func (s *Service) initFactoryContract(cfg *config.Config) error { ...@@ -196,8 +197,10 @@ func (s *Service) initFactoryContract(cfg *config.Config) error {
return nil return nil
} }
func (s *Service) initBondClaims() error { func (s *Service) initBondClaims(cfg *config.Config) error {
claimer := claims.NewBondClaimer(s.logger, s.metrics, s.registry.CreateBondContract, s.txSender) 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) s.claimer = claims.NewBondClaimScheduler(s.logger, s.metrics, claimer)
return nil 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