Commit d653ea62 authored by Adrian Sutton's avatar Adrian Sutton Committed by GitHub

op-challenger: Tidy up modules (#6413)

* op-challenger: Remove dependency from config to flags.

Now all CLI logic is encapsulated in cmd and flags module, with config and fault modules being focussed on business logic.

* op-challenger: Move shared types into a types module

Makes it much simpler to avoid dependency cycles by having core type definitions in a different module to the service wiring code that creates everything.

* op-e2e: Update for new challenger modules.
parent e7a25442
......@@ -60,7 +60,7 @@ func run(args []string, action ConfigAction) error {
}
logger.Info("Starting op-challenger", "version", VersionWithMeta)
cfg, err := config.NewConfigFromCLI(ctx)
cfg, err := flags.NewConfigFromCLI(ctx)
if err != nil {
return err
}
......
......@@ -6,7 +6,6 @@ import (
"testing"
"github.com/ethereum-optimism/optimism/op-challenger/config"
"github.com/ethereum-optimism/optimism/op-challenger/flags"
"github.com/ethereum-optimism/optimism/op-service/txmgr"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
......@@ -39,12 +38,12 @@ func TestLogLevel(t *testing.T) {
func TestDefaultCLIOptionsMatchDefaultConfig(t *testing.T) {
cfg := configForArgs(t, addRequiredArgs())
defaultCfg := config.NewConfig(l1EthRpc, common.HexToAddress(gameAddressValue), flags.TraceTypeAlphabet, alphabetTrace, cannonDatadir, true, 4)
defaultCfg := config.NewConfig(l1EthRpc, common.HexToAddress(gameAddressValue), config.TraceTypeAlphabet, alphabetTrace, cannonDatadir, true, 4)
require.Equal(t, defaultCfg, cfg)
}
func TestDefaultConfigIsValid(t *testing.T) {
cfg := config.NewConfig(l1EthRpc, common.HexToAddress(gameAddressValue), flags.TraceTypeAlphabet, alphabetTrace, cannonDatadir, true, 4)
cfg := config.NewConfig(l1EthRpc, common.HexToAddress(gameAddressValue), config.TraceTypeAlphabet, alphabetTrace, cannonDatadir, true, 4)
require.NoError(t, cfg.Check())
}
......@@ -66,7 +65,7 @@ func TestTraceType(t *testing.T) {
verifyArgsInvalid(t, "flag trace-type is required", addRequiredArgsExcept("--trace-type"))
})
for _, traceType := range flags.TraceTypes {
for _, traceType := range config.TraceTypes {
traceType := traceType
t.Run("Valid_"+traceType.String(), func(t *testing.T) {
cfg := configForArgs(t, addRequiredArgsExcept("--trace-type", "--trace-type", traceType.String()))
......@@ -172,7 +171,7 @@ func requiredArgs() map[string]string {
"--agree-with-proposed-output": agreeWithProposedOutput,
"--l1-eth-rpc": l1EthRpc,
"--game-address": gameAddressValue,
"--trace-type": flags.TraceTypeAlphabet.String(),
"--trace-type": "alphabet",
"--alphabet": alphabetTrace,
"--cannon-datadir": cannonDatadir,
}
......
......@@ -2,14 +2,10 @@ package config
import (
"errors"
"strings"
"fmt"
"github.com/ethereum/go-ethereum/common"
"github.com/urfave/cli/v2"
"github.com/ethereum-optimism/optimism/op-challenger/flags"
opservice "github.com/ethereum-optimism/optimism/op-service"
"github.com/ethereum-optimism/optimism/op-service/txmgr"
"github.com/ethereum/go-ethereum/common"
)
var (
......@@ -20,6 +16,36 @@ var (
ErrMissingGameAddress = errors.New("missing game address")
)
type TraceType string
const (
TraceTypeAlphabet TraceType = "alphabet"
TraceTypeCannon TraceType = "cannon"
)
var TraceTypes = []TraceType{TraceTypeAlphabet, TraceTypeCannon}
func (t TraceType) String() string {
return string(t)
}
func (t *TraceType) Set(value string) error {
if !ValidTraceType(TraceType(value)) {
return fmt.Errorf("unknown trace type: %q", value)
}
*t = TraceType(value)
return nil
}
func ValidTraceType(value TraceType) bool {
for _, t := range TraceTypes {
if t == value {
return true
}
}
return false
}
// Config is a well typed config that is parsed from the CLI params.
// This also contains config options for auxiliary services.
// It is used to initialize the challenger.
......@@ -29,7 +55,7 @@ type Config struct {
AgreeWithProposedOutput bool // Temporary config if we agree or disagree with the posted output
GameDepth int // Depth of the game tree
TraceType flags.TraceType // Type of trace
TraceType TraceType // Type of trace
AlphabetTrace string // String for the AlphabetTraceProvider
CannonDatadir string // Cannon Data Directory for the CannonTraceProvider
......@@ -39,7 +65,7 @@ type Config struct {
func NewConfig(
l1EthRpc string,
gameAddress common.Address,
traceType flags.TraceType,
traceType TraceType,
alphabetTrace string,
cannonDatadir string,
agreeWithProposedOutput bool,
......@@ -70,10 +96,10 @@ func (c Config) Check() error {
if c.TraceType == "" {
return ErrMissingTraceType
}
if c.TraceType == flags.TraceTypeCannon && c.CannonDatadir == "" {
if c.TraceType == TraceTypeCannon && c.CannonDatadir == "" {
return ErrMissingCannonDatadir
}
if c.TraceType == flags.TraceTypeAlphabet && c.AlphabetTrace == "" {
if c.TraceType == TraceTypeAlphabet && c.AlphabetTrace == "" {
return ErrMissingAlphabetTrace
}
if err := c.TxMgrConfig.Check(); err != nil {
......@@ -81,30 +107,3 @@ func (c Config) Check() error {
}
return nil
}
// NewConfigFromCLI parses the Config from the provided flags or environment variables.
func NewConfigFromCLI(ctx *cli.Context) (*Config, error) {
if err := flags.CheckRequired(ctx); err != nil {
return nil, err
}
dgfAddress, err := opservice.ParseAddress(ctx.String(flags.DGFAddressFlag.Name))
if err != nil {
return nil, err
}
txMgrConfig := txmgr.ReadCLIConfig(ctx)
traceTypeFlag := flags.TraceType(strings.ToLower(ctx.String(flags.TraceTypeFlag.Name)))
return &Config{
// Required Flags
L1EthRpc: ctx.String(flags.L1EthRpcFlag.Name),
TraceType: traceTypeFlag,
GameAddress: dgfAddress,
AlphabetTrace: ctx.String(flags.AlphabetFlag.Name),
CannonDatadir: ctx.String(flags.CannonDatadirFlag.Name),
AgreeWithProposedOutput: ctx.Bool(flags.AgreeWithProposedOutputFlag.Name),
GameDepth: ctx.Int(flags.GameDepthFlag.Name),
TxMgrConfig: txMgrConfig,
}, nil
}
......@@ -3,7 +3,6 @@ package config
import (
"testing"
"github.com/ethereum-optimism/optimism/op-challenger/flags"
"github.com/ethereum-optimism/optimism/op-service/txmgr"
"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/require"
......@@ -18,27 +17,27 @@ var (
gameDepth = 4
)
func validConfig(traceType flags.TraceType) Config {
func validConfig(traceType TraceType) Config {
cfg := NewConfig(validL1EthRpc, validGameAddress, traceType, validAlphabetTrace, validCannonDatadir, agreeWithProposedOutput, gameDepth)
return cfg
}
// TestValidConfigIsValid checks that the config provided by validConfig is actually valid
func TestValidConfigIsValid(t *testing.T) {
err := validConfig(flags.TraceTypeCannon).Check()
err := validConfig(TraceTypeCannon).Check()
require.NoError(t, err)
}
func TestTxMgrConfig(t *testing.T) {
t.Run("Invalid", func(t *testing.T) {
config := validConfig(flags.TraceTypeCannon)
config := validConfig(TraceTypeCannon)
config.TxMgrConfig = txmgr.CLIConfig{}
require.Equal(t, config.Check().Error(), "must provide a L1 RPC url")
})
}
func TestL1EthRpcRequired(t *testing.T) {
config := validConfig(flags.TraceTypeCannon)
config := validConfig(TraceTypeCannon)
config.L1EthRpc = ""
require.ErrorIs(t, config.Check(), ErrMissingL1EthRPC)
config.L1EthRpc = validL1EthRpc
......@@ -46,7 +45,7 @@ func TestL1EthRpcRequired(t *testing.T) {
}
func TestGameAddressRequired(t *testing.T) {
config := validConfig(flags.TraceTypeCannon)
config := validConfig(TraceTypeCannon)
config.GameAddress = common.Address{}
require.ErrorIs(t, config.Check(), ErrMissingGameAddress)
config.GameAddress = validGameAddress
......@@ -54,7 +53,7 @@ func TestGameAddressRequired(t *testing.T) {
}
func TestAlphabetTraceRequired(t *testing.T) {
config := validConfig(flags.TraceTypeAlphabet)
config := validConfig(TraceTypeAlphabet)
config.AlphabetTrace = ""
require.ErrorIs(t, config.Check(), ErrMissingAlphabetTrace)
config.AlphabetTrace = validAlphabetTrace
......@@ -62,7 +61,7 @@ func TestAlphabetTraceRequired(t *testing.T) {
}
func TestCannonTraceRequired(t *testing.T) {
config := validConfig(flags.TraceTypeCannon)
config := validConfig(TraceTypeCannon)
config.CannonDatadir = ""
require.ErrorIs(t, config.Check(), ErrMissingCannonDatadir)
config.CannonDatadir = validCannonDatadir
......
......@@ -5,6 +5,7 @@ import (
"testing"
"github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/ethereum-optimism/optimism/op-challenger/fault/types"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/accounts/abi/bind/backends"
"github.com/ethereum/go-ethereum/common"
......@@ -81,7 +82,7 @@ func TestBuildFaultStepData(t *testing.T) {
responder, _ := newTestFaultResponder(t, false)
data, err := responder.buildStepTxData(StepCallData{
data, err := responder.buildStepTxData(types.StepCallData{
ClaimIndex: 2,
IsAttack: false,
StateData: []byte{0x01},
......
......@@ -5,9 +5,19 @@ import (
"errors"
"fmt"
"github.com/ethereum-optimism/optimism/op-challenger/fault/types"
"github.com/ethereum/go-ethereum/log"
)
// Responder takes a response action & executes.
// For full op-challenger this means executing the transaction on chain.
type Responder interface {
CanResolve(ctx context.Context) bool
Resolve(ctx context.Context) error
Respond(ctx context.Context, response types.Claim) error
Step(ctx context.Context, stepData types.StepCallData) error
}
type Agent struct {
solver *Solver
loader Loader
......@@ -17,7 +27,7 @@ type Agent struct {
log log.Logger
}
func NewAgent(loader Loader, maxDepth int, trace TraceProvider, responder Responder, agreeWithProposedOutput bool, log log.Logger) *Agent {
func NewAgent(loader Loader, maxDepth int, trace types.TraceProvider, responder Responder, agreeWithProposedOutput bool, log log.Logger) *Agent {
return &Agent{
solver: NewSolver(maxDepth, trace),
loader: loader,
......@@ -67,7 +77,7 @@ func (a *Agent) tryResolve(ctx context.Context) bool {
}
// newGameFromContracts initializes a new game state from the state in the contract
func (a *Agent) newGameFromContracts(ctx context.Context) (Game, error) {
func (a *Agent) newGameFromContracts(ctx context.Context) (types.Game, error) {
claims, err := a.loader.FetchClaims(ctx)
if err != nil {
return nil, fmt.Errorf("failed to fetch claims: %w", err)
......@@ -75,7 +85,7 @@ func (a *Agent) newGameFromContracts(ctx context.Context) (Game, error) {
if len(claims) == 0 {
return nil, errors.New("no claims")
}
game := NewGameState(a.agreeWithProposedOutput, claims[0], uint64(a.maxDepth))
game := types.NewGameState(a.agreeWithProposedOutput, claims[0], uint64(a.maxDepth))
if err := game.PutAll(claims[1:]); err != nil {
return nil, fmt.Errorf("failed to load claims into the local state: %w", err)
}
......@@ -83,7 +93,7 @@ func (a *Agent) newGameFromContracts(ctx context.Context) (Game, error) {
}
// move determines & executes the next move given a claim
func (a *Agent) move(ctx context.Context, claim Claim, game Game) error {
func (a *Agent) move(ctx context.Context, claim types.Claim, game types.Game) error {
nextMove, err := a.solver.NextMove(claim, game.AgreeWithClaimLevel(claim))
if err != nil {
return fmt.Errorf("execute next move: %w", err)
......@@ -105,7 +115,7 @@ func (a *Agent) move(ctx context.Context, claim Claim, game Game) error {
}
// step determines & executes the next step against a leaf claim through the responder
func (a *Agent) step(ctx context.Context, claim Claim, game Game) error {
func (a *Agent) step(ctx context.Context, claim types.Claim, game types.Game) error {
if claim.Depth() != a.maxDepth {
return nil
}
......@@ -129,7 +139,7 @@ func (a *Agent) step(ctx context.Context, claim Claim, game Game) error {
a.log.Info("Performing step", "is_attack", step.IsAttack,
"depth", step.LeafClaim.Depth(), "index_at_depth", step.LeafClaim.IndexAtDepth(), "value", step.LeafClaim.Value)
callData := StepCallData{
callData := types.StepCallData{
ClaimIndex: uint64(step.LeafClaim.ContractIndex),
IsAttack: step.IsAttack,
StateData: step.PreState,
......
......@@ -4,11 +4,12 @@ import (
"math/big"
"strings"
"github.com/ethereum-optimism/optimism/op-challenger/fault/types"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
)
var _ TraceProvider = (*AlphabetProvider)(nil)
var _ types.TraceProvider = (*AlphabetProvider)(nil)
// AlphabetProvider is a [TraceProvider] that provides claims for specific
// indices in the given trace.
......@@ -29,7 +30,7 @@ func NewAlphabetProvider(state string, depth uint64) *AlphabetProvider {
func (ap *AlphabetProvider) GetPreimage(i uint64) ([]byte, []byte, error) {
// The index cannot be larger than the maximum index as computed by the depth.
if i >= ap.maxLen {
return nil, nil, ErrIndexTooLarge
return nil, nil, types.ErrIndexTooLarge
}
// We extend the deepest hash to the maximum depth if the trace is not expansive.
if i >= uint64(len(ap.state)) {
......
......@@ -4,6 +4,7 @@ import (
"math/big"
"testing"
"github.com/ethereum-optimism/optimism/op-challenger/fault/types"
"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/require"
)
......@@ -66,7 +67,7 @@ func TestGetPreimage_Succeeds(t *testing.T) {
func TestGetPreimage_TooLargeIndex_Fails(t *testing.T) {
ap := NewAlphabetProvider("abc", 2)
_, _, err := ap.GetPreimage(4)
require.ErrorIs(t, err, ErrIndexTooLarge)
require.ErrorIs(t, err, types.ErrIndexTooLarge)
}
// TestGet_Succeeds tests the Get function.
......@@ -83,7 +84,7 @@ func TestGet_Succeeds(t *testing.T) {
func TestGet_IndexTooLarge(t *testing.T) {
ap := NewAlphabetProvider("abc", 2)
_, err := ap.Get(4)
require.ErrorIs(t, err, ErrIndexTooLarge)
require.ErrorIs(t, err, types.ErrIndexTooLarge)
}
// TestGet_Extends tests the Get function with an index that is larger
......
......@@ -5,6 +5,7 @@ import (
"math/big"
"github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/ethereum-optimism/optimism/op-challenger/fault/types"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
......@@ -59,9 +60,9 @@ func (fc *FaultCaller) LogGameInfo(ctx context.Context) {
// 0: In Progress
// 1: Challenger Won
// 2: Defender Won
func (fc *FaultCaller) GetGameStatus(ctx context.Context) (GameStatus, error) {
func (fc *FaultCaller) GetGameStatus(ctx context.Context) (types.GameStatus, error) {
status, err := fc.Status(&bind.CallOpts{Context: ctx})
return GameStatus(status), err
return types.GameStatus(status), err
}
// GetClaimDataLength returns the number of claims in the game.
......@@ -79,13 +80,13 @@ func (fc *FaultCaller) LogClaimDataLength(ctx context.Context) {
}
// GameStatusString returns the current game status as a string.
func GameStatusString(status GameStatus) string {
func GameStatusString(status types.GameStatus) string {
switch status {
case GameStatusInProgress:
case types.GameStatusInProgress:
return "In Progress"
case GameStatusChallengerWon:
case types.GameStatusChallengerWon:
return "Challenger Won"
case GameStatusDefenderWon:
case types.GameStatusDefenderWon:
return "Defender Won"
default:
return "Unknown"
......
......@@ -6,6 +6,7 @@ import (
"math/big"
"testing"
"github.com/ethereum-optimism/optimism/op-challenger/fault/types"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/stretchr/testify/require"
)
......@@ -40,7 +41,7 @@ func TestFaultCaller_GetGameStatus(t *testing.T) {
tests := []struct {
name string
caller FaultDisputeGameCaller
expectedStatus GameStatus
expectedStatus types.GameStatus
expectedErr error
}{
{
......@@ -48,7 +49,7 @@ func TestFaultCaller_GetGameStatus(t *testing.T) {
caller: &mockFaultDisputeGameCaller{
status: 1,
},
expectedStatus: GameStatusChallengerWon,
expectedStatus: types.GameStatusChallengerWon,
expectedErr: nil,
},
{
......
......@@ -4,6 +4,7 @@ import (
"context"
"math/big"
"github.com/ethereum-optimism/optimism/op-challenger/fault/types"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
)
......@@ -22,7 +23,7 @@ type ClaimFetcher interface {
// Loader is a minimal interface for loading onchain [Claim] data.
type Loader interface {
FetchClaims(ctx context.Context) ([]Claim, error)
FetchClaims(ctx context.Context) ([]types.Claim, error)
}
// loader pulls in fault dispute game claim data periodically and over subscriptions.
......@@ -38,20 +39,20 @@ func NewLoader(claimFetcher ClaimFetcher) *loader {
}
// fetchClaim fetches a single [Claim] with a hydrated parent.
func (l *loader) fetchClaim(ctx context.Context, arrIndex uint64) (Claim, error) {
func (l *loader) fetchClaim(ctx context.Context, arrIndex uint64) (types.Claim, error) {
callOpts := bind.CallOpts{
Context: ctx,
}
fetchedClaim, err := l.claimFetcher.ClaimData(&callOpts, new(big.Int).SetUint64(arrIndex))
if err != nil {
return Claim{}, err
return types.Claim{}, err
}
claim := Claim{
ClaimData: ClaimData{
claim := types.Claim{
ClaimData: types.ClaimData{
Value: fetchedClaim.Claim,
Position: NewPositionFromGIndex(fetchedClaim.Position.Uint64()),
Position: types.NewPositionFromGIndex(fetchedClaim.Position.Uint64()),
},
Countered: fetchedClaim.Countered,
Clock: fetchedClaim.Clock.Uint64(),
......@@ -63,11 +64,11 @@ func (l *loader) fetchClaim(ctx context.Context, arrIndex uint64) (Claim, error)
parentIndex := uint64(fetchedClaim.ParentIndex)
parentClaim, err := l.claimFetcher.ClaimData(&callOpts, new(big.Int).SetUint64(parentIndex))
if err != nil {
return Claim{}, err
return types.Claim{}, err
}
claim.Parent = ClaimData{
claim.Parent = types.ClaimData{
Value: parentClaim.Claim,
Position: NewPositionFromGIndex(parentClaim.Position.Uint64()),
Position: types.NewPositionFromGIndex(parentClaim.Position.Uint64()),
}
}
......@@ -75,7 +76,7 @@ func (l *loader) fetchClaim(ctx context.Context, arrIndex uint64) (Claim, error)
}
// FetchClaims fetches all claims from the fault dispute game.
func (l *loader) FetchClaims(ctx context.Context) ([]Claim, error) {
func (l *loader) FetchClaims(ctx context.Context) ([]types.Claim, error) {
// Get the current claim count.
claimCount, err := l.claimFetcher.ClaimDataLen(&bind.CallOpts{
Context: ctx,
......@@ -85,7 +86,7 @@ func (l *loader) FetchClaims(ctx context.Context) ([]Claim, error) {
}
// Fetch each claim and build a list.
claimList := make([]Claim, claimCount.Uint64())
claimList := make([]types.Claim, claimCount.Uint64())
for i := uint64(0); i < claimCount.Uint64(); i++ {
claim, err := l.fetchClaim(ctx, i)
if err != nil {
......
......@@ -6,6 +6,7 @@ import (
"math/big"
"testing"
"github.com/ethereum-optimism/optimism/op-challenger/fault/types"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/stretchr/testify/require"
)
......@@ -94,41 +95,41 @@ func TestLoader_FetchClaims_Succeeds(t *testing.T) {
loader := NewLoader(mockClaimFetcher)
claims, err := loader.FetchClaims(context.Background())
require.NoError(t, err)
require.ElementsMatch(t, []Claim{
require.ElementsMatch(t, []types.Claim{
{
ClaimData: ClaimData{
ClaimData: types.ClaimData{
Value: expectedClaims[0].Claim,
Position: NewPositionFromGIndex(expectedClaims[0].Position.Uint64()),
Position: types.NewPositionFromGIndex(expectedClaims[0].Position.Uint64()),
},
Parent: ClaimData{
Parent: types.ClaimData{
Value: expectedClaims[0].Claim,
Position: NewPositionFromGIndex(expectedClaims[0].Position.Uint64()),
Position: types.NewPositionFromGIndex(expectedClaims[0].Position.Uint64()),
},
Countered: false,
Clock: uint64(0),
ContractIndex: 0,
},
{
ClaimData: ClaimData{
ClaimData: types.ClaimData{
Value: expectedClaims[1].Claim,
Position: NewPositionFromGIndex(expectedClaims[1].Position.Uint64()),
Position: types.NewPositionFromGIndex(expectedClaims[1].Position.Uint64()),
},
Parent: ClaimData{
Parent: types.ClaimData{
Value: expectedClaims[0].Claim,
Position: NewPositionFromGIndex(expectedClaims[1].Position.Uint64()),
Position: types.NewPositionFromGIndex(expectedClaims[1].Position.Uint64()),
},
Countered: false,
Clock: uint64(0),
ContractIndex: 1,
},
{
ClaimData: ClaimData{
ClaimData: types.ClaimData{
Value: expectedClaims[2].Claim,
Position: NewPositionFromGIndex(expectedClaims[2].Position.Uint64()),
Position: types.NewPositionFromGIndex(expectedClaims[2].Position.Uint64()),
},
Parent: ClaimData{
Parent: types.ClaimData{
Value: expectedClaims[0].Claim,
Position: NewPositionFromGIndex(expectedClaims[2].Position.Uint64()),
Position: types.NewPositionFromGIndex(expectedClaims[2].Position.Uint64()),
},
Countered: false,
Clock: uint64(0),
......
......@@ -4,11 +4,12 @@ import (
"context"
"time"
"github.com/ethereum-optimism/optimism/op-challenger/fault/types"
"github.com/ethereum/go-ethereum/log"
)
type GameInfo interface {
GetGameStatus(context.Context) (GameStatus, error)
GetGameStatus(context.Context) (types.GameStatus, error)
LogGameInfo(ctx context.Context)
}
......@@ -43,11 +44,11 @@ func progressGame(ctx context.Context, logger log.Logger, agreeWithProposedOutpu
if status, err := caller.GetGameStatus(ctx); err != nil {
logger.Warn("Unable to retrieve game status", "err", err)
} else if status != 0 {
var expectedStatus GameStatus
var expectedStatus types.GameStatus
if agreeWithProposedOutput {
expectedStatus = GameStatusChallengerWon
expectedStatus = types.GameStatusChallengerWon
} else {
expectedStatus = GameStatusDefenderWon
expectedStatus = types.GameStatusDefenderWon
}
if expectedStatus == status {
logger.Info("Game won", "status", GameStatusString(status))
......
......@@ -5,6 +5,7 @@ import (
"errors"
"testing"
"github.com/ethereum-optimism/optimism/op-challenger/fault/types"
"github.com/ethereum-optimism/optimism/op-node/testlog"
"github.com/ethereum/go-ethereum/log"
"github.com/stretchr/testify/require"
......@@ -43,7 +44,7 @@ func TestProgressGame_LogErrorFromAct(t *testing.T) {
func TestProgressGame_LogErrorWhenGameLost(t *testing.T) {
tests := []struct {
name string
status GameStatus
status types.GameStatus
agreeWithOutput bool
logLevel log.Lvl
logMsg string
......@@ -51,7 +52,7 @@ func TestProgressGame_LogErrorWhenGameLost(t *testing.T) {
}{
{
name: "GameLostAsDefender",
status: GameStatusChallengerWon,
status: types.GameStatusChallengerWon,
agreeWithOutput: false,
logLevel: log.LvlError,
logMsg: "Game lost",
......@@ -59,7 +60,7 @@ func TestProgressGame_LogErrorWhenGameLost(t *testing.T) {
},
{
name: "GameLostAsChallenger",
status: GameStatusDefenderWon,
status: types.GameStatusDefenderWon,
agreeWithOutput: true,
logLevel: log.LvlError,
logMsg: "Game lost",
......@@ -67,7 +68,7 @@ func TestProgressGame_LogErrorWhenGameLost(t *testing.T) {
},
{
name: "GameWonAsDefender",
status: GameStatusDefenderWon,
status: types.GameStatusDefenderWon,
agreeWithOutput: false,
logLevel: log.LvlInfo,
logMsg: "Game won",
......@@ -75,7 +76,7 @@ func TestProgressGame_LogErrorWhenGameLost(t *testing.T) {
},
{
name: "GameWonAsChallenger",
status: GameStatusChallengerWon,
status: types.GameStatusChallengerWon,
agreeWithOutput: true,
logLevel: log.LvlInfo,
logMsg: "Game won",
......@@ -120,12 +121,12 @@ func (a *stubActor) Act(ctx context.Context) error {
}
type stubGameInfo struct {
status GameStatus
status types.GameStatus
err error
logCount int
}
func (s *stubGameInfo) GetGameStatus(ctx context.Context) (GameStatus, error) {
func (s *stubGameInfo) GetGameStatus(ctx context.Context) (types.GameStatus, error) {
return s.status, s.err
}
......
......@@ -5,6 +5,7 @@ import (
"math/big"
"github.com/ethereum-optimism/optimism/op-bindings/bindings"
types2 "github.com/ethereum-optimism/optimism/op-challenger/fault/types"
"github.com/ethereum-optimism/optimism/op-service/txmgr"
"github.com/ethereum/go-ethereum"
......@@ -62,7 +63,7 @@ func (r *faultResponder) buildResolveData() ([]byte, error) {
}
// BuildTx builds the transaction for the [faultResponder].
func (r *faultResponder) BuildTx(ctx context.Context, response Claim) ([]byte, error) {
func (r *faultResponder) BuildTx(ctx context.Context, response types2.Claim) ([]byte, error) {
if response.DefendsParent() {
txData, err := r.buildFaultDefendData(response.ParentContractIndex, response.ValueBytes())
if err != nil {
......@@ -103,7 +104,7 @@ func (r *faultResponder) Resolve(ctx context.Context) error {
}
// Respond takes a [Claim] and executes the response action.
func (r *faultResponder) Respond(ctx context.Context, response Claim) error {
func (r *faultResponder) Respond(ctx context.Context, response types2.Claim) error {
txData, err := r.BuildTx(ctx, response)
if err != nil {
return err
......@@ -131,7 +132,7 @@ func (r *faultResponder) sendTxAndWait(ctx context.Context, txData []byte) error
}
// buildStepTxData creates the transaction data for the step function.
func (r *faultResponder) buildStepTxData(stepData StepCallData) ([]byte, error) {
func (r *faultResponder) buildStepTxData(stepData types2.StepCallData) ([]byte, error) {
return r.fdgAbi.Pack(
"step",
big.NewInt(int64(stepData.ClaimIndex)),
......@@ -142,7 +143,7 @@ func (r *faultResponder) buildStepTxData(stepData StepCallData) ([]byte, error)
}
// Step accepts step data and executes the step on the fault dispute game contract.
func (r *faultResponder) Step(ctx context.Context, stepData StepCallData) error {
func (r *faultResponder) Step(ctx context.Context, stepData types2.StepCallData) error {
txData, err := r.buildStepTxData(stepData)
if err != nil {
return err
......
......@@ -7,6 +7,7 @@ import (
"testing"
"github.com/ethereum-optimism/optimism/op-bindings/bindings"
types2 "github.com/ethereum-optimism/optimism/op-challenger/fault/types"
"github.com/ethereum-optimism/optimism/op-node/testlog"
"github.com/ethereum-optimism/optimism/op-service/txmgr"
......@@ -107,14 +108,14 @@ func TestResponder_Resolve_Success(t *testing.T) {
// bubbles up the error returned by the [txmgr.Send] method.
func TestResponder_Respond_SendFails(t *testing.T) {
responder, mockTxMgr := newTestFaultResponder(t, true)
err := responder.Respond(context.Background(), Claim{
ClaimData: ClaimData{
err := responder.Respond(context.Background(), types2.Claim{
ClaimData: types2.ClaimData{
Value: common.Hash{0x01},
Position: NewPositionFromGIndex(2),
Position: types2.NewPositionFromGIndex(2),
},
Parent: ClaimData{
Parent: types2.ClaimData{
Value: common.Hash{0x02},
Position: NewPositionFromGIndex(1),
Position: types2.NewPositionFromGIndex(1),
},
ContractIndex: 0,
ParentContractIndex: 0,
......@@ -127,14 +128,14 @@ func TestResponder_Respond_SendFails(t *testing.T) {
// succeeds when the tx candidate is successfully sent through the txmgr.
func TestResponder_Respond_Success(t *testing.T) {
responder, mockTxMgr := newTestFaultResponder(t, false)
err := responder.Respond(context.Background(), Claim{
ClaimData: ClaimData{
err := responder.Respond(context.Background(), types2.Claim{
ClaimData: types2.ClaimData{
Value: common.Hash{0x01},
Position: NewPositionFromGIndex(2),
Position: types2.NewPositionFromGIndex(2),
},
Parent: ClaimData{
Parent: types2.ClaimData{
Value: common.Hash{0x02},
Position: NewPositionFromGIndex(1),
Position: types2.NewPositionFromGIndex(1),
},
ContractIndex: 0,
ParentContractIndex: 0,
......@@ -147,14 +148,14 @@ func TestResponder_Respond_Success(t *testing.T) {
// returns a tx candidate with the correct data for an attack tx.
func TestResponder_BuildTx_Attack(t *testing.T) {
responder, _ := newTestFaultResponder(t, false)
responseClaim := Claim{
ClaimData: ClaimData{
responseClaim := types2.Claim{
ClaimData: types2.ClaimData{
Value: common.Hash{0x01},
Position: NewPositionFromGIndex(2),
Position: types2.NewPositionFromGIndex(2),
},
Parent: ClaimData{
Parent: types2.ClaimData{
Value: common.Hash{0x02},
Position: NewPositionFromGIndex(1),
Position: types2.NewPositionFromGIndex(1),
},
ContractIndex: 0,
ParentContractIndex: 7,
......@@ -178,14 +179,14 @@ func TestResponder_BuildTx_Attack(t *testing.T) {
// returns a tx candidate with the correct data for a defend tx.
func TestResponder_BuildTx_Defend(t *testing.T) {
responder, _ := newTestFaultResponder(t, false)
responseClaim := Claim{
ClaimData: ClaimData{
responseClaim := types2.Claim{
ClaimData: types2.ClaimData{
Value: common.Hash{0x01},
Position: NewPositionFromGIndex(3),
Position: types2.NewPositionFromGIndex(3),
},
Parent: ClaimData{
Parent: types2.ClaimData{
Value: common.Hash{0x02},
Position: NewPositionFromGIndex(6),
Position: types2.NewPositionFromGIndex(6),
},
ContractIndex: 0,
ParentContractIndex: 7,
......
......@@ -7,7 +7,7 @@ import (
"github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/ethereum-optimism/optimism/op-challenger/config"
"github.com/ethereum-optimism/optimism/op-challenger/fault/cannon"
"github.com/ethereum-optimism/optimism/op-challenger/flags"
"github.com/ethereum-optimism/optimism/op-challenger/fault/types"
"github.com/ethereum-optimism/optimism/op-service/txmgr"
"github.com/ethereum-optimism/optimism/op-service/txmgr/metrics"
"github.com/ethereum/go-ethereum/ethclient"
......@@ -52,11 +52,11 @@ func NewService(ctx context.Context, logger log.Logger, cfg *config.Config) (*se
return nil, fmt.Errorf("failed to create the responder: %w", err)
}
var trace TraceProvider
var trace types.TraceProvider
switch cfg.TraceType {
case flags.TraceTypeCannon:
case config.TraceTypeCannon:
trace = cannon.NewCannonTraceProvider(cfg.CannonDatadir)
case flags.TraceTypeAlphabet:
case config.TraceTypeAlphabet:
trace = NewAlphabetProvider(cfg.AlphabetTrace, uint64(cfg.GameDepth))
default:
return nil, fmt.Errorf("unsupported trace type: %v", cfg.TraceType)
......
......@@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"github.com/ethereum-optimism/optimism/op-challenger/fault/types"
"github.com/ethereum/go-ethereum/common"
)
......@@ -13,12 +14,12 @@ var (
// Solver uses a [TraceProvider] to determine the moves to make in a dispute game.
type Solver struct {
TraceProvider
types.TraceProvider
gameDepth int
}
// NewSolver creates a new [Solver] using the provided [TraceProvider].
func NewSolver(gameDepth int, traceProvider TraceProvider) *Solver {
func NewSolver(gameDepth int, traceProvider types.TraceProvider) *Solver {
return &Solver{
traceProvider,
gameDepth,
......@@ -26,7 +27,7 @@ func NewSolver(gameDepth int, traceProvider TraceProvider) *Solver {
}
// NextMove returns the next move to make given the current state of the game.
func (s *Solver) NextMove(claim Claim, agreeWithClaimLevel bool) (*Claim, error) {
func (s *Solver) NextMove(claim types.Claim, agreeWithClaimLevel bool) (*types.Claim, error) {
if agreeWithClaimLevel {
return nil, nil
}
......@@ -38,7 +39,7 @@ func (s *Solver) NextMove(claim Claim, agreeWithClaimLevel bool) (*Claim, error)
return s.handleMiddle(claim)
}
func (s *Solver) handleRoot(claim Claim) (*Claim, error) {
func (s *Solver) handleRoot(claim types.Claim) (*types.Claim, error) {
agree, err := s.agreeWithClaim(claim.ClaimData)
if err != nil {
return nil, err
......@@ -53,7 +54,7 @@ func (s *Solver) handleRoot(claim Claim) (*Claim, error) {
}
}
func (s *Solver) handleMiddle(claim Claim) (*Claim, error) {
func (s *Solver) handleMiddle(claim types.Claim) (*types.Claim, error) {
claimCorrect, err := s.agreeWithClaim(claim.ClaimData)
if err != nil {
return nil, err
......@@ -69,7 +70,7 @@ func (s *Solver) handleMiddle(claim Claim) (*Claim, error) {
}
type StepData struct {
LeafClaim Claim
LeafClaim types.Claim
IsAttack bool
PreState []byte
ProofData []byte
......@@ -77,7 +78,7 @@ type StepData struct {
// AttemptStep determines what step should occur for a given leaf claim.
// An error will be returned if the claim is not at the max depth.
func (s *Solver) AttemptStep(claim Claim, agreeWithClaimLevel bool) (StepData, error) {
func (s *Solver) AttemptStep(claim types.Claim, agreeWithClaimLevel bool) (StepData, error) {
if claim.Depth() != s.gameDepth {
return StepData{}, errors.New("cannot step on non-leaf claims")
}
......@@ -114,41 +115,41 @@ func (s *Solver) AttemptStep(claim Claim, agreeWithClaimLevel bool) (StepData, e
}
// attack returns a response that attacks the claim.
func (s *Solver) attack(claim Claim) (*Claim, error) {
func (s *Solver) attack(claim types.Claim) (*types.Claim, error) {
position := claim.Attack()
value, err := s.traceAtPosition(position)
if err != nil {
return nil, fmt.Errorf("attack claim: %w", err)
}
return &Claim{
ClaimData: ClaimData{Value: value, Position: position},
return &types.Claim{
ClaimData: types.ClaimData{Value: value, Position: position},
Parent: claim.ClaimData,
ParentContractIndex: claim.ContractIndex,
}, nil
}
// defend returns a response that defends the claim.
func (s *Solver) defend(claim Claim) (*Claim, error) {
func (s *Solver) defend(claim types.Claim) (*types.Claim, error) {
position := claim.Defend()
value, err := s.traceAtPosition(position)
if err != nil {
return nil, fmt.Errorf("defend claim: %w", err)
}
return &Claim{
ClaimData: ClaimData{Value: value, Position: position},
return &types.Claim{
ClaimData: types.ClaimData{Value: value, Position: position},
Parent: claim.ClaimData,
ParentContractIndex: claim.ContractIndex,
}, nil
}
// agreeWithClaim returns true if the claim is correct according to the internal [TraceProvider].
func (s *Solver) agreeWithClaim(claim ClaimData) (bool, error) {
func (s *Solver) agreeWithClaim(claim types.ClaimData) (bool, error) {
ourValue, err := s.traceAtPosition(claim.Position)
return ourValue == claim.Value, err
}
// traceAtPosition returns the [common.Hash] from internal [TraceProvider] at the given [Position].
func (s *Solver) traceAtPosition(p Position) (common.Hash, error) {
func (s *Solver) traceAtPosition(p types.Position) (common.Hash, error) {
index := p.TraceIndex(s.gameDepth)
hash, err := s.Get(index)
return hash, err
......
......@@ -3,6 +3,7 @@ package fault
import (
"testing"
"github.com/ethereum-optimism/optimism/op-challenger/fault/types"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/stretchr/testify/require"
......@@ -23,52 +24,52 @@ func TestSolver_NextMove_Opponent(t *testing.T) {
// The following claims are created using the state: "abcdexyz".
// The responses are the responses we expect from the solver.
indices := []struct {
claim Claim
response ClaimData
claim types.Claim
response types.ClaimData
}{
{
Claim{
ClaimData: ClaimData{
types.Claim{
ClaimData: types.ClaimData{
Value: alphabetClaim(7, "z"),
Position: NewPosition(0, 0),
Position: types.NewPosition(0, 0),
},
// Root claim has no parent
},
ClaimData{
types.ClaimData{
Value: alphabetClaim(3, "d"),
Position: NewPosition(1, 0),
Position: types.NewPosition(1, 0),
},
},
{
Claim{
ClaimData: ClaimData{
types.Claim{
ClaimData: types.ClaimData{
Value: alphabetClaim(3, "d"),
Position: NewPosition(1, 0),
Position: types.NewPosition(1, 0),
},
Parent: ClaimData{
Parent: types.ClaimData{
Value: alphabetClaim(7, "h"),
Position: NewPosition(0, 0),
Position: types.NewPosition(0, 0),
},
},
ClaimData{
types.ClaimData{
Value: alphabetClaim(5, "f"),
Position: NewPosition(2, 2),
Position: types.NewPosition(2, 2),
},
},
{
Claim{
ClaimData: ClaimData{
types.Claim{
ClaimData: types.ClaimData{
Value: alphabetClaim(5, "x"),
Position: NewPosition(2, 2),
Position: types.NewPosition(2, 2),
},
Parent: ClaimData{
Parent: types.ClaimData{
Value: alphabetClaim(7, "h"),
Position: NewPosition(1, 1),
Position: types.NewPosition(1, 1),
},
},
ClaimData{
types.ClaimData{
Value: alphabetClaim(4, "e"),
Position: NewPosition(3, 4),
Position: types.NewPosition(3, 4),
},
},
}
......@@ -85,10 +86,10 @@ func TestNoMoveAgainstOwnLevel(t *testing.T) {
mallory := NewAlphabetProvider("abcdepqr", uint64(maxDepth))
solver := NewSolver(maxDepth, mallory)
claim := Claim{
ClaimData: ClaimData{
claim := types.Claim{
ClaimData: types.ClaimData{
Value: alphabetClaim(7, "z"),
Position: NewPosition(0, 0),
Position: types.NewPosition(0, 0),
},
// Root claim has no parent
}
......@@ -104,10 +105,10 @@ func TestAttemptStep(t *testing.T) {
solver := NewSolver(maxDepth, canonicalProvider)
_, _, middle, bottom := createTestClaims()
zero := Claim{
ClaimData: ClaimData{
zero := types.Claim{
ClaimData: types.ClaimData{
// Zero value is a purposely disagree with claim value "a"
Position: NewPosition(3, 0),
Position: types.NewPosition(3, 0),
},
}
......@@ -150,3 +151,39 @@ func (a *alphabetWithProofProvider) GetPreimage(i uint64) ([]byte, []byte, error
}
return preimage, []byte{byte(i)}, nil
}
func createTestClaims() (types.Claim, types.Claim, types.Claim, types.Claim) {
// root & middle are from the trace "abcdexyz"
// top & bottom are from the trace "abcdefgh"
root := types.Claim{
ClaimData: types.ClaimData{
Value: common.HexToHash("0x000000000000000000000000000000000000000000000000000000000000077a"),
Position: types.NewPosition(0, 0),
},
// Root claim has no parent
}
top := types.Claim{
ClaimData: types.ClaimData{
Value: common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000364"),
Position: types.NewPosition(1, 0),
},
Parent: root.ClaimData,
}
middle := types.Claim{
ClaimData: types.ClaimData{
Value: common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000578"),
Position: types.NewPosition(2, 2),
},
Parent: top.ClaimData,
}
bottom := types.Claim{
ClaimData: types.ClaimData{
Value: common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000465"),
Position: types.NewPosition(3, 4),
},
Parent: middle.ClaimData,
}
return root, top, middle, bottom
}
package fault
package types
import (
"errors"
......@@ -26,7 +26,7 @@ type Game interface {
// IsDuplicate returns true if the provided [Claim] already exists in the game state.
IsDuplicate(claim Claim) bool
// AgreeWithLevel returns if the game state agrees with the provided claim level.
// AgreeWithClaimLevel returns if the game state agrees with the provided claim level.
AgreeWithClaimLevel(claim Claim) bool
}
......
package fault
package types
import (
"context"
"errors"
"github.com/ethereum/go-ethereum/common"
......@@ -83,12 +82,3 @@ func (c *Claim) IsRoot() bool {
func (c *Claim) DefendsParent() bool {
return (c.IndexAtDepth() >> 1) != c.Parent.IndexAtDepth()
}
// Responder takes a response action & executes.
// For full op-challenger this means executing the transaction on chain.
type Responder interface {
CanResolve(ctx context.Context) bool
Resolve(ctx context.Context) error
Respond(ctx context.Context, response Claim) error
Step(ctx context.Context, stepData StepCallData) error
}
......@@ -4,6 +4,7 @@ import (
"fmt"
"strings"
"github.com/ethereum-optimism/optimism/op-challenger/config"
openum "github.com/ethereum-optimism/optimism/op-service/enum"
"github.com/urfave/cli/v2"
......@@ -20,36 +21,6 @@ func prefixEnvVars(name string) []string {
return opservice.PrefixEnvVar(envVarPrefix, name)
}
type TraceType string
const (
TraceTypeAlphabet TraceType = "alphabet"
TraceTypeCannon TraceType = "cannon"
)
var TraceTypes = []TraceType{TraceTypeAlphabet, TraceTypeCannon}
func (t TraceType) String() string {
return string(t)
}
func (t *TraceType) Set(value string) error {
if !ValidTraceType(TraceType(value)) {
return fmt.Errorf("unknown trace type: %q", value)
}
*t = TraceType(value)
return nil
}
func ValidTraceType(value TraceType) bool {
for _, t := range TraceTypes {
if t == value {
return true
}
}
return false
}
var (
// Required Flags
L1EthRpcFlag = &cli.StringFlag{
......@@ -64,10 +35,10 @@ var (
}
TraceTypeFlag = &cli.GenericFlag{
Name: "trace-type",
Usage: "The trace type. Valid options: " + openum.EnumString(TraceTypes),
Usage: "The trace type. Valid options: " + openum.EnumString(config.TraceTypes),
EnvVars: prefixEnvVars("TRACE_TYPE"),
Value: func() *TraceType {
out := TraceType("") // No default value
Value: func() *config.TraceType {
out := config.TraceType("") // No default value
return &out
}(),
}
......@@ -125,18 +96,45 @@ func CheckRequired(ctx *cli.Context) error {
return fmt.Errorf("flag %s is required", f.Names()[0])
}
}
gameType := TraceType(strings.ToLower(ctx.String(TraceTypeFlag.Name)))
gameType := config.TraceType(strings.ToLower(ctx.String(TraceTypeFlag.Name)))
switch gameType {
case TraceTypeCannon:
case config.TraceTypeCannon:
if !ctx.IsSet(CannonDatadirFlag.Name) {
return fmt.Errorf("flag %s is required", "cannon-datadir")
}
case TraceTypeAlphabet:
case config.TraceTypeAlphabet:
if !ctx.IsSet(AlphabetFlag.Name) {
return fmt.Errorf("flag %s is required", "alphabet")
}
default:
return fmt.Errorf("invalid trace type. must be one of %v", TraceTypes)
return fmt.Errorf("invalid trace type. must be one of %v", config.TraceTypes)
}
return nil
}
// NewConfigFromCLI parses the Config from the provided flags or environment variables.
func NewConfigFromCLI(ctx *cli.Context) (*config.Config, error) {
if err := CheckRequired(ctx); err != nil {
return nil, err
}
dgfAddress, err := opservice.ParseAddress(ctx.String(DGFAddressFlag.Name))
if err != nil {
return nil, err
}
txMgrConfig := txmgr.ReadCLIConfig(ctx)
traceTypeFlag := config.TraceType(strings.ToLower(ctx.String(TraceTypeFlag.Name)))
return &config.Config{
// Required Flags
L1EthRpc: ctx.String(L1EthRpcFlag.Name),
TraceType: traceTypeFlag,
GameAddress: dgfAddress,
AlphabetTrace: ctx.String(AlphabetFlag.Name),
CannonDatadir: ctx.String(CannonDatadirFlag.Name),
AgreeWithProposedOutput: ctx.Bool(AgreeWithProposedOutputFlag.Name),
GameDepth: ctx.Int(GameDepthFlag.Name),
TxMgrConfig: txMgrConfig,
}, nil
}
......@@ -11,7 +11,7 @@ import (
"github.com/ethereum-optimism/optimism/op-chain-ops/deployer"
"github.com/ethereum-optimism/optimism/op-challenger/config"
"github.com/ethereum-optimism/optimism/op-challenger/fault"
"github.com/ethereum-optimism/optimism/op-challenger/flags"
"github.com/ethereum-optimism/optimism/op-challenger/fault/types"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/challenger"
"github.com/ethereum-optimism/optimism/op-service/client/utils"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
......@@ -107,7 +107,7 @@ func (g *FaultGameHelper) StartChallenger(ctx context.Context, l1Endpoint string
func(c *config.Config) {
c.GameAddress = g.addr
c.GameDepth = alphabetGameDepth
c.TraceType = flags.TraceTypeAlphabet
c.TraceType = config.TraceTypeAlphabet
// By default the challenger agrees with the root claim (thus disagrees with the proposed output)
// This can be overridden by passing in options
c.AlphabetTrace = g.claimedAlphabet
......@@ -169,7 +169,7 @@ func (g *FaultGameHelper) WaitForClaim(ctx context.Context, predicate func(claim
func (g *FaultGameHelper) WaitForClaimAtMaxDepth(ctx context.Context, countered bool) {
g.WaitForClaim(ctx, func(claim ContractClaim) bool {
pos := fault.NewPositionFromGIndex(claim.Position.Uint64())
pos := types.NewPositionFromGIndex(claim.Position.Uint64())
return pos.Depth() == g.maxDepth && claim.Countered == countered
})
}
......
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