Commit 005be54b authored by refcell's avatar refcell Committed by GitHub

feat(op-challenger): Selective Claim Resolution (#9780)

* feat(op-challenger): selective claim resolution

* fix(op-challenger): Add selective claim resolution tests
parent 00950152
......@@ -101,6 +101,8 @@ type Config struct {
AdditionalBondClaimants []common.Address // List of addresses to claim bonds for in addition to the tx manager sender
SelectiveClaimResolution bool // Whether to only resolve claims for the claimants in AdditionalBondClaimants union [TxSender.From()]
TraceTypes []TraceType // Type of traces supported
// Specific to the output cannon trace type
......
......@@ -76,6 +76,12 @@ func TestGameFactoryAddressRequired(t *testing.T) {
require.ErrorIs(t, config.Check(), ErrMissingGameFactoryAddress)
}
func TestSelectiveClaimResolutionNotRequired(t *testing.T) {
config := validConfig(TraceTypeCannon)
require.Equal(t, false, config.SelectiveClaimResolution)
require.NoError(t, config.Check())
}
func TestGameAllowlistNotRequired(t *testing.T) {
config := validConfig(TraceTypeCannon)
config.GameAllowlist = []common.Address{}
......
......@@ -145,6 +145,11 @@ var (
EnvVars: prefixEnvVars("GAME_WINDOW"),
Value: config.DefaultGameWindow,
}
SelectiveClaimResolutionFlag = &cli.BoolFlag{
Name: "selective-claim-resolution",
Usage: "Only resolve claims for the configured claimants",
EnvVars: prefixEnvVars("SELECTIVE_CLAIM_RESOLUTION"),
}
UnsafeAllowInvalidPrestate = &cli.BoolFlag{
Name: "unsafe-allow-invalid-prestate",
Usage: "Allow responding to games where the absolute prestate is configured incorrectly. THIS IS UNSAFE!",
......@@ -180,6 +185,7 @@ var optionalFlags = []cli.Flag{
CannonSnapshotFreqFlag,
CannonInfoFreqFlag,
GameWindowFlag,
SelectiveClaimResolutionFlag,
UnsafeAllowInvalidPrestate,
}
......@@ -299,30 +305,31 @@ func NewConfigFromCLI(ctx *cli.Context) (*config.Config, error) {
}
return &config.Config{
// Required Flags
L1EthRpc: ctx.String(L1EthRpcFlag.Name),
L1Beacon: ctx.String(L1BeaconFlag.Name),
TraceTypes: traceTypes,
GameFactoryAddress: gameFactoryAddress,
GameAllowlist: allowedGames,
GameWindow: ctx.Duration(GameWindowFlag.Name),
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),
CannonL2GenesisPath: ctx.String(CannonL2GenesisFlag.Name),
CannonBin: ctx.String(CannonBinFlag.Name),
CannonServer: ctx.String(CannonServerFlag.Name),
CannonAbsolutePreState: ctx.String(CannonPreStateFlag.Name),
Datadir: ctx.String(DatadirFlag.Name),
CannonL2: ctx.String(CannonL2Flag.Name),
CannonSnapshotFreq: ctx.Uint(CannonSnapshotFreqFlag.Name),
CannonInfoFreq: ctx.Uint(CannonInfoFreqFlag.Name),
TxMgrConfig: txMgrConfig,
MetricsConfig: metricsConfig,
PprofConfig: pprofConfig,
AllowInvalidPrestate: ctx.Bool(UnsafeAllowInvalidPrestate.Name),
L1EthRpc: ctx.String(L1EthRpcFlag.Name),
L1Beacon: ctx.String(L1BeaconFlag.Name),
TraceTypes: traceTypes,
GameFactoryAddress: gameFactoryAddress,
GameAllowlist: allowedGames,
GameWindow: ctx.Duration(GameWindowFlag.Name),
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),
CannonL2GenesisPath: ctx.String(CannonL2GenesisFlag.Name),
CannonBin: ctx.String(CannonBinFlag.Name),
CannonServer: ctx.String(CannonServerFlag.Name),
CannonAbsolutePreState: ctx.String(CannonPreStateFlag.Name),
Datadir: ctx.String(DatadirFlag.Name),
CannonL2: ctx.String(CannonL2Flag.Name),
CannonSnapshotFreq: ctx.Uint(CannonSnapshotFreqFlag.Name),
CannonInfoFreq: ctx.Uint(CannonInfoFreqFlag.Name),
TxMgrConfig: txMgrConfig,
MetricsConfig: metricsConfig,
PprofConfig: pprofConfig,
SelectiveClaimResolution: ctx.Bool(SelectiveClaimResolutionFlag.Name),
AllowInvalidPrestate: ctx.Bool(UnsafeAllowInvalidPrestate.Name),
}, nil
}
......@@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"slices"
"sync"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/solver"
......@@ -33,16 +34,29 @@ type Agent struct {
solver *solver.GameSolver
loader ClaimLoader
responder Responder
selective bool
claimants []common.Address
maxDepth types.Depth
log log.Logger
}
func NewAgent(m metrics.Metricer, loader ClaimLoader, maxDepth types.Depth, trace types.TraceAccessor, responder Responder, log log.Logger) *Agent {
func NewAgent(
m metrics.Metricer,
loader ClaimLoader,
maxDepth types.Depth,
trace types.TraceAccessor,
responder Responder,
log log.Logger,
selective bool,
claimants []common.Address,
) *Agent {
return &Agent{
metrics: m,
solver: solver.NewGameSolver(maxDepth, trace),
loader: loader,
responder: responder,
selective: selective,
claimants: claimants,
maxDepth: maxDepth,
log: log,
}
......@@ -129,6 +143,15 @@ func (a *Agent) tryResolveClaims(ctx context.Context) error {
var resolvableClaims []int64
for _, claim := range claims {
if a.selective {
a.log.Trace("Selective claim resolution, checking if claim is incentivized", "claimIdx", claim.ContractIndex)
isUncounteredClaim := slices.Contains(a.claimants, claim.Claimant) && claim.CounteredBy == common.Address{}
ourCounter := slices.Contains(a.claimants, claim.CounteredBy)
if !isUncounteredClaim && !ourCounter {
a.log.Debug("Skipping claim to check resolution", "claimIdx", claim.ContractIndex)
continue
}
}
a.log.Trace("Checking if claim is resolvable", "claimIdx", claim.ContractIndex)
if err := a.responder.CallResolveClaim(ctx, uint64(claim.ContractIndex)); err == nil {
a.log.Info("Resolving claim", "claimIdx", claim.ContractIndex)
......
......@@ -9,6 +9,7 @@ import (
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace"
"github.com/stretchr/testify/require"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/test"
......@@ -52,6 +53,80 @@ func TestDoNotMakeMovesWhenGameIsResolvable(t *testing.T) {
}
}
func createClaimsWithClaimants(t *testing.T, d types.Depth) []types.Claim {
claimBuilder := test.NewClaimBuilder(t, d, alphabet.NewTraceProvider(big.NewInt(0), d))
rootClaim := claimBuilder.CreateRootClaim()
claim1 := rootClaim
claim1.Claimant = common.BigToAddress(big.NewInt(1))
claim2 := claimBuilder.AttackClaim(claim1)
claim2.Claimant = common.BigToAddress(big.NewInt(2))
claim3 := claimBuilder.AttackClaim(claim2)
claim3.Claimant = common.BigToAddress(big.NewInt(3))
return []types.Claim{claim1, claim2, claim3}
}
func TestAgent_SelectiveClaimResolution(t *testing.T) {
ctx := context.Background()
tests := []struct {
name string
callResolveStatus gameTypes.GameStatus
selective bool
claimants []common.Address
claims []types.Claim
expectedResolveCount int
}{
{
name: "NonSelectiveEmptyClaimants",
callResolveStatus: gameTypes.GameStatusDefenderWon,
selective: false,
claimants: []common.Address{},
claims: createClaimsWithClaimants(t, types.Depth(4)),
expectedResolveCount: 3,
},
{
name: "NonSelectiveWithClaimants",
callResolveStatus: gameTypes.GameStatusDefenderWon,
selective: false,
claimants: []common.Address{common.BigToAddress(big.NewInt(1))},
claims: createClaimsWithClaimants(t, types.Depth(4)),
expectedResolveCount: 3,
},
{
name: "SelectiveEmptyClaimants",
callResolveStatus: gameTypes.GameStatusDefenderWon,
selective: true,
claimants: []common.Address{},
claims: createClaimsWithClaimants(t, types.Depth(4)),
},
{
name: "SelectiveWithClaimants",
callResolveStatus: gameTypes.GameStatusDefenderWon,
selective: true,
claimants: []common.Address{common.BigToAddress(big.NewInt(1))},
claims: createClaimsWithClaimants(t, types.Depth(4)),
expectedResolveCount: 1,
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
agent, claimLoader, responder := setupTestAgent(t)
agent.selective = test.selective
agent.claimants = test.claimants
claimLoader.maxLoads = 1
claimLoader.claims = test.claims
responder.callResolveStatus = test.callResolveStatus
require.NoError(t, agent.Act(ctx))
require.Equal(t, test.expectedResolveCount, responder.callResolveClaimCount, "should check if game is resolvable")
require.Equal(t, test.expectedResolveCount, responder.resolveClaimCount, "should check if game is resolvable")
})
}
}
func TestLoadClaimsWhenGameNotResolvable(t *testing.T) {
// Checks that if the game isn't resolvable, that the agent continues on to start checking claims
agent, claimLoader, responder := setupTestAgent(t)
......@@ -77,17 +152,21 @@ func setupTestAgent(t *testing.T) (*Agent, *stubClaimLoader, *stubResponder) {
depth := types.Depth(4)
provider := alphabet.NewTraceProvider(big.NewInt(0), depth)
responder := &stubResponder{}
agent := NewAgent(metrics.NoopMetrics, claimLoader, depth, trace.NewSimpleTraceAccessor(provider), responder, logger)
agent := NewAgent(metrics.NoopMetrics, claimLoader, depth, trace.NewSimpleTraceAccessor(provider), responder, logger, false, []common.Address{})
return agent, claimLoader, responder
}
type stubClaimLoader struct {
callCount int
maxLoads int
claims []types.Claim
}
func (s *stubClaimLoader) GetAllClaims(ctx context.Context) ([]types.Claim, error) {
s.callCount++
if s.callCount > s.maxLoads && s.maxLoads != 0 {
return []types.Claim{}, nil
}
return s.claims, nil
}
......
......@@ -70,6 +70,8 @@ func NewGamePlayer(
validators []Validator,
creator resourceCreator,
l1HeaderSource L1HeaderSource,
selective bool,
claimants []common.Address,
) (*GamePlayer, error) {
logger = logger.New("game", addr)
......@@ -129,7 +131,7 @@ func NewGamePlayer(
return nil, fmt.Errorf("failed to create the responder: %w", err)
}
agent := NewAgent(m, loader, gameDepth, accessor, responder, logger)
agent := NewAgent(m, loader, gameDepth, accessor, responder, logger, selective, claimants)
return &GamePlayer{
act: agent.Act,
loader: loader,
......
......@@ -17,6 +17,7 @@ import (
"github.com/ethereum-optimism/optimism/op-challenger/metrics"
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum-optimism/optimism/op-service/sources/batching"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/log"
)
......@@ -45,6 +46,8 @@ func RegisterGameTypes(
gameFactory *contracts.DisputeGameFactoryContract,
caller *batching.MultiCaller,
l1HeaderSource L1HeaderSource,
selective bool,
claimants []common.Address,
) (CloseFunc, error) {
var closer CloseFunc
var l2Client *ethclient.Client
......@@ -59,17 +62,17 @@ func RegisterGameTypes(
syncValidator := newSyncStatusValidator(rollupClient)
if cfg.TraceTypeEnabled(config.TraceTypeCannon) {
if err := registerCannon(faultTypes.CannonGameType, registry, ctx, cl, logger, m, cfg, syncValidator, rollupClient, txSender, gameFactory, caller, l2Client, l1HeaderSource); err != nil {
if err := registerCannon(faultTypes.CannonGameType, registry, ctx, cl, logger, m, cfg, syncValidator, rollupClient, txSender, gameFactory, caller, l2Client, l1HeaderSource, selective, claimants); err != nil {
return nil, fmt.Errorf("failed to register cannon game type: %w", err)
}
}
if cfg.TraceTypeEnabled(config.TraceTypePermissioned) {
if err := registerCannon(faultTypes.PermissionedGameType, registry, ctx, cl, logger, m, cfg, syncValidator, rollupClient, txSender, gameFactory, caller, l2Client, l1HeaderSource); err != nil {
if err := registerCannon(faultTypes.PermissionedGameType, registry, ctx, cl, logger, m, cfg, syncValidator, rollupClient, txSender, gameFactory, caller, l2Client, l1HeaderSource, selective, claimants); err != nil {
return nil, fmt.Errorf("failed to register permissioned cannon game type: %w", err)
}
}
if cfg.TraceTypeEnabled(config.TraceTypeAlphabet) {
if err := registerAlphabet(registry, ctx, cl, logger, m, syncValidator, rollupClient, txSender, gameFactory, caller, l1HeaderSource); err != nil {
if err := registerAlphabet(registry, ctx, cl, logger, m, syncValidator, rollupClient, txSender, gameFactory, caller, l1HeaderSource, selective, claimants); err != nil {
return nil, fmt.Errorf("failed to register alphabet game type: %w", err)
}
}
......@@ -88,6 +91,8 @@ func registerAlphabet(
gameFactory *contracts.DisputeGameFactoryContract,
caller *batching.MultiCaller,
l1HeaderSource L1HeaderSource,
selective bool,
claimants []common.Address,
) error {
playerCreator := func(game types.GameMetadata, dir string) (scheduler.GamePlayer, error) {
contract, err := contracts.NewFaultDisputeGameContract(game.Proxy, caller)
......@@ -116,7 +121,7 @@ func registerAlphabet(
}
prestateValidator := NewPrestateValidator("alphabet", contract.GetAbsolutePrestateHash, alphabet.PrestateProvider)
genesisValidator := NewPrestateValidator("output root", contract.GetGenesisOutputRoot, prestateProvider)
return NewGamePlayer(ctx, cl, logger, m, dir, game.Proxy, txSender, contract, syncValidator, []Validator{prestateValidator, genesisValidator}, creator, l1HeaderSource)
return NewGamePlayer(ctx, cl, logger, m, dir, game.Proxy, txSender, contract, syncValidator, []Validator{prestateValidator, genesisValidator}, creator, l1HeaderSource, selective, claimants)
}
oracle, err := createOracle(ctx, gameFactory, caller, faultTypes.AlphabetGameType)
if err != nil {
......@@ -162,6 +167,8 @@ func registerCannon(
caller *batching.MultiCaller,
l2Client cannon.L2HeaderSource,
l1HeaderSource L1HeaderSource,
selective bool,
claimants []common.Address,
) error {
cannonPrestateProvider := cannon.NewPrestateProvider(cfg.CannonAbsolutePreState)
playerCreator := func(game types.GameMetadata, dir string) (scheduler.GamePlayer, error) {
......@@ -191,7 +198,7 @@ func registerCannon(
}
prestateValidator := NewPrestateValidator("cannon", contract.GetAbsolutePrestateHash, cannonPrestateProvider)
genesisValidator := NewPrestateValidator("output root", contract.GetGenesisOutputRoot, prestateProvider)
return NewGamePlayer(ctx, cl, logger, m, dir, game.Proxy, txSender, contract, syncValidator, []Validator{prestateValidator, genesisValidator}, creator, l1HeaderSource)
return NewGamePlayer(ctx, cl, logger, m, dir, game.Proxy, txSender, contract, syncValidator, []Validator{prestateValidator, genesisValidator}, creator, l1HeaderSource, selective, claimants)
}
oracle, err := createOracle(ctx, gameFactory, caller, gameType)
if err != nil {
......
......@@ -48,7 +48,8 @@ type Service struct {
cl *clock.SimpleClock
claimer *claims.BondClaimScheduler
claimants []common.Address
claimer *claims.BondClaimScheduler
factoryContract *contracts.DisputeGameFactoryContract
registry *registry.GameTypeRegistry
......@@ -85,6 +86,7 @@ func (s *Service) initFromConfig(ctx context.Context, cfg *config.Config) error
if err := s.initTxManager(ctx, cfg); err != nil {
return fmt.Errorf("failed to init tx manager: %w", err)
}
s.initClaimants(cfg)
if err := s.initL1Client(ctx, cfg); err != nil {
return fmt.Errorf("failed to init l1 client: %w", err)
}
......@@ -123,6 +125,11 @@ func (s *Service) initFromConfig(ctx context.Context, cfg *config.Config) error
return nil
}
func (s *Service) initClaimants(cfg *config.Config) {
claimants := []common.Address{s.txSender.From()}
s.claimants = append(claimants, cfg.AdditionalBondClaimants...)
}
func (s *Service) initTxManager(ctx context.Context, cfg *config.Config) error {
txMgr, err := txmgr.NewSimpleTxManager("challenger", s.logger, s.metrics, cfg.TxMgrConfig)
if err != nil {
......@@ -198,9 +205,7 @@ func (s *Service) initFactoryContract(cfg *config.Config) error {
}
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...)
claimer := claims.NewBondClaimer(s.logger, s.metrics, s.registry.CreateBondContract, s.txSender, s.claimants...)
s.claimer = claims.NewBondClaimScheduler(s.logger, s.metrics, claimer)
return nil
}
......@@ -220,7 +225,7 @@ func (s *Service) initRollupClient(ctx context.Context, cfg *config.Config) erro
func (s *Service) registerGameTypes(ctx context.Context, cfg *config.Config) error {
gameTypeRegistry := registry.NewGameTypeRegistry()
caller := batching.NewMultiCaller(s.l1Client.Client(), batching.DefaultBatchSize)
closer, err := fault.RegisterGameTypes(gameTypeRegistry, ctx, s.cl, s.logger, s.metrics, cfg, s.rollupClient, s.txSender, s.factoryContract, caller, s.l1Client)
closer, err := fault.RegisterGameTypes(gameTypeRegistry, ctx, s.cl, s.logger, s.metrics, cfg, s.rollupClient, s.txSender, s.factoryContract, caller, s.l1Client, cfg.SelectiveClaimResolution, s.claimants)
if err != nil {
return err
}
......
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