Commit d61af8a3 authored by refcell.eth's avatar refcell.eth Committed by GitHub

Merge pull request #8379 from ethereum-optimism/aj/output-cannon-new-contract

challenger: Switch output_cannon to use the new contract type.
parents bd06d4ee a51785e0
...@@ -34,7 +34,6 @@ var ( ...@@ -34,7 +34,6 @@ var (
ErrCannonNetworkAndL2Genesis = errors.New("only specify one of network or l2 genesis path") ErrCannonNetworkAndL2Genesis = errors.New("only specify one of network or l2 genesis path")
ErrCannonNetworkUnknown = errors.New("unknown cannon network") ErrCannonNetworkUnknown = errors.New("unknown cannon network")
ErrMissingRollupRpc = errors.New("missing rollup rpc url") ErrMissingRollupRpc = errors.New("missing rollup rpc url")
ErrCannonAndOutputCannonConflict = errors.New("trace types cannon and outputCannon cannot be enabled at the same time")
) )
type TraceType string type TraceType string
...@@ -184,12 +183,6 @@ func (c Config) Check() error { ...@@ -184,12 +183,6 @@ func (c Config) Check() error {
if c.RollupRpc == "" { if c.RollupRpc == "" {
return ErrMissingRollupRpc return ErrMissingRollupRpc
} }
if c.TraceTypeEnabled(TraceTypeCannon) && (c.TraceTypeEnabled(TraceTypeOutputCannon) || c.TraceTypeEnabled(TraceTypeOutputAlphabet)) {
return ErrCannonAndOutputCannonConflict
}
}
if c.TraceTypeEnabled(TraceTypeCannon) && c.TraceTypeEnabled(TraceTypeOutputCannon) {
return ErrCannonAndOutputCannonConflict
} }
if c.TraceTypeEnabled(TraceTypeCannon) || c.TraceTypeEnabled(TraceTypeOutputCannon) { if c.TraceTypeEnabled(TraceTypeCannon) || c.TraceTypeEnabled(TraceTypeOutputCannon) {
if c.CannonBin == "" { if c.CannonBin == "" {
......
...@@ -146,12 +146,6 @@ func TestRollupRpcRequired_OutputAlphabet(t *testing.T) { ...@@ -146,12 +146,6 @@ func TestRollupRpcRequired_OutputAlphabet(t *testing.T) {
require.ErrorIs(t, config.Check(), ErrMissingRollupRpc) require.ErrorIs(t, config.Check(), ErrMissingRollupRpc)
} }
func TestCannotEnableBothCannonAndOutputCannonTraceTypes(t *testing.T) {
config := validConfig(TraceTypeOutputCannon)
config.TraceTypes = append(config.TraceTypes, TraceTypeCannon)
require.ErrorIs(t, config.Check(), ErrCannonAndOutputCannonConflict)
}
func TestCannonL2Required(t *testing.T) { func TestCannonL2Required(t *testing.T) {
config := validConfig(TraceTypeCannon) config := validConfig(TraceTypeCannon)
config.CannonL2 = "" config.CannonL2 = ""
...@@ -214,7 +208,7 @@ func TestNetworkMustBeValid(t *testing.T) { ...@@ -214,7 +208,7 @@ func TestNetworkMustBeValid(t *testing.T) {
func TestRequireConfigForMultipleTraceTypes(t *testing.T) { func TestRequireConfigForMultipleTraceTypes(t *testing.T) {
cfg := validConfig(TraceTypeCannon) cfg := validConfig(TraceTypeCannon)
cfg.TraceTypes = []TraceType{TraceTypeCannon, TraceTypeAlphabet} cfg.TraceTypes = []TraceType{TraceTypeCannon, TraceTypeAlphabet, TraceTypeOutputCannon}
// Set all required options and check its valid // Set all required options and check its valid
cfg.RollupRpc = validRollupRpc cfg.RollupRpc = validRollupRpc
cfg.AlphabetTrace = validAlphabetTrace cfg.AlphabetTrace = validAlphabetTrace
...@@ -228,4 +222,9 @@ func TestRequireConfigForMultipleTraceTypes(t *testing.T) { ...@@ -228,4 +222,9 @@ func TestRequireConfigForMultipleTraceTypes(t *testing.T) {
// Require alphabet specific args // Require alphabet specific args
cfg.AlphabetTrace = "" cfg.AlphabetTrace = ""
require.ErrorIs(t, cfg.Check(), ErrMissingAlphabetTrace) require.ErrorIs(t, cfg.Check(), ErrMissingAlphabetTrace)
cfg.AlphabetTrace = validAlphabetTrace
// Require output cannon specific args
cfg.RollupRpc = ""
require.ErrorIs(t, cfg.Check(), ErrMissingRollupRpc)
} }
...@@ -36,12 +36,7 @@ type GameContract interface { ...@@ -36,12 +36,7 @@ type GameContract interface {
GetMaxGameDepth(ctx context.Context) (uint64, error) GetMaxGameDepth(ctx context.Context) (uint64, error)
} }
type gameTypeResources interface { type resourceCreator func(ctx context.Context, logger log.Logger, gameDepth uint64, dir string) (types.TraceAccessor, error)
Contract() GameContract
CreateAccessor(ctx context.Context, logger log.Logger, gameDepth uint64, dir string) (types.TraceAccessor, error)
}
type resourceCreator func(addr common.Address) (gameTypeResources, error)
func NewGamePlayer( func NewGamePlayer(
ctx context.Context, ctx context.Context,
...@@ -50,17 +45,11 @@ func NewGamePlayer( ...@@ -50,17 +45,11 @@ func NewGamePlayer(
dir string, dir string,
addr common.Address, addr common.Address,
txMgr txmgr.TxManager, txMgr txmgr.TxManager,
loader GameContract,
creator resourceCreator, creator resourceCreator,
) (*GamePlayer, error) { ) (*GamePlayer, error) {
logger = logger.New("game", addr) logger = logger.New("game", addr)
resources, err := creator(addr)
if err != nil {
return nil, fmt.Errorf("failed to create game resources: %w", err)
}
loader := resources.Contract()
status, err := loader.GetStatus(ctx) status, err := loader.GetStatus(ctx)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to fetch game status: %w", err) return nil, fmt.Errorf("failed to fetch game status: %w", err)
...@@ -84,7 +73,7 @@ func NewGamePlayer( ...@@ -84,7 +73,7 @@ func NewGamePlayer(
return nil, fmt.Errorf("failed to fetch the game depth: %w", err) return nil, fmt.Errorf("failed to fetch the game depth: %w", err)
} }
accessor, err := resources.CreateAccessor(ctx, logger, gameDepth, dir) accessor, err := creator(ctx, logger, gameDepth, dir)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create trace accessor: %w", err) return nil, fmt.Errorf("failed to create trace accessor: %w", err)
} }
......
...@@ -16,14 +16,13 @@ import ( ...@@ -16,14 +16,13 @@ import (
"github.com/ethereum-optimism/optimism/op-challenger/metrics" "github.com/ethereum-optimism/optimism/op-challenger/metrics"
"github.com/ethereum-optimism/optimism/op-service/sources/batching" "github.com/ethereum-optimism/optimism/op-service/sources/batching"
"github.com/ethereum-optimism/optimism/op-service/txmgr" "github.com/ethereum-optimism/optimism/op-service/txmgr"
"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"
) )
var ( var (
cannonGameType = uint8(0) cannonGameType = uint8(0)
outputCannonGameType = uint8(0) // TODO(client-pod#260): Switch the output cannon game type to 1 outputCannonGameType = uint8(253) // TODO(client-pod#43): Switch the output cannon game type to 1
outputAlphabetGameType = uint8(254) outputAlphabetGameType = uint8(254)
alphabetGameType = uint8(255) alphabetGameType = uint8(255)
) )
...@@ -57,7 +56,7 @@ func RegisterGameTypes( ...@@ -57,7 +56,7 @@ func RegisterGameTypes(
registerOutputCannon(registry, ctx, logger, m, cfg, txMgr, caller, l2Client) registerOutputCannon(registry, ctx, logger, m, cfg, txMgr, caller, l2Client)
} }
if cfg.TraceTypeEnabled(config.TraceTypeOutputAlphabet) { if cfg.TraceTypeEnabled(config.TraceTypeOutputAlphabet) {
registerOutputAlphabet(registry, ctx, logger, m, cfg, txMgr, caller, l2Client) registerOutputAlphabet(registry, ctx, logger, m, cfg, txMgr, caller)
} }
if cfg.TraceTypeEnabled(config.TraceTypeCannon) { if cfg.TraceTypeEnabled(config.TraceTypeCannon) {
registerCannon(registry, ctx, logger, m, cfg, txMgr, caller, l2Client) registerCannon(registry, ctx, logger, m, cfg, txMgr, caller, l2Client)
...@@ -75,54 +74,33 @@ func registerOutputAlphabet( ...@@ -75,54 +74,33 @@ func registerOutputAlphabet(
m metrics.Metricer, m metrics.Metricer,
cfg *config.Config, cfg *config.Config,
txMgr txmgr.TxManager, txMgr txmgr.TxManager,
caller *batching.MultiCaller, caller *batching.MultiCaller) {
l2Client cannon.L2HeaderSource) { playerCreator := func(game types.GameMetadata, dir string) (scheduler.GamePlayer, error) {
resourceCreator := func(addr common.Address) (gameTypeResources, error) { contract, err := contracts.NewOutputBisectionGameContract(game.Proxy, caller)
contract, err := contracts.NewOutputBisectionGameContract(addr, caller)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &outputAlphabetResources{ creator := func(ctx context.Context, logger log.Logger, gameDepth uint64, dir string) (faultTypes.TraceAccessor, error) {
m: m, // TODO(client-pod#44): Validate absolute pre-state for split games
cfg: cfg, prestateBlock, poststateBlock, err := contract.GetBlockRange(ctx)
l2Client: l2Client, if err != nil {
contract: contract, return nil, err
}, nil }
} splitDepth, err := contract.GetSplitDepth(ctx)
playerCreator := func(game types.GameMetadata, dir string) (scheduler.GamePlayer, error) { if err != nil {
return NewGamePlayer(ctx, logger, m, dir, game.Proxy, txMgr, resourceCreator) return nil, err
}
accessor, err := outputs.NewOutputAlphabetTraceAccessor(ctx, logger, m, cfg, gameDepth, splitDepth, prestateBlock, poststateBlock)
if err != nil {
return nil, err
}
return accessor, nil
}
return NewGamePlayer(ctx, logger, m, dir, game.Proxy, txMgr, contract, creator)
} }
registry.RegisterGameType(outputAlphabetGameType, playerCreator) registry.RegisterGameType(outputAlphabetGameType, playerCreator)
} }
type outputAlphabetResources struct {
m metrics.Metricer
cfg *config.Config
l2Client cannon.L2HeaderSource
contract *contracts.OutputBisectionGameContract
}
func (r *outputAlphabetResources) Contract() GameContract {
return r.contract
}
func (r *outputAlphabetResources) CreateAccessor(ctx context.Context, logger log.Logger, gameDepth uint64, dir string) (faultTypes.TraceAccessor, error) {
// TODO(client-pod#44): Validate absolute pre-state for split games
prestateBlock, poststateBlock, err := r.contract.GetBlockRange(ctx)
if err != nil {
return nil, err
}
splitDepth, err := r.contract.GetSplitDepth(ctx)
if err != nil {
return nil, err
}
accessor, err := outputs.NewOutputAlphabetTraceAccessor(ctx, logger, r.m, r.cfg, gameDepth, splitDepth, prestateBlock, poststateBlock)
if err != nil {
return nil, err
}
return accessor, nil
}
func registerOutputCannon( func registerOutputCannon(
registry Registry, registry Registry,
ctx context.Context, ctx context.Context,
...@@ -132,51 +110,28 @@ func registerOutputCannon( ...@@ -132,51 +110,28 @@ func registerOutputCannon(
txMgr txmgr.TxManager, txMgr txmgr.TxManager,
caller *batching.MultiCaller, caller *batching.MultiCaller,
l2Client cannon.L2HeaderSource) { l2Client cannon.L2HeaderSource) {
resourceCreator := func(addr common.Address) (gameTypeResources, error) { playerCreator := func(game types.GameMetadata, dir string) (scheduler.GamePlayer, error) {
// Currently still using the old fault dispute game contracts for output_cannon contract, err := contracts.NewOutputBisectionGameContract(game.Proxy, caller)
// as the output bisection+cannon contract isn't being deployed.
contract, err := contracts.NewFaultDisputeGameContract(addr, caller)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &outputCannonResources{ creator := func(ctx context.Context, logger log.Logger, gameDepth uint64, dir string) (faultTypes.TraceAccessor, error) {
m: m, // TODO(client-pod#44): Validate absolute pre-state for split games
cfg: cfg, agreed, disputed, err := contract.GetBlockRange(ctx)
l2Client: l2Client, if err != nil {
contract: contract, return nil, err
}, nil }
} accessor, err := outputs.NewOutputCannonTraceAccessor(ctx, logger, m, cfg, l2Client, contract, dir, gameDepth, agreed, disputed)
playerCreator := func(game types.GameMetadata, dir string) (scheduler.GamePlayer, error) { if err != nil {
return NewGamePlayer(ctx, logger, m, dir, game.Proxy, txMgr, resourceCreator) return nil, err
}
return accessor, nil
}
return NewGamePlayer(ctx, logger, m, dir, game.Proxy, txMgr, contract, creator)
} }
registry.RegisterGameType(outputCannonGameType, playerCreator) registry.RegisterGameType(outputCannonGameType, playerCreator)
} }
type outputCannonResources struct {
m metrics.Metricer
cfg *config.Config
l2Client cannon.L2HeaderSource
contract *contracts.FaultDisputeGameContract // TODO(client-pod#260): Use the OutputBisectionGame Contract
}
func (r *outputCannonResources) Contract() GameContract {
return r.contract
}
func (r *outputCannonResources) CreateAccessor(ctx context.Context, logger log.Logger, gameDepth uint64, dir string) (faultTypes.TraceAccessor, error) {
// TODO(client-pod#44): Validate absolute pre-state for split games
// TODO(client-pod#43): Updated contracts should expose this as the pre and post state blocks
agreed, disputed, err := r.contract.GetProposals(ctx)
if err != nil {
return nil, err
}
accessor, err := outputs.NewOutputCannonTraceAccessor(ctx, logger, r.m, r.cfg, r.l2Client, r.contract, dir, gameDepth, agreed.L2BlockNumber.Uint64(), disputed.L2BlockNumber.Uint64())
if err != nil {
return nil, err
}
return accessor, nil
}
func registerCannon( func registerCannon(
registry Registry, registry Registry,
ctx context.Context, ctx context.Context,
...@@ -186,47 +141,27 @@ func registerCannon( ...@@ -186,47 +141,27 @@ func registerCannon(
txMgr txmgr.TxManager, txMgr txmgr.TxManager,
caller *batching.MultiCaller, caller *batching.MultiCaller,
l2Client cannon.L2HeaderSource) { l2Client cannon.L2HeaderSource) {
resourceCreator := func(addr common.Address) (gameTypeResources, error) { playerCreator := func(game types.GameMetadata, dir string) (scheduler.GamePlayer, error) {
contract, err := contracts.NewFaultDisputeGameContract(addr, caller) contract, err := contracts.NewFaultDisputeGameContract(game.Proxy, caller)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &cannonResources{ creator := func(ctx context.Context, logger log.Logger, gameDepth uint64, dir string) (faultTypes.TraceAccessor, error) {
m: m, localInputs, err := cannon.FetchLocalInputs(ctx, contract, l2Client)
cfg: cfg, if err != nil {
l2Client: l2Client, return nil, fmt.Errorf("failed to fetch cannon local inputs: %w", err)
contract: contract, }
}, nil provider := cannon.NewTraceProvider(logger, m, cfg, faultTypes.NoLocalContext, localInputs, dir, gameDepth)
} if err := ValidateAbsolutePrestate(ctx, provider, contract); err != nil {
playerCreator := func(game types.GameMetadata, dir string) (scheduler.GamePlayer, error) { return nil, err
return NewGamePlayer(ctx, logger, m, dir, game.Proxy, txMgr, resourceCreator) }
return trace.NewSimpleTraceAccessor(provider), nil
}
return NewGamePlayer(ctx, logger, m, dir, game.Proxy, txMgr, contract, creator)
} }
registry.RegisterGameType(cannonGameType, playerCreator) registry.RegisterGameType(cannonGameType, playerCreator)
} }
type cannonResources struct {
m metrics.Metricer
cfg *config.Config
l2Client cannon.L2HeaderSource
contract *contracts.FaultDisputeGameContract
}
func (r *cannonResources) Contract() GameContract {
return r.contract
}
func (r *cannonResources) CreateAccessor(ctx context.Context, logger log.Logger, gameDepth uint64, dir string) (faultTypes.TraceAccessor, error) {
localInputs, err := cannon.FetchLocalInputs(ctx, r.contract, r.l2Client)
if err != nil {
return nil, fmt.Errorf("failed to fetch cannon local inputs: %w", err)
}
provider := cannon.NewTraceProvider(logger, r.m, r.cfg, faultTypes.NoLocalContext, localInputs, dir, gameDepth)
if err := ValidateAbsolutePrestate(ctx, provider, r.contract); err != nil {
return nil, err
}
return trace.NewSimpleTraceAccessor(provider), nil
}
func registerAlphabet( func registerAlphabet(
registry Registry, registry Registry,
ctx context.Context, ctx context.Context,
...@@ -235,35 +170,19 @@ func registerAlphabet( ...@@ -235,35 +170,19 @@ func registerAlphabet(
cfg *config.Config, cfg *config.Config,
txMgr txmgr.TxManager, txMgr txmgr.TxManager,
caller *batching.MultiCaller) { caller *batching.MultiCaller) {
resourceCreator := func(addr common.Address) (gameTypeResources, error) { playerCreator := func(game types.GameMetadata, dir string) (scheduler.GamePlayer, error) {
contract, err := contracts.NewFaultDisputeGameContract(addr, caller) contract, err := contracts.NewFaultDisputeGameContract(game.Proxy, caller)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &alphabetResources{ creator := func(ctx context.Context, logger log.Logger, gameDepth uint64, dir string) (faultTypes.TraceAccessor, error) {
cfg: cfg, provider := alphabet.NewTraceProvider(cfg.AlphabetTrace, gameDepth)
contract: contract, if err := ValidateAbsolutePrestate(ctx, provider, contract); err != nil {
}, nil return nil, err
} }
playerCreator := func(game types.GameMetadata, dir string) (scheduler.GamePlayer, error) { return trace.NewSimpleTraceAccessor(provider), nil
return NewGamePlayer(ctx, logger, m, dir, game.Proxy, txMgr, resourceCreator) }
return NewGamePlayer(ctx, logger, m, dir, game.Proxy, txMgr, contract, creator)
} }
registry.RegisterGameType(alphabetGameType, playerCreator) registry.RegisterGameType(alphabetGameType, playerCreator)
} }
type alphabetResources struct {
cfg *config.Config
contract *contracts.FaultDisputeGameContract
}
func (r *alphabetResources) Contract() GameContract {
return r.contract
}
func (r *alphabetResources) CreateAccessor(ctx context.Context, _ log.Logger, gameDepth uint64, _ string) (faultTypes.TraceAccessor, error) {
provider := alphabet.NewTraceProvider(r.cfg.AlphabetTrace, gameDepth)
if err := ValidateAbsolutePrestate(ctx, provider, r.contract); err != nil {
return nil, err
}
return trace.NewSimpleTraceAccessor(provider), nil
}
...@@ -17,8 +17,6 @@ import ( ...@@ -17,8 +17,6 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
const defaultTimeout = 5 * time.Minute
type FaultGameHelper struct { type FaultGameHelper struct {
t *testing.T t *testing.T
require *require.Assertions require *require.Assertions
...@@ -59,14 +57,6 @@ func (g *FaultGameHelper) WaitForClaimCount(ctx context.Context, count int64) { ...@@ -59,14 +57,6 @@ func (g *FaultGameHelper) WaitForClaimCount(ctx context.Context, count int64) {
} }
} }
type ContractClaim struct {
ParentIndex uint32
Countered bool
Claim [32]byte
Position *big.Int
Clock *big.Int
}
func (g *FaultGameHelper) MaxDepth(ctx context.Context) int64 { func (g *FaultGameHelper) MaxDepth(ctx context.Context) int64 {
depth, err := g.game.MAXGAMEDEPTH(&bind.CallOpts{Context: ctx}) depth, err := g.game.MAXGAMEDEPTH(&bind.CallOpts{Context: ctx})
g.require.NoError(err, "Failed to load game depth") g.require.NoError(err, "Failed to load game depth")
...@@ -252,12 +242,6 @@ func (g *FaultGameHelper) WaitForInactivity(ctx context.Context, numInactiveBloc ...@@ -252,12 +242,6 @@ func (g *FaultGameHelper) WaitForInactivity(ctx context.Context, numInactiveBloc
} }
} }
// Mover is a function that either attacks or defends the claim at parentClaimIdx
type Mover func(parentClaimIdx int64)
// Stepper is a function that attempts to perform a step against the claim at parentClaimIdx
type Stepper func(parentClaimIdx int64)
// DefendRootClaim uses the supplied Mover to perform moves in an attempt to defend the root claim. // DefendRootClaim uses the supplied Mover to perform moves in an attempt to defend the root claim.
// It is assumed that the output root being disputed is valid and that an honest op-challenger is already running. // It is assumed that the output root being disputed is valid and that an honest op-challenger is already running.
// When the game has reached the maximum depth it waits for the honest challenger to counter the leaf claim with step. // When the game has reached the maximum depth it waits for the honest challenger to counter the leaf claim with step.
...@@ -337,10 +321,6 @@ func (g *FaultGameHelper) Defend(ctx context.Context, claimIdx int64, claim comm ...@@ -337,10 +321,6 @@ func (g *FaultGameHelper) Defend(ctx context.Context, claimIdx int64, claim comm
g.require.NoError(err, "Defend transaction was not OK") g.require.NoError(err, "Defend transaction was not OK")
} }
type ErrWithData interface {
ErrorData() interface{}
}
// StepFails attempts to call step and verifies that it fails with ValidStep() // StepFails attempts to call step and verifies that it fails with ValidStep()
func (g *FaultGameHelper) StepFails(claimIdx int64, isAttack bool, stateData []byte, proof []byte) { func (g *FaultGameHelper) StepFails(claimIdx int64, isAttack bool, stateData []byte, proof []byte) {
g.t.Logf("Attempting step against claim %v isAttack: %v", claimIdx, isAttack) g.t.Logf("Attempting step against claim %v isAttack: %v", claimIdx, isAttack)
......
...@@ -22,6 +22,7 @@ import ( ...@@ -22,6 +22,7 @@ import (
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/wait" "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/wait"
"github.com/ethereum-optimism/optimism/op-node/rollup" "github.com/ethereum-optimism/optimism/op-node/rollup"
"github.com/ethereum-optimism/optimism/op-service/dial" "github.com/ethereum-optimism/optimism/op-service/dial"
"github.com/ethereum-optimism/optimism/op-service/sources"
"github.com/ethereum-optimism/optimism/op-service/testlog" "github.com/ethereum-optimism/optimism/op-service/testlog"
"github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
...@@ -34,7 +35,7 @@ import ( ...@@ -34,7 +35,7 @@ import (
const alphabetGameType uint8 = 255 const alphabetGameType uint8 = 255
const cannonGameType uint8 = 0 const cannonGameType uint8 = 0
const outputCannonGameType uint8 = 0 // TODO(client-pod#43): This should be a unique game type const outputCannonGameType uint8 = 253 // TODO(client-pod#43): Switch this game type to 1
const alphabetGameDepth = 4 const alphabetGameDepth = 4
var lastAlphabetTraceIndex = big.NewInt(1<<alphabetGameDepth - 1) var lastAlphabetTraceIndex = big.NewInt(1<<alphabetGameDepth - 1)
...@@ -140,7 +141,10 @@ func (h *FactoryHelper) StartAlphabetGame(ctx context.Context, claimedAlphabet s ...@@ -140,7 +141,10 @@ func (h *FactoryHelper) StartAlphabetGame(ctx context.Context, claimedAlphabet s
} }
func (h *FactoryHelper) StartOutputCannonGame(ctx context.Context, rollupEndpoint string, rootClaim common.Hash) *OutputCannonGameHelper { func (h *FactoryHelper) StartOutputCannonGame(ctx context.Context, rollupEndpoint string, rootClaim common.Hash) *OutputCannonGameHelper {
extraData, _, _ := h.createDisputeGameExtraData(ctx) rollupClient, err := dial.DialRollupClientWithTimeout(ctx, 30*time.Second, testlog.Logger(h.t, log.LvlInfo), rollupEndpoint)
h.require.NoError(err)
extraData, _ := h.createBisectionGameExtraData(ctx, rollupClient)
ctx, cancel := context.WithTimeout(ctx, 1*time.Minute) ctx, cancel := context.WithTimeout(ctx, 1*time.Minute)
defer cancel() defer cancel()
...@@ -154,23 +158,20 @@ func (h *FactoryHelper) StartOutputCannonGame(ctx context.Context, rollupEndpoin ...@@ -154,23 +158,20 @@ func (h *FactoryHelper) StartOutputCannonGame(ctx context.Context, rollupEndpoin
h.require.Len(rcpt.Logs, 1, "should have emitted a single DisputeGameCreated event") h.require.Len(rcpt.Logs, 1, "should have emitted a single DisputeGameCreated event")
createdEvent, err := h.factory.ParseDisputeGameCreated(*rcpt.Logs[0]) createdEvent, err := h.factory.ParseDisputeGameCreated(*rcpt.Logs[0])
h.require.NoError(err) h.require.NoError(err)
game, err := bindings.NewFaultDisputeGame(createdEvent.DisputeProxy, h.client) game, err := bindings.NewOutputBisectionGame(createdEvent.DisputeProxy, h.client)
h.require.NoError(err)
rollupClient, err := dial.DialRollupClientWithTimeout(ctx, 30*time.Second, testlog.Logger(h.t, log.LvlInfo), rollupEndpoint)
h.require.NoError(err) h.require.NoError(err)
return &OutputCannonGameHelper{ return &OutputCannonGameHelper{
FaultGameHelper: FaultGameHelper{ OutputGameHelper: OutputGameHelper{
t: h.t, t: h.t,
require: h.require, require: h.require,
client: h.client, client: h.client,
opts: h.opts, opts: h.opts,
game: game, game: game,
factoryAddr: h.factoryAddr, factoryAddr: h.factoryAddr,
addr: createdEvent.DisputeProxy, addr: createdEvent.DisputeProxy,
rollupClient: rollupClient,
}, },
rollupClient: rollupClient,
} }
} }
...@@ -277,6 +278,15 @@ func (h *FactoryHelper) createCannonGame(ctx context.Context, rootClaim common.H ...@@ -277,6 +278,15 @@ func (h *FactoryHelper) createCannonGame(ctx context.Context, rootClaim common.H
} }
} }
func (h *FactoryHelper) createBisectionGameExtraData(ctx context.Context, client *sources.RollupClient) (extraData []byte, l2BlockNumber uint64) {
syncStatus, err := client.SyncStatus(ctx)
h.require.NoError(err, "failed to get sync status")
l2BlockNumber = syncStatus.SafeL2.Number
extraData = make([]byte, 32)
binary.BigEndian.PutUint64(extraData, l2BlockNumber)
return
}
func (h *FactoryHelper) createDisputeGameExtraData(ctx context.Context) (extraData []byte, l1Head *big.Int, l2BlockNumber uint64) { func (h *FactoryHelper) createDisputeGameExtraData(ctx context.Context) (extraData []byte, l1Head *big.Int, l2BlockNumber uint64) {
l2BlockNumber = h.waitForProposals(ctx) l2BlockNumber = h.waitForProposals(ctx)
l1Head = h.checkpointL1Block(ctx) l1Head = h.checkpointL1Block(ctx)
......
...@@ -2,19 +2,14 @@ package disputegame ...@@ -2,19 +2,14 @@ package disputegame
import ( import (
"context" "context"
"math/big"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/challenger" "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/challenger"
"github.com/ethereum-optimism/optimism/op-node/rollup" "github.com/ethereum-optimism/optimism/op-node/rollup"
"github.com/ethereum-optimism/optimism/op-service/sources"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core"
) )
type OutputCannonGameHelper struct { type OutputCannonGameHelper struct {
FaultGameHelper OutputGameHelper
rollupClient *sources.RollupClient
} }
func (g *OutputCannonGameHelper) StartChallenger( func (g *OutputCannonGameHelper) StartChallenger(
...@@ -39,30 +34,3 @@ func (g *OutputCannonGameHelper) StartChallenger( ...@@ -39,30 +34,3 @@ func (g *OutputCannonGameHelper) StartChallenger(
}) })
return c return c
} }
func (g *OutputCannonGameHelper) WaitForCorrectOutputRoot(ctx context.Context, claimIdx int64) {
g.WaitForClaimCount(ctx, claimIdx+1)
claim := g.getClaim(ctx, claimIdx)
err, blockNum := g.blockNumForClaim(ctx, claim)
g.require.NoError(err)
output, err := g.rollupClient.OutputAtBlock(ctx, blockNum)
g.require.NoErrorf(err, "Failed to get output at block %v", blockNum)
g.require.EqualValuesf(output.OutputRoot, claim.Claim, "Incorrect output root at claim %v. Expected to be from block %v", claimIdx, blockNum)
}
func (g *OutputCannonGameHelper) blockNumForClaim(ctx context.Context, claim ContractClaim) (error, uint64) {
proposals, err := g.game.Proposals(&bind.CallOpts{Context: ctx})
g.require.NoError(err, "failed to retrieve proposals")
prestateBlockNum := proposals.Starting.L2BlockNumber
disputedBlockNum := proposals.Disputed.L2BlockNumber
gameDepth := g.MaxDepth(ctx)
// TODO(client-pod#43): Load this from the contract
topDepth := gameDepth / 2
traceIdx := types.NewPositionFromGIndex(claim.Position).TraceIndex(int(topDepth))
blockNum := new(big.Int).Add(prestateBlockNum, traceIdx).Uint64() + 1
if blockNum > disputedBlockNum.Uint64() {
blockNum = disputedBlockNum.Uint64()
}
return err, blockNum
}
package disputegame
import (
"context"
"fmt"
"math/big"
"testing"
"time"
"github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/wait"
"github.com/ethereum-optimism/optimism/op-service/sources"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
gethtypes "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/stretchr/testify/require"
)
const defaultTimeout = 5 * time.Minute
type OutputGameHelper struct {
t *testing.T
require *require.Assertions
client *ethclient.Client
opts *bind.TransactOpts
game *bindings.OutputBisectionGame
factoryAddr common.Address
addr common.Address
rollupClient *sources.RollupClient
}
func (g *OutputGameHelper) Addr() common.Address {
return g.addr
}
func (g *OutputGameHelper) SplitDepth(ctx context.Context) int64 {
splitDepth, err := g.game.SPLITDEPTH(&bind.CallOpts{Context: ctx})
g.require.NoError(err, "failed to load split depth")
return splitDepth.Int64()
}
func (g *OutputGameHelper) WaitForCorrectOutputRoot(ctx context.Context, claimIdx int64) {
g.WaitForClaimCount(ctx, claimIdx+1)
claim := g.getClaim(ctx, claimIdx)
err, blockNum := g.blockNumForClaim(ctx, claim)
g.require.NoError(err)
output, err := g.rollupClient.OutputAtBlock(ctx, blockNum)
g.require.NoErrorf(err, "Failed to get output at block %v", blockNum)
g.require.EqualValuesf(output.OutputRoot, claim.Claim, "Incorrect output root at claim %v. Expected to be from block %v", claimIdx, blockNum)
}
func (g *OutputGameHelper) blockNumForClaim(ctx context.Context, claim ContractClaim) (error, uint64) {
opts := &bind.CallOpts{Context: ctx}
prestateBlockNum, err := g.game.GENESISBLOCKNUMBER(opts)
g.require.NoError(err, "failed to retrieve proposals")
disputedBlockNum, err := g.game.L2BlockNumber(opts)
g.require.NoError(err, "failed to retrieve l2 block number")
topDepth := g.SplitDepth(ctx)
traceIdx := types.NewPositionFromGIndex(claim.Position).TraceIndex(int(topDepth))
blockNum := new(big.Int).Add(prestateBlockNum, traceIdx).Uint64() + 1
if blockNum > disputedBlockNum.Uint64() {
blockNum = disputedBlockNum.Uint64()
}
return err, blockNum
}
func (g *OutputGameHelper) GameDuration(ctx context.Context) time.Duration {
duration, err := g.game.GAMEDURATION(&bind.CallOpts{Context: ctx})
g.require.NoError(err, "failed to get game duration")
return time.Duration(duration) * time.Second
}
// WaitForClaimCount waits until there are at least count claims in the game.
// This does not check that the number of claims is exactly the specified count to avoid intermittent failures
// where a challenger posts an additional claim before this method sees the number of claims it was waiting for.
func (g *OutputGameHelper) WaitForClaimCount(ctx context.Context, count int64) {
timedCtx, cancel := context.WithTimeout(ctx, defaultTimeout)
defer cancel()
err := wait.For(timedCtx, time.Second, func() (bool, error) {
actual, err := g.game.ClaimDataLen(&bind.CallOpts{Context: timedCtx})
if err != nil {
return false, err
}
g.t.Log("Waiting for claim count", "current", actual, "expected", count, "game", g.addr)
return actual.Cmp(big.NewInt(count)) >= 0, nil
})
if err != nil {
g.LogGameData(ctx)
g.require.NoErrorf(err, "Did not find expected claim count %v", count)
}
}
type ContractClaim struct {
ParentIndex uint32
Countered bool
Claim [32]byte
Position *big.Int
Clock *big.Int
}
func (g *OutputGameHelper) MaxDepth(ctx context.Context) int64 {
depth, err := g.game.MAXGAMEDEPTH(&bind.CallOpts{Context: ctx})
g.require.NoError(err, "Failed to load game depth")
return depth.Int64()
}
func (g *OutputGameHelper) waitForClaim(ctx context.Context, errorMsg string, predicate func(claim ContractClaim) bool) {
timedCtx, cancel := context.WithTimeout(ctx, defaultTimeout)
defer cancel()
err := wait.For(timedCtx, time.Second, func() (bool, error) {
count, err := g.game.ClaimDataLen(&bind.CallOpts{Context: timedCtx})
if err != nil {
return false, fmt.Errorf("retrieve number of claims: %w", err)
}
// Search backwards because the new claims are at the end and more likely the ones we want.
for i := count.Int64() - 1; i >= 0; i-- {
claimData, err := g.game.ClaimData(&bind.CallOpts{Context: timedCtx}, big.NewInt(i))
if err != nil {
return false, fmt.Errorf("retrieve claim %v: %w", i, err)
}
if predicate(claimData) {
return true, nil
}
}
return false, nil
})
if err != nil { // Avoid waiting time capturing game data when there's no error
g.require.NoErrorf(err, "%v\n%v", errorMsg, g.gameData(ctx))
}
}
func (g *OutputGameHelper) waitForNoClaim(ctx context.Context, errorMsg string, predicate func(claim ContractClaim) bool) {
timedCtx, cancel := context.WithTimeout(ctx, defaultTimeout)
defer cancel()
err := wait.For(timedCtx, time.Second, func() (bool, error) {
count, err := g.game.ClaimDataLen(&bind.CallOpts{Context: timedCtx})
if err != nil {
return false, fmt.Errorf("retrieve number of claims: %w", err)
}
// Search backwards because the new claims are at the end and more likely the ones we will fail on.
for i := count.Int64() - 1; i >= 0; i-- {
claimData, err := g.game.ClaimData(&bind.CallOpts{Context: timedCtx}, big.NewInt(i))
if err != nil {
return false, fmt.Errorf("retrieve claim %v: %w", i, err)
}
if predicate(claimData) {
return false, nil
}
}
return true, nil
})
if err != nil { // Avoid waiting time capturing game data when there's no error
g.require.NoErrorf(err, "%v\n%v", errorMsg, g.gameData(ctx))
}
}
func (g *OutputGameHelper) GetClaimValue(ctx context.Context, claimIdx int64) common.Hash {
g.WaitForClaimCount(ctx, claimIdx+1)
claim := g.getClaim(ctx, claimIdx)
return claim.Claim
}
func (g *OutputGameHelper) GetClaimPosition(ctx context.Context, claimIdx int64) types.Position {
g.WaitForClaimCount(ctx, claimIdx+1)
claim := g.getClaim(ctx, claimIdx)
return types.NewPositionFromGIndex(claim.Position)
}
// getClaim retrieves the claim data for a specific index.
// Note that it is deliberately not exported as tests should use WaitForClaim to avoid race conditions.
func (g *OutputGameHelper) getClaim(ctx context.Context, claimIdx int64) ContractClaim {
claimData, err := g.game.ClaimData(&bind.CallOpts{Context: ctx}, big.NewInt(claimIdx))
if err != nil {
g.require.NoErrorf(err, "retrieve claim %v", claimIdx)
}
return claimData
}
func (g *OutputGameHelper) WaitForClaimAtDepth(ctx context.Context, depth int) {
g.waitForClaim(
ctx,
fmt.Sprintf("Could not find claim depth %v", depth),
func(claim ContractClaim) bool {
pos := types.NewPositionFromGIndex(claim.Position)
return pos.Depth() == depth
})
}
func (g *OutputGameHelper) WaitForClaimAtMaxDepth(ctx context.Context, countered bool) {
maxDepth := g.MaxDepth(ctx)
g.waitForClaim(
ctx,
fmt.Sprintf("Could not find claim depth %v with countered=%v", maxDepth, countered),
func(claim ContractClaim) bool {
pos := types.NewPositionFromGIndex(claim.Position)
return int64(pos.Depth()) == maxDepth && claim.Countered == countered
})
}
func (g *OutputGameHelper) WaitForAllClaimsCountered(ctx context.Context) {
g.waitForNoClaim(
ctx,
"Did not find all claims countered",
func(claim ContractClaim) bool {
return !claim.Countered
})
}
func (g *OutputGameHelper) Resolve(ctx context.Context) {
ctx, cancel := context.WithTimeout(ctx, time.Minute)
defer cancel()
tx, err := g.game.Resolve(g.opts)
g.require.NoError(err)
_, err = wait.ForReceiptOK(ctx, g.client, tx.Hash())
g.require.NoError(err)
}
func (g *OutputGameHelper) Status(ctx context.Context) Status {
status, err := g.game.Status(&bind.CallOpts{Context: ctx})
g.require.NoError(err)
return Status(status)
}
func (g *OutputGameHelper) WaitForGameStatus(ctx context.Context, expected Status) {
g.t.Logf("Waiting for game %v to have status %v", g.addr, expected)
timedCtx, cancel := context.WithTimeout(ctx, defaultTimeout)
defer cancel()
err := wait.For(timedCtx, time.Second, func() (bool, error) {
ctx, cancel := context.WithTimeout(timedCtx, 30*time.Second)
defer cancel()
status, err := g.game.Status(&bind.CallOpts{Context: ctx})
if err != nil {
return false, fmt.Errorf("game status unavailable: %w", err)
}
g.t.Logf("Game %v has state %v, waiting for state %v", g.addr, Status(status), expected)
return expected == Status(status), nil
})
g.require.NoErrorf(err, "wait for game status. Game state: \n%v", g.gameData(ctx))
}
func (g *OutputGameHelper) WaitForInactivity(ctx context.Context, numInactiveBlocks int, untilGameEnds bool) {
g.t.Logf("Waiting for game %v to have no activity for %v blocks", g.addr, numInactiveBlocks)
headCh := make(chan *gethtypes.Header, 100)
headSub, err := g.client.SubscribeNewHead(ctx, headCh)
g.require.NoError(err)
defer headSub.Unsubscribe()
var lastActiveBlock uint64
for {
if untilGameEnds && g.Status(ctx) != StatusInProgress {
break
}
select {
case head := <-headCh:
if lastActiveBlock == 0 {
lastActiveBlock = head.Number.Uint64()
continue
} else if lastActiveBlock+uint64(numInactiveBlocks) < head.Number.Uint64() {
return
}
block, err := g.client.BlockByNumber(ctx, head.Number)
g.require.NoError(err)
numActions := 0
for _, tx := range block.Transactions() {
if tx.To().Hex() == g.addr.Hex() {
numActions++
}
}
if numActions != 0 {
g.t.Logf("Game %v has %v actions in block %d. Resetting inactivity timeout", g.addr, numActions, block.NumberU64())
lastActiveBlock = head.Number.Uint64()
}
case err := <-headSub.Err():
g.require.NoError(err)
case <-ctx.Done():
g.require.Fail("Context canceled", ctx.Err())
}
}
}
// Mover is a function that either attacks or defends the claim at parentClaimIdx
type Mover func(parentClaimIdx int64)
// Stepper is a function that attempts to perform a step against the claim at parentClaimIdx
type Stepper func(parentClaimIdx int64)
// DefendRootClaim uses the supplied Mover to perform moves in an attempt to defend the root claim.
// It is assumed that the output root being disputed is valid and that an honest op-challenger is already running.
// When the game has reached the maximum depth it waits for the honest challenger to counter the leaf claim with step.
func (g *OutputGameHelper) DefendRootClaim(ctx context.Context, performMove Mover) {
maxDepth := g.MaxDepth(ctx)
for claimCount := int64(1); claimCount < maxDepth; {
g.LogGameData(ctx)
claimCount++
// Wait for the challenger to counter
g.WaitForClaimCount(ctx, claimCount)
// Respond with our own move
performMove(claimCount - 1)
claimCount++
g.WaitForClaimCount(ctx, claimCount)
}
// Wait for the challenger to call step and counter our invalid claim
g.WaitForClaimAtMaxDepth(ctx, true)
}
// ChallengeRootClaim uses the supplied Mover and Stepper to perform moves and steps in an attempt to challenge the root claim.
// It is assumed that the output root being disputed is invalid and that an honest op-challenger is already running.
// When the game has reached the maximum depth it calls the Stepper to attempt to counter the leaf claim.
// Since the output root is invalid, it should not be possible for the Stepper to call step successfully.
func (g *OutputGameHelper) ChallengeRootClaim(ctx context.Context, performMove Mover, attemptStep Stepper) {
maxDepth := g.MaxDepth(ctx)
for claimCount := int64(1); claimCount < maxDepth; {
g.LogGameData(ctx)
// Perform our move
performMove(claimCount - 1)
claimCount++
g.WaitForClaimCount(ctx, claimCount)
// Wait for the challenger to counter
claimCount++
g.WaitForClaimCount(ctx, claimCount)
}
// Confirm the game has reached max depth and the last claim hasn't been countered
g.WaitForClaimAtMaxDepth(ctx, false)
g.LogGameData(ctx)
// It's on us to call step if we want to win but shouldn't be possible
attemptStep(maxDepth)
}
func (g *OutputGameHelper) WaitForNewClaim(ctx context.Context, checkPoint int64) (int64, error) {
return g.waitForNewClaim(ctx, checkPoint, defaultTimeout)
}
func (g *OutputGameHelper) waitForNewClaim(ctx context.Context, checkPoint int64, timeout time.Duration) (int64, error) {
timedCtx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
var newClaimLen int64
err := wait.For(timedCtx, time.Second, func() (bool, error) {
actual, err := g.game.ClaimDataLen(&bind.CallOpts{Context: ctx})
if err != nil {
return false, err
}
newClaimLen = actual.Int64()
return actual.Cmp(big.NewInt(checkPoint)) > 0, nil
})
return newClaimLen, err
}
func (g *OutputGameHelper) Attack(ctx context.Context, claimIdx int64, claim common.Hash) {
tx, err := g.game.Attack(g.opts, big.NewInt(claimIdx), claim)
g.require.NoError(err, "Attack transaction did not send")
_, err = wait.ForReceiptOK(ctx, g.client, tx.Hash())
g.require.NoError(err, "Attack transaction was not OK")
}
func (g *OutputGameHelper) Defend(ctx context.Context, claimIdx int64, claim common.Hash) {
tx, err := g.game.Defend(g.opts, big.NewInt(claimIdx), claim)
g.require.NoError(err, "Defend transaction did not send")
_, err = wait.ForReceiptOK(ctx, g.client, tx.Hash())
g.require.NoError(err, "Defend transaction was not OK")
}
type ErrWithData interface {
ErrorData() interface{}
}
// StepFails attempts to call step and verifies that it fails with ValidStep()
func (g *OutputGameHelper) StepFails(claimIdx int64, isAttack bool, stateData []byte, proof []byte) {
g.t.Logf("Attempting step against claim %v isAttack: %v", claimIdx, isAttack)
_, err := g.game.Step(g.opts, big.NewInt(claimIdx), isAttack, stateData, proof)
errData, ok := err.(ErrWithData)
g.require.Truef(ok, "Error should provide ErrorData method: %v", err)
g.require.Equal("0xfb4e40dd", errData.ErrorData(), "Revert reason should be abi encoded ValidStep()")
}
// ResolveClaim resolves a single subgame
func (g *OutputGameHelper) ResolveClaim(ctx context.Context, claimIdx int64) {
tx, err := g.game.ResolveClaim(g.opts, big.NewInt(claimIdx))
g.require.NoError(err, "ResolveClaim transaction did not send")
_, err = wait.ForReceiptOK(ctx, g.client, tx.Hash())
g.require.NoError(err, "ResolveClaim transaction was not OK")
}
func (g *OutputGameHelper) gameData(ctx context.Context) string {
opts := &bind.CallOpts{Context: ctx}
maxDepth := int(g.MaxDepth(ctx))
splitDepth := int(g.SplitDepth(ctx))
claimCount, err := g.game.ClaimDataLen(opts)
info := fmt.Sprintf("Claim count: %v\n", claimCount)
g.require.NoError(err, "Fetching claim count")
for i := int64(0); i < claimCount.Int64(); i++ {
claim, err := g.game.ClaimData(opts, big.NewInt(i))
g.require.NoErrorf(err, "Fetch claim %v", i)
pos := types.NewPositionFromGIndex(claim.Position)
info = info + fmt.Sprintf("%v - Position: %v, Depth: %v, IndexAtDepth: %v Trace Index: %v, Value: %v, Countered: %v, ParentIndex: %v\n",
i, claim.Position.Int64(), pos.Depth(), pos.IndexAtDepth(), pos.TraceIndex(maxDepth), common.Hash(claim.Claim).Hex(), claim.Countered, claim.ParentIndex)
}
status, err := g.game.Status(opts)
g.require.NoError(err, "Load game status")
return fmt.Sprintf("Game %v - %v - Split Depth: %v - Max Depth: %v:\n%v\n",
g.addr, Status(status), splitDepth, maxDepth, info)
}
func (g *OutputGameHelper) LogGameData(ctx context.Context) {
g.t.Log(g.gameData(ctx))
}
...@@ -31,12 +31,19 @@ func TestOutputCannonGame(t *testing.T) { ...@@ -31,12 +31,19 @@ func TestOutputCannonGame(t *testing.T) {
) )
game.LogGameData(ctx) game.LogGameData(ctx)
maxDepth := game.MaxDepth(ctx)
// Challenger should post an output root to counter claims down to the leaf level of the top game // Challenger should post an output root to counter claims down to the leaf level of the top game
// TODO(client-pod#43): Load the depth of the top game from the contract instead of deriving it splitDepth := game.SplitDepth(ctx)
for i := int64(1); i <= maxDepth/2+1; i += 2 { for i := int64(1); i < splitDepth; i += 2 {
game.WaitForCorrectOutputRoot(ctx, i) game.WaitForCorrectOutputRoot(ctx, i)
game.Attack(ctx, i, common.Hash{0xaa}) game.Attack(ctx, i, common.Hash{0xaa})
game.LogGameData(ctx) game.LogGameData(ctx)
} }
game.WaitForCorrectOutputRoot(ctx, splitDepth)
// Post the first cannon output root (with 01 status code to show the output root is invalid)
game.Attack(ctx, splitDepth, common.Hash{0x01})
// Challenger should counter
game.WaitForClaimAtDepth(ctx, int(splitDepth+2))
game.LogGameData(ctx)
} }
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